DEV Community

Lucia Diaz Cuadrado
Lucia Diaz Cuadrado

Posted on

Cómo implementar VERI*FACTU en Python paso a paso (huella, XML, QR y envío a la AEAT)

Si mantienes un programa de facturación en España, ya sabes que llega VERI*FACTU, el sistema de la Agencia Tributaria que desarrolla el Real Decreto 1007/2023 (la «ley antifraude»). Cada factura tiene que generar un registro de facturación con una huella encadenada a la anterior. Ese registro se envía a la AEAT y la factura impresa lleva un código QR que el cliente puede verificar.

La AEAT publica la especificación en varios PDF y XSD. En PHP y .NET ya hay librerías asentadas, pero en Python no había ninguna completa, así que la he escrito y la he publicado como open source:
verifactu-python (pip install verifactu-sdk).

En este artículo veremos las cuatro piezas del sistema y cómo resolver cada una.

1. La huella (hash) encadenada

Es la parte en la que más gente se equivoca. Se concatenan ocho campos del registro con el formato campo=valor&campo=valor…, se calcula SHA-256 sobre la cadena en UTF-8 y el resultado se expresa en hexadecimal y mayúsculas:

IDEmisorFactura=89890001K&NumSerieFactura=12345678/G33&FechaExpedicionFactura=01-01-2024&TipoFactura=F1&CuotaTotal=12.35&ImporteTotal=123.45&Huella=
&FechaHoraHusoGenRegistro=2024-01-01T19:20:30+01:00
Enter fullscreen mode Exit fullscreen mode

(Aquí está partida en tres líneas para que se lea; en realidad es una sola cadena sin saltos de línea.)

Hay tres detalles que dan problemas:

  • Huella= va vacío en el primer registro. En los siguientes lleva la huella del registro anterior.
  • A los valores se les quitan los espacios del principio y del final.
  • FechaHoraHusoGenRegistro lleva el huso horario (+01:00 o +02:00).
from verifactu_sdk import huella_alta

huella_alta(
    id_emisor="89890001K",
    num_serie="12345678/G33",
    fecha_expedicion="01-01-2024",
    tipo_factura="F1",
    cuota_total="12.35",
    importe_total="123.45",
    huella_anterior=None,
    fecha_hora_huso_gen="2024-01-01T19:20:30+01:00",
)
# '3C464DAF61ACB827C65FDA19F352A4E3BDC2C640E9E9FC4CC058073F38F12F60'
Enter fullscreen mode Exit fullscreen mode

Ese valor coincide con el ejemplo oficial del documento de la AEAT. La librería tiene tests con los tres casos oficiales: primer registro, alta encadenada y anulación.

Para depurar: verifactu huella alta ... -v imprime la cadena intermedia.

2. Encadenar registros sin romper la cadena

Cada registro apunta al anterior. Si dos facturas se generan a la vez, o si guardas la huella fuera de la transacción, la cadena se rompe. La clase Cadena lleva la cuenta del último eslabón y tú solo tienes que guardarlo en base de datos:

from datetime import date
from verifactu_sdk import Cadena, LineaDesglose, Persona, RegistroAlta, SistemaInformatico

emisor = Persona("Mi Empresa SL", nif="B12345674")
sistema = SistemaInformatico(
    nombre_razon="Mi Empresa SL", nif="B12345674", nombre_sistema="MiFacturador",
    id_sistema="01", version="1.0.0", numero_instalacion="1",
)

factura = RegistroAlta(
    emisor=emisor,
    num_serie="2026-A-0001",
    fecha_expedicion=date.today(),
    descripcion="Servicios de consultoría",
    destinatarios=[Persona("Cliente SA", nif="A28015865")],
    desglose=[
        LineaDesglose(base_imponible="200.00", tipo_impositivo="21"),
        LineaDesglose(base_imponible="50.00", tipo_impositivo="10"),
    ],
    sistema=sistema,
)

cadena = Cadena(ultimo=None)   # en producción: Cadena(RegistroAnterior.from_dict(guardado))
cadena.encadenar(factura)

print(factura.cuota_total_str(), factura.importe_total_str())  # 47.00 297.00
print(factura.huella)
eslabon = cadena.ultimo.to_dict()  # guárdalo en la MISMA transacción que la factura
Enter fullscreen mode Exit fullscreen mode

CuotaTotal e ImporteTotal se calculan a partir del desglose con redondeo al céntimo, aunque puedes pasarlos a mano si tu ERP ya los tiene.

3. El XML, validado contra el XSD oficial

El servicio web espera un RegFactuSistemaFacturacion cuyos elementos van en un orden muy estricto. Si generas el XML a mano con plantillas, tarde o temprano te equivocarás en el orden o en un minOccurs. La librería construye el XML y lo valida sin conexión contra los XSD oficiales de la AEAT, que van incluidos en el paquete:

from verifactu_sdk import Cabecera, construir_peticion, validar

peticion = construir_peticion(Cabecera(emisor), [factura])
validar(peticion)  # lanza ErrorValidacionXSD con la línea y el motivo si algo no cuadra
Enter fullscreen mode Exit fullscreen mode

Si ya generas el XML por tu cuenta, puedes usar solo el validador:

verifactu validar registro.xml
Enter fullscreen mode Exit fullscreen mode

Hay ejemplos de rectificativas (R1 a R5), anulaciones, operaciones exentas (E1 a E8) y destinatarios extranjeros (IDOtro) en
examples/casos.py.

4. El código QR de la factura

La factura impresa debe llevar un QR con la URL de cotejo de la AEAT. Encima va el texto «QR tributario:» y, en sistemas VERI*FACTU, debajo va la frase «Factura verificable en la sede electrónica de la AEAT». La URL tiene que ir correctamente codificada: por ejemplo, un & en el número de serie se escribe %26.

from verifactu_sdk import url_qr, qr_png

url = url_qr(
    nif=emisor.nif,
    num_serie=factura.num_serie,
    fecha_expedicion=factura.fecha_expedicion,
    importe_total=factura.importe_total_str(),
)
open("qr.png", "wb").write(qr_png(url))  # requiere: pip install "verifactu-sdk[qr]"
Enter fullscreen mode Exit fullscreen mode

5. Envío a la AEAT

El envío se hace por SOAP con certificado electrónico cualificado (persona física, representante o sello). Además, la normativa obliga a un control de flujo: hay que esperar TiempoEsperaEnvio segundos entre envíos (60 al principio) salvo que se acumulen 1000 registros. El cliente se encarga de ello:

from verifactu_sdk import ClienteVerifactu, Entorno

with ClienteVerifactu("certificado.p12", password="****", entorno=Entorno.PRUEBAS) as aeat:
    respuesta = aeat.enviar(Cabecera(emisor), [factura])
    print(respuesta.estado, respuesta.csv)          # Correcto / ParcialmenteCorrecto / Incorrecto
Enter fullscreen mode Exit fullscreen mode

Prueba primero en Entorno.PRUEBAS, el portal de pruebas externas de la AEAT, y pasa a producción solo cuando todo funcione.

Bonus: VERI*FACTU desde tu asistente de IA (MCP)

La librería incluye un servidor MCP para que Claude, Cursor u otros agentes puedan calcular huellas, construir y validar el XML o generar la URL del QR mientras programas. No envía nada a la AEAT:

claude mcp add verifactu -- uvx --from "verifactu-sdk[mcp]" verifactu-sdk-mcp
Enter fullscreen mode Exit fullscreen mode

Conclusión

VERI*FACTU tiene más letra pequeña que dificultad técnica: el orden de los campos de la huella, el orden de los elementos del XML, la codificación de la URL del QR y el control de flujo. Si lo implementas en Python, espero que [verifactu-python]https://github.com/ldiazcuadrado/verifactu-python) te ahorre horas. Es software libre (MIT), así que si la AEAT te rechaza algún caso real, abre un issue con los datos anonimizados y lo arreglamos.

Si te resulta útil, una ⭐ en GitHub ayuda a que otros lo encuentren.

Top comments (0)