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
(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.
-
FechaHoraHusoGenRegistrolleva el huso horario (+01:00o+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'
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 ... -vimprime 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
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
Si ya generas el XML por tu cuenta, puedes usar solo el validador:
verifactu validar registro.xml
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]"
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
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
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)