Resumen
La huella es el SHA-256 de una cadena Campo1=valor1&Campo2=valor2&… codificada en UTF-8, con los campos en un orden fijo y la huella del registro anterior dentro. El resultado va en hexadecimal y en mayúsculas (64 caracteres). La especificación vigente es la versión 0.1.2, de 27 de agosto de 2024.
Campos y orden
| # | Registro de alta | Registro de anulación |
|---|---|---|
| 1 | IDEmisorFactura | IDEmisorFacturaAnulada |
| 2 | NumSerieFactura | NumSerieFacturaAnulada |
| 3 | FechaExpedicionFactura | FechaExpedicionFacturaAnulada |
| 4 | TipoFactura | Huella (del registro anterior) |
| 5 | CuotaTotal | FechaHoraHusoGenRegistro |
| 6 | ImporteTotal | |
| 7 | Huella (del registro anterior) | |
| 8 | FechaHoraHusoGenRegistro |
Los detalles que hacen fallar el cálculo
- Nombres de campo literales. Se escriben tal como en el XML, respetando mayúsculas:
IDEmisorFactura, noIdEmisorFactura. - Primer registro. La huella anterior no existe: el campo queda vacío,
…&Huella=&FechaHoraHusoGenRegistro=…. En el XML ese registro llevaPrimerRegistro= «S», pero su propia huella sí se calcula. - Espacios. Se eliminan al inicio y al final de cada valor antes de concatenar. La AEAT también los elimina al recibir el XML.
- Fechas.
FechaExpedicionFacturava como DD-MM-AAAA (01-01-2024).FechaHoraHusoGenRegistrova en ISO 8601 con huso (2024-01-01T19:20:30+01:00) y debe ser idéntica a la del XML. - Importes. La especificación trata igual los valores con uno o dos decimales («123.1» y «123.10»). Lo seguro es usar exactamente el valor que escribes en el XML. El XSD no admite más de dos decimales.
- Codificación. UTF-8 (importa si el número de serie lleva caracteres no ASCII).
- Salida. Hexadecimal en mayúsculas. Muchas librerías devuelven minúsculas por defecto.
Los tres ejemplos oficiales
La especificación incluye tres ejemplos encadenados. Son los vectores de prueba de cualquier implementación:
| Registro | Datos que cambian | Huella resultante |
|---|---|---|
| 1. Alta (primer registro) | NumSerieFactura 12345678/G33, Huella vacía, 19:20:30 | 3C464DAF61ACB827C65FDA19F352A4E3BDC2C640E9E9FC4CC058073F38F12F60 |
| 2. Alta | NumSerieFactura 12345679/G34, Huella = la del 1, 19:20:35 | F7B94CFD8924EDFF273501B01EE5153E4CE8F259766F88CF6ACB8935802A2B97 |
| 3. Anulación del 2 | Huella = la del 2, 19:20:40 | 177547C0D57AC74748561D054A9CEC14B4C4EA23D1BEFD6F2E69E3A388F90C68 |
Datos comunes: IDEmisorFactura 89890001K, FechaExpedicionFactura 01-01-2024, TipoFactura F1, CuotaTotal 12.35, ImporteTotal 123.45 y huso +01:00. La cadena completa del primero es:
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:00Código en Python
Solo usa la biblioteca estándar. Al ejecutarlo imprime las tres huellas oficiales:
import hashlib
def huella_alta(emisor, num_serie, fecha, tipo, cuota, importe, huella_anterior, fecha_hora):
campos = [
("IDEmisorFactura", emisor),
("NumSerieFactura", num_serie),
("FechaExpedicionFactura", fecha),
("TipoFactura", tipo),
("CuotaTotal", cuota),
("ImporteTotal", importe),
("Huella", huella_anterior), # "" en el primer registro
("FechaHoraHusoGenRegistro", fecha_hora),
]
cadena = "&".join(f"{k}={v.strip()}" for k, v in campos)
return hashlib.sha256(cadena.encode("utf-8")).hexdigest().upper()
def huella_anulacion(emisor, num_serie, fecha, huella_anterior, fecha_hora):
campos = [
("IDEmisorFacturaAnulada", emisor),
("NumSerieFacturaAnulada", num_serie),
("FechaExpedicionFacturaAnulada", fecha),
("Huella", huella_anterior),
("FechaHoraHusoGenRegistro", fecha_hora),
]
cadena = "&".join(f"{k}={v.strip()}" for k, v in campos)
return hashlib.sha256(cadena.encode("utf-8")).hexdigest().upper()
if __name__ == "__main__":
# Ejemplos oficiales de la especificación de la AEAT (v0.1.2)
h1 = huella_alta("89890001K", "12345678/G33", "01-01-2024", "F1", "12.35", "123.45", "", "2024-01-01T19:20:30+01:00")
h2 = huella_alta("89890001K", "12345679/G34", "01-01-2024", "F1", "12.35", "123.45", h1, "2024-01-01T19:20:35+01:00")
h3 = huella_anulacion("89890001K", "12345679/G34", "01-01-2024", h2, "2024-01-01T19:20:40+01:00")
print(h1) # 3C464DAF61ACB827C65FDA19F352A4E3BDC2C640E9E9FC4CC058073F38F12F60
print(h2) # F7B94CFD8924EDFF273501B01EE5153E4CE8F259766F88CF6ACB8935802A2B97
print(h3) # 177547C0D57AC74748561D054A9CEC14B4C4EA23D1BEFD6F2E69E3A388F90C68
Código en JavaScript (Node.js y navegador)
Usa Web Crypto, disponible en Node.js 18+ y en los navegadores modernos, sin dependencias:
// Node.js 18+ y navegadores modernos (Web Crypto)
async function sha256Hex(texto) {
const bytes = new TextEncoder().encode(texto); // UTF-8
const digest = await crypto.subtle.digest('SHA-256', bytes);
return [...new Uint8Array(digest)].map((b) => b.toString(16).padStart(2, '0')).join('').toUpperCase();
}
const cadena = (campos) => campos.map(([k, v]) => `${k}=${v.trim()}`).join('&');
export function huellaAlta(r, huellaAnterior = '') {
return sha256Hex(cadena([
['IDEmisorFactura', r.emisor],
['NumSerieFactura', r.numSerie],
['FechaExpedicionFactura', r.fecha],
['TipoFactura', r.tipo],
['CuotaTotal', r.cuota],
['ImporteTotal', r.importe],
['Huella', huellaAnterior],
['FechaHoraHusoGenRegistro', r.fechaHora],
]));
}
export function huellaAnulacion(r, huellaAnterior) {
return sha256Hex(cadena([
['IDEmisorFacturaAnulada', r.emisor],
['NumSerieFacturaAnulada', r.numSerie],
['FechaExpedicionFacturaAnulada', r.fecha],
['Huella', huellaAnterior],
['FechaHoraHusoGenRegistro', r.fechaHora],
]));
}
// Ejemplos oficiales de la especificación de la AEAT (v0.1.2)
const h1 = await huellaAlta({ emisor: '89890001K', numSerie: '12345678/G33', fecha: '01-01-2024', tipo: 'F1', cuota: '12.35', importe: '123.45', fechaHora: '2024-01-01T19:20:30+01:00' });
const h2 = await huellaAlta({ emisor: '89890001K', numSerie: '12345679/G34', fecha: '01-01-2024', tipo: 'F1', cuota: '12.35', importe: '123.45', fechaHora: '2024-01-01T19:20:35+01:00' }, h1);
const h3 = await huellaAnulacion({ emisor: '89890001K', numSerie: '12345679/G34', fecha: '01-01-2024', fechaHora: '2024-01-01T19:20:40+01:00' }, h2);
console.log(h1, h2, h3);
Estos dos fragmentos se ejecutan en los tests de esta web en cada cambio y deben producir exactamente las huellas oficiales. No publicamos versiones en Java o PHP porque no las hemos podido ejecutar en nuestro entorno de pruebas.
Verificar una cadena completa
Para cada registro, recalcula su huella con sus datos y la huella anterior que declara, y compárala con la que lleva. Después comprueba que la huella anterior que declara (en Encadenamiento/RegistroAnterior) es la del registro previo, y que el emisor, el número y la fecha que declara corresponden a esa factura. Si lo primero falla, se han alterado datos; si falla lo segundo, falta, sobra o se ha reordenado algún registro. Es exactamente lo que hace el comprobador de huellas.