How VeriFactu's SHA-256 Hash Chain Works
VeriFactu requires each invoice registration to include a cryptographic fingerprint of the previous one, forming a tamper-evident chain that AEAT can verify at any time. Here's exactly how that chain is constructed, field by field.
Why the chain exists
VeriFactu (Verificación Inmediata de Facturas, Royal Decree 1007/2023) is Spain's real-time invoice registration system, planned to become mandatory in October 2028 (pending regulatory approval). Its goal is to make it structurally impossible to modify or delete an invoice after submission without AEAT detecting the tampering.
The mechanism is a SHA-256 hash chain: every RegistroAlta (invoice
registration record) you send to AEAT must include the SHA-256 fingerprint of the
previous record you sent for the same issuer. If you alter record N, the hash stored
in record N+1 no longer matches, and AEAT's verification fails immediately.
The chaining block: EncadenandoRegistro
Each RegistroAlta sent to AEAT contains an
EncadenandoRegistro block that references the immediately previous record:
{
"EncadenandoRegistro": {
"NumSerieFacturaAnterior": "FACT-2026-099",
"FechaExpedicionFacturaAnterior": "09-05-2026",
"HuellaFacturaAnterior": "3A9F2B1..."
}
}
The three fields work together: NumSerieFacturaAnterior and
FechaExpedicionFacturaAnterior identify which previous record is
being referenced, and HuellaFacturaAnterior is the SHA-256 fingerprint
of that record's canonical string. AEAT stores both the record content and its
fingerprint, so it can re-compute the hash at verification time and detect any mismatch.
Computing the SHA-256 fingerprint
The fingerprint (huella) of a record is the SHA-256 hash, in uppercase hexadecimal, of a canonical string constructed from the record's key fields. The canonical string format is:
IDEmisorFactura=<NIF>&NumSerieFactura=<series>&FechaExpedicionFactura=<date>&TipoFactura=<type>&CuotaTotal=<vat>&ImporteTotal=<total>&Huella=<prev-hash>&FechaHoraHusoHorarioFirma=<timestamp>
Fields are joined with & (ampersand, no spaces), in exactly this
order. Values must be trimmed (no leading or trailing whitespace). The string is
encoded as UTF-8 before hashing.
| Field | Example | Notes |
|---|---|---|
| IDEmisorFactura | B12345678 | NIF of the invoice issuer |
| NumSerieFactura | FACT-2026-100 | Invoice series + number |
| FechaExpedicionFactura | 09-05-2026 | Format: DD-MM-YYYY |
| TipoFactura | F1 | F1 standard, F2 simplified, R1–R5 rectificativas |
| CuotaTotal | 21.00 | Total VAT amount, 2 decimal places, period separator |
| ImporteTotal | 121.00 | Invoice total inc. VAT, 2 decimal places |
| Huella | 3A9F2B1… | SHA-256 fingerprint of the previous record |
| FechaHoraHusoHorarioFirma | 2026-05-09T14:23:45+02:00 | Signing timestamp, ISO 8601 with timezone offset |
Step-by-step: computing the hash in Python
import hashlib
def compute_huella(
id_emisor: str,
num_serie: str,
fecha_expedicion: str, # "DD-MM-YYYY"
tipo_factura: str, # "F1", "F2", "R1"...
cuota_total: str, # "21.00"
importe_total: str, # "121.00"
huella_anterior: str, # SHA-256 of previous record (hex uppercase)
fecha_hora_firma: str, # "2026-05-09T14:23:45+02:00"
) -> str:
canonical = (
f"IDEmisorFactura={id_emisor}&"
f"NumSerieFactura={num_serie}&"
f"FechaExpedicionFactura={fecha_expedicion}&"
f"TipoFactura={tipo_factura}&"
f"CuotaTotal={cuota_total}&"
f"ImporteTotal={importe_total}&"
f"Huella={huella_anterior}&"
f"FechaHoraHusoHorarioFirma={fecha_hora_firma}"
)
digest = hashlib.sha256(canonical.encode("utf-8")).hexdigest()
return digest.upper()
# Invoice #100 — previous record's fingerprint was "3A9F2B1..."
huella_100 = compute_huella(
id_emisor = "B12345678",
num_serie = "FACT-2026-100",
fecha_expedicion = "09-05-2026",
tipo_factura = "F1",
cuota_total = "21.00",
importe_total = "121.00",
huella_anterior = "3A9F2B1...", # hash of record #99
fecha_hora_firma = "2026-05-09T14:23:45+02:00",
)
# huella_100 is what you put in record #101's HuellaFacturaAnterior
The first record in a chain
Every issuer starts with a first invoice. There is no previous record, so what goes
into HuellaFacturaAnterior?
For the very first RegistroAlta of an issuer, the chaining block is
omitted entirely: you don't send EncadenandoRegistro at all. AEAT
recognises the absence of this block as the chain initializer and stores the
first record's own fingerprint as the starting point of the chain.
Common mistakes
1. Wrong decimal separator
CuotaTotal and ImporteTotal use a period (.)
as the decimal separator, never a comma. Spain uses commas colloquially, but the
canonical string follows the XML spec. 21,00 will produce a wrong hash.
2. Trailing zeros dropped
Always use exactly two decimal places: 21.00, not 21 or
21.0. The hash is computed over the literal string, so
"21.00" and "21" produce different results.
3. Timezone offset in FechaHoraHusoHorarioFirma
The signing timestamp must include the local timezone offset, not UTC. In mainland
Spain that's +01:00 in winter and +02:00 in summer (CEST).
Using Z (UTC) when the clock says CET/CEST will produce a hash mismatch
at verification time if AEAT re-derives the timestamp from the XML signature.
4. Re-hashing from stored invoice data
Once you've submitted record N and received AEAT's acknowledgement, store
the computed Huella directly. Don't reconstruct it later from your
database invoice fields; if any field was stored with a different format (extra
spaces, different precision), re-computation will produce a different hash and your
chain will be broken for record N+1.
What AEAT validates at verification time
When AEAT verifies your records (on demand or during an audit), they reconstruct the
canonical string for each record from the stored field values, re-compute the SHA-256
hash, and check it against the HuellaFacturaAnterior value in the next
record in sequence. If any record in the chain has been modified, even by a single
character, every subsequent hash in the chain becomes invalid, and the sequence of
failures pinpoints exactly where the chain was broken.
This is what makes VeriFactu more tamper-evident than a simple audit log: the chain is cryptographically self-verifying. There is no separate "audit hash" to maintain; the chain is the audit.
Cancellations and corrections
Cancellation records (RegistroAnulacion) and rectificative invoices
(R1–R5) are also part of the chain. A cancellation record
references the original invoice it cancels, but it still participates in the hash
chain as a regular record. The next submission (whether another invoice or another
cancellation) must include the fingerprint of the cancellation record.