Cotejo · referencia técnica
La huella de un registro Verifactu
Cómo se calcula el hash SHA-256 que encadena los registros de facturación: el orden exacto de los campos, las reglas de formato que provocan casi todos los fallos, y los tres vectores de prueba oficiales de la AEAT.
En corto. Cada registro de facturación lleva una huella SHA-256 que incluye la huella del registro anterior, formando una cadena. Se calcula sobre una cadena de texto campo=valor&campo=valor con los campos en un orden fijo, codificada en UTF-8, y se escribe en hexadecimal, en mayúsculas, con 64 caracteres.
El campo Huella de esa cadena es el del registro anterior, y va vacío (Huella=) en el primero. Aun así, la huella del propio registro se calcula e informa siempre.
1. El algoritmo y la cadena canónica
El algoritmo es SHA-256, y a día de hoy es el único permitido. La entrada es una cadena de texto codificada en UTF-8. La salida va en hexadecimal, en mayúsculas, y siempre ocupa 64 caracteres.
Los campos que entran en el cálculo se concatenan así:
nombreCampo1=valorCampo1&nombreCampo2=valorCampo2&…&nombreCampoN=valorCampoN
Uno de esos campos es Huella y contiene la huella del registro anterior. Ahí está el encadenamiento: el registro N incorpora el hash del N−1, así que alterar un registro pasado invalida todos los posteriores. En el primero de un sistema de facturación no hay anterior, así que el campo va vacío, pero la huella del registro sí debe calcularse e informarse igualmente.
// La huella de un registro de alta. El campo Huella es la del registro ANTERIOR:
// va vacío en el primero de la cadena.
const campos = [
["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"],
];
const cadena = campos.map(([k, v]) => `${k}=${v}`).join("&");
const bytes = new TextEncoder().encode(cadena); // UTF-8
const digest = await crypto.subtle.digest("SHA-256", bytes);
const huella = [...new Uint8Array(digest)]
.map((b) => b.toString(16).padStart(2, "0"))
.join("")
.toUpperCase(); // 64 caracteres
// 3C464DAF61ACB827C65FDA19F352A4E3BDC2C640E9E9FC4CC058073F38F12F602. Los campos, y en qué orden van
El orden no es negociable: es el de aparición en los diseños de registro del anexo de la orden, y cualquier permuta da una huella distinta. Hay tres juegos de campos según el tipo de registro.
Registro de alta
| # | Campo | Ruta en el XML |
|---|---|---|
| 1 | IDEmisorFactura | RegistroAlta/IDFactura/IDEmisorFactura |
| 2 | NumSerieFactura | RegistroAlta/IDFactura/NumSerieFactura |
| 3 | FechaExpedicionFactura | RegistroAlta/IDFactura/FechaExpedicionFactura |
| 4 | TipoFactura | RegistroAlta/TipoFactura |
| 5 | CuotaTotal | RegistroAlta/CuotaTotal |
| 6 | ImporteTotal | RegistroAlta/ImporteTotal |
| 7 | Huella | RegistroAlta/Encadenamiento/RegistroAnterior/Huella |
| 8 | FechaHoraHusoGenRegistro | RegistroAlta/FechaHoraHusoGenRegistro |
Registro de anulación
| # | Campo | Ruta en el XML |
|---|---|---|
| 1 | IDEmisorFacturaAnulada | RegistroAnulacion/IDFactura/IDEmisorFacturaAnulada |
| 2 | NumSerieFacturaAnulada | RegistroAnulacion/IDFactura/NumSerieFacturaAnulada |
| 3 | FechaExpedicionFacturaAnulada | RegistroAnulacion/IDFactura/FechaExpedicionFacturaAnulada |
| 4 | Huella | RegistroAnulacion/Encadenamiento/RegistroAnterior/Huella |
| 5 | FechaHoraHusoGenRegistro | RegistroAnulacion/FechaHoraHusoGenRegistro |
Registro de evento
| # | Campo | Ruta en el XML |
|---|---|---|
| 1 | NIF | RegistroEvento/Evento/SistemaInformatico/NIF |
| 2 | ID | RegistroEvento/Evento/SistemaInformatico/IDOtro/ID |
| 3 | IdSistemaInformatico | RegistroEvento/Evento/SistemaInformatico/IdSistemaInformatico |
| 4 | Version | RegistroEvento/Evento/SistemaInformatico/Version |
| 5 | NumeroInstalacion | RegistroEvento/Evento/SistemaInformatico/NumeroInstalacion |
| 6 | NIF | RegistroEvento/Evento/ObligadoEmision/NIF |
| 7 | TipoEvento | RegistroEvento/Evento/TipoEvento |
| 8 | HuellaEvento | RegistroEvento/Evento/Encadenamiento/EventoAnterior/HuellaEvento |
| 9 | FechaHoraHusoGenEvento | RegistroEvento/Evento/FechaHoraHusoGenEvento |
El nombre «NIF» aparece dos veces en los eventos
En el registro de evento, NIF figura en las posiciones 1 y 6: una vez para el sistema informático y otra para el obligado a emitir. Si modelas los campos como un mapa nombre → valor, el segundo pisa al primero y la huella sale mal. Los campos son una lista ordenada, no un objeto.
3. Reglas de formato de los valores
El valor de cada campo es el que lleva el XML, pero con tres matices que provocan casi todos los fallos de huella:
- Se recortan los espacios de los extremos, no los de dentro. Si el XML trae
<NumSerieFactura> 12345678 / G33 </NumSerieFactura>, el valor es12345678 / G33, con sus espacios interiores intactos. - Un campo ausente o vacío escribe solo su nombre y el «=». Sin valor detrás:
…&Huella=&FechaHoraHusoGenRegistro=… - En los importes, una o dos posiciones decimales dan igual. La norma dice que los ceros a la derecha no son relevantes:
123.1y123.10se consideran ambos válidos. Producen hashes distintos, pero la AEAT acepta los dos. Un verificador que solo pruebe el literal del XML dará falsos negativos.
4. Vectores de prueba oficiales
Son los tres ejemplos del documento de la AEAT. Si tu implementación reproduce estas tres huellas, el cálculo es correcto. Cotejo los tiene congelados como tests.
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
3C464DAF61ACB827C65FDA19F352A4E3BDC2C640E9E9FC4CC058073F38F12F60
IDEmisorFactura=89890001K&NumSerieFactura=12345679/G34&FechaExpedicionFactura=01-01-2024&TipoFactura=F1&CuotaTotal=12.35&ImporteTotal=123.45&Huella=3C464DAF61ACB827C65FDA19F352A4E3BDC2C640E9E9FC4CC058073F38F12F60&FechaHoraHusoGenRegistro=2024-01-01T19:20:35+01:00
F7B94CFD8924EDFF273501B01EE5153E4CE8F259766F88CF6ACB8935802A2B97
IDEmisorFacturaAnulada=89890001K&NumSerieFacturaAnulada=12345679/G34&FechaExpedicionFacturaAnulada=01-01-2024&Huella=F7B94CFD8924EDFF273501B01EE5153E4CE8F259766F88CF6ACB8935802A2B97&FechaHoraHusoGenRegistro=2024-01-01T19:20:40+01:00
177547C0D57AC74748561D054A9CEC14B4C4EA23D1BEFD6F2E69E3A388F90C68
Fíjate en cómo la huella del caso 1 aparece dentro de la cadena del caso 2, y la del 2 dentro de la del 3. Eso es la cadena.
5. Fuentes oficiales
Todo lo anterior sale del documento de la AEAT, que es la fuente de verdad. Si algo de esta página lo contradice, mándanos un aviso: tiene razón él.
- Detalle de las especificaciones técnicas para generación de la huella o «hash» de los registros de facturación (v0.1.2, 27/08/2024)
- Sistemas Informáticos de Facturación (SIF) y VERI*FACTU, sede electrónica de la AEAT
El marco legal es el Real Decreto 1007/2023 y la Orden HAC/1177/2024.
La otra mitad de la norma es el código impreso en la factura: el código QR de Verifactu.