Python · FastAPI · httpx

Integración de VeriFactu con Python y FastAPI

Si estás desarrollando un ERP, un SaaS de facturación o cualquier software que necesite cumplir con la normativa VeriFactu, esta guía te muestra el camino más corto desde cero hasta emitir tu primera factura verificada con Python.

VeriFactuAPI es un servicio SaaS que abstrae toda la complejidad del protocolo de la AEAT: la generación del XML firmado, la cadena de huellas, el envío y la gestión de respuestas. Tú llamas a una API REST y nosotros nos encargamos del resto. No necesitas certificados propios, ni lidiar con los XSD de Hacienda, ni mantener la infraestructura de envío.

Esta guía muestra cómo integrar VeriFactuAPI dentro de una aplicación FastAPI, usando httpx como cliente HTTP asíncrono y Pydantic para modelar los datos de factura.

Requisitos e instalación

Instala las dependencias necesarias:

BASH
pip install fastapi httpx uvicorn
  • FastAPI — framework web para construir los endpoints de tu aplicación.
  • httpx — cliente HTTP asíncrono para llamar a la API de VeriFactuAPI.
  • uvicorn — servidor ASGI para ejecutar la aplicación.

Necesitas además:

  • Python 3.9 o superior
  • Una cuenta en verifactuapi.es (el plan gratuito incluye entorno de pruebas)

Flujo completo: de la autenticación a la factura registrada

El flujo para enviar una factura tiene tres pasos: autenticarte, generar el registro de facturación y comprobar su estado en la AEAT. Esta comprobación puede hacerse mediante webhook (recomendado) o con consulta manual (polling).

Estructura del proyecto

TEXT
mi_proyecto/
├── main.py          # Aplicación FastAPI con los endpoints
├── verifactu.py     # Cliente para la API de VerifactuAPI
└── models.py        # Modelos Pydantic

Paso 1 — Autenticación y obtención del token

verifactu.py — cliente base

Python
import httpx

VERIFACTU_BASE_URL = "https://app.verifactuapi.es/api"


async def login(email: str, password: str) -> str:
    async with httpx.AsyncClient(timeout=30) as client:
        try:
            response = await client.post(
                f"{VERIFACTU_BASE_URL}/login",
                json={"email": email, "password": password},
                headers={"Accept": "application/json"},
            )
        except httpx.RequestError as e:
            raise RuntimeError(f"Error de red en login: {e}") from e

    try:
        data = response.json()
    except Exception as e:
        raise RuntimeError("Respuesta JSON inválida en login.") from e

    if not data.get("success"):
        raise RuntimeError(f"Login fallido: {data.get('code')}, {data.get('message')}")

    return data["token"]

main.py — endpoint de login

Python
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
from verifactu import login

app = FastAPI()


class LoginRequest(BaseModel):
    email: str
    password: str


@app.post("/auth/token")
async def obtener_token(body: LoginRequest):
    try:
        token = await login(body.email, body.password)
        return {"token": token}
    except RuntimeError as e:
        raise HTTPException(status_code=401, detail=str(e))
El token tiene una validez de 4 horas. En producción, guárdalo en Redis o en una variable de estado de la aplicación y renuévalo al caducar para evitar un login en cada petición.

Paso 2 — Generar registro de facturación

Este es el núcleo de la integración. El siguiente ejemplo cubre el caso más habitual: una factura ordinaria (F1) con IVA al 21%.

models.py — modelos Pydantic

Python
from pydantic import BaseModel
from typing import Optional


class Destinatario(BaseModel):
    NombreRazon: str
    NIF: str


class DesgloseFiscal(BaseModel):
    Impuesto: int
    ClaveRegimen: int
    CalificacionOperacion: int
    TipoImpositivo: float
    BaseImponibleOImporteNoSujeto: float
    CuotaRepercutida: float


class FacturaRequest(BaseModel):
    IDEmisorFactura: str
    NumSerieFactura: str
    FechaExpedicionFactura: str        # Formato YYYY-MM-DD
    TipoFactura: str
    DescripcionOperacion: str
    EmitidaPorTercODesti: Optional[str] = None
    Destinatarios: list[Destinatario]
    Desglose: list[DesgloseFiscal]
    CuotaTotal: float
    ImporteTotal: float

verifactu.py — función de registro

Python
async def registrar_factura(token: str, factura: dict) -> dict:
    async with httpx.AsyncClient(timeout=30) as client:
        try:
            response = await client.post(
                f"{VERIFACTU_BASE_URL}/alta-registro-facturacion",
                json=factura,
                headers={
                    "Accept": "application/json",
                    "Authorization": f"Bearer {token}",
                },
            )
        except httpx.RequestError as e:
            raise RuntimeError(f"Error de red al registrar factura: {e}") from e

    try:
        data = response.json()
    except Exception as e:
        raise RuntimeError("Respuesta JSON inválida al registrar factura.") from e

    if not data.get("success"):
        raise RuntimeError(f"Error al registrar factura: {data.get('message')}")

    return data["data"]["items"][0]

main.py — endpoint de facturación

Python
from models import FacturaRequest
from verifactu import registrar_factura


@app.post("/facturas")
async def emitir_factura(factura: FacturaRequest, token: str):
    try:
        resultado = await registrar_factura(token, factura.model_dump())
    except RuntimeError as e:
        raise HTTPException(status_code=400, detail=str(e))

    return {
        "id":          resultado["id"],
        "estado_aeat": resultado["estado_aeat"],
        "qr_image":    resultado["qr_image"],
        "url_qr":      resultado.get("url_qr"),
    }

Ejemplo de body para el endpoint /facturas

JSON
{
  "IDEmisorFactura":        "A39200019",
  "NumSerieFactura":        "FAC/2025/0001",
  "FechaExpedicionFactura": "2025-04-22",
  "TipoFactura":            "F1",
  "DescripcionOperacion":   "Servicios de desarrollo web - Abril 2025",
  "EmitidaPorTercODesti":   null,
  "Destinatarios": [
    { "NombreRazon": "Cliente Ejemplo S.A.", "NIF": "39707287H" }
  ],
  "Desglose": [
    {
      "Impuesto": 1, "ClaveRegimen": 1, "CalificacionOperacion": 1,
      "TipoImpositivo": 21, "BaseImponibleOImporteNoSujeto": 1000, "CuotaRepercutida": 210
    }
  ],
  "CuotaTotal": 210,
  "ImporteTotal": 1210
}

Paso 3 — Consultar el estado en la AEAT

El envío a la AEAT es asíncrono. La factura se registra inmediatamente con estado "No Registrado" y se envía en el siguiente ciclo de proceso. Puedes consultar el estado de dos formas:

Opción A — Polling manual

verifactu.py — función de consulta

Python
async def consultar_estado(token: str, registro_id: int) -> dict:
    async with httpx.AsyncClient(timeout=30) as client:
        try:
            response = await client.get(
                f"{VERIFACTU_BASE_URL}/alta-registro-facturacion/{registro_id}",
                headers={
                    "Accept": "application/json",
                    "Authorization": f"Bearer {token}",
                },
            )
        except httpx.RequestError as e:
            raise RuntimeError(f"Error de red al consultar estado: {e}") from e

    try:
        data = response.json()
    except Exception as e:
        raise RuntimeError("Respuesta JSON inválida al consultar estado.") from e

    if not data.get("success"):
        raise RuntimeError(f"Error al consultar el estado de la factura: {data.get('message')}")

    items = data["data"]["items"]
    return {
        "estado_aeat":            items["estado_aeat"],
        "codigo_error_aeat":      items["codigo_error_aeat"],
        "descripcion_error_aeat": items["descripcion_error_aeat"],
    }

main.py — endpoint de consulta

Python
from verifactu import consultar_estado


@app.get("/facturas/{registro_id}/estado")
async def estado_factura(registro_id: int, token: str):
    try:
        estado = await consultar_estado(token, registro_id)
    except RuntimeError as e:
        raise HTTPException(status_code=400, detail=str(e))

    # Valores posibles: "No Registrado" | "Pendiente" | "Incorrecto" | "Correcto"
    return estado

Opción B — Webhook (recomendado)

FastAPI es ideal para recibir webhooks. Registra la URL de tu endpoint en el panel de VeriFactuAPI y recibirás una notificación en cuanto la AEAT responda, sin polling.

Python
from pydantic import BaseModel


class WebhookPayload(BaseModel):
    estado: str
    num_serie: str
    factura_id: int
    codigo_error: str
    descripcion_error: str
    fecha_notificacion: str


@app.post("/webhooks/verifactu")
async def recibir_webhook(payload: WebhookPayload):
    # Aquí actualiza el estado en tu base de datos
    print(f"Factura {payload.num_serie} -> {payload.estado}")
    return {"ok": True}

El payload que recibirás tiene este formato:

JSON
{
  "estado": "Correcto",
  "num_serie": "FAC/2025/0001",
  "factura_id": 1,
  "codigo_error": "",
  "descripcion_error": "",
  "fecha_notificacion": "YYYY-MM-DD HH:MM:SS"
}

Formatos de respuesta JSON

Las respuestas de la API siguen una estructura JSON consistente. Entender estos formatos te permite validar respuestas de forma uniforme en tu cliente httpx.

Formato 1 — success (operación correcta)

JSON
{
  "success": true,
  "message": "Inicio de sesión exitoso",
  "code": 200,
  "token": "eyJ...",
  "expires_at": "2026-04-24 18:30:00"
}
  • success: true cuando la operación finaliza correctamente.
  • message: texto descriptivo del resultado.
  • code: código HTTP devuelto por la API.

Formato 2 — fail (error)

JSON
{
  "success": false,
  "message": "El campo IDEmisorFactura es obligatorio.",
  "error": "VALIDATION_ERROR",
  "code": 400
}
  • success: false cuando la operación falla.
  • message: descripción del error para mostrar o registrar en logs.
  • error: código interno o técnico del error.
  • code: código HTTP asociado al fallo.

Formato 3 — list (listados)

JSON
{
  "success": true,
  "message": "Invoice listed successfully",
  "code": 200,
  "data": { "items": [], "count": 0 },
  "pagination": { "total": 0, "perPage": 0, "currentPage": 0, "lastPage": 0 }
}
  • data.items: array con resultados.
  • data.count: número de elementos devueltos en la página actual.
  • pagination: bloque con metadatos de paginación.

Arrancar la aplicación

BASH
uvicorn main:app --reload

Una vez arrancada, FastAPI genera automáticamente la documentación interactiva en http://localhost:8000/docs, donde puedes probar todos los endpoints directamente desde el navegador.

Próximos pasos

  • Consulta la referencia completa de la API para ver todos los parámetros disponibles (facturas rectificativas, TicketBAI, webhooks, etc.)
  • Revisa las listas de valores (/api/listas) para conocer todos los códigos válidos de TipoFactura, Impuesto, ClaveRegimen y CalificacionOperacion

¿Listo para probarlo?

Crea tu cuenta gratuita y emite tu primera factura verificada en menos de 10 minutos. El entorno de pruebas está completamente habilitado desde el primer día: sin tarjeta, sin límites artificiales, con la misma API que usarás en producción.