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:
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
mi_proyecto/
├── main.py # Aplicación FastAPI con los endpoints
├── verifactu.py # Cliente para la API de VerifactuAPI
└── models.py # Modelos PydanticPaso 1 — Autenticación y obtención del token
verifactu.py — cliente base
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
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))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
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: floatverifactu.py — función de registro
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
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
{
"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
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
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 estadoOpció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.
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:
{
"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)
{
"success": true,
"message": "Inicio de sesión exitoso",
"code": 200,
"token": "eyJ...",
"expires_at": "2026-04-24 18:30:00"
}success:truecuando la operación finaliza correctamente.message: texto descriptivo del resultado.code: código HTTP devuelto por la API.
Formato 2 — fail (error)
{
"success": false,
"message": "El campo IDEmisorFactura es obligatorio.",
"error": "VALIDATION_ERROR",
"code": 400
}success:falsecuando 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)
{
"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
uvicorn main:app --reloadUna 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 deTipoFactura,Impuesto,ClaveRegimenyCalificacionOperacion
¿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.