Skip to content

Hash chain

Every record submitted to the AEAT carries a huella (literally, fingerprint) — a 64-character uppercase hexadecimal SHA-256 digest that chains it to the previous record from the same taxpayer. The chain is what makes the registry tamper-evident: changing any past record invalidates every subsequent hash.

The full algorithm is specified in Especificaciones técnicas para generación de la huella o hash de los registros de facturación v0.1.2. The SDK implements it verbatim; the three reference hashes in §6 of that PDF are covered by unit tests and match byte-for-byte.

Fields hashed

The hash is computed over a concatenation name1=value1&name2=value2&… with a fixed, ordered list of fields per record type:

RegistroAlta (eight fields)

OrderField nameSource
1IDEmisorFacturainvoiceId.issuerNif
2NumSerieFacturainvoiceId.seriesNumber
3FechaExpedicionFacturainvoiceId.issueDate (DD-MM-YYYY)
4TipoFacturainvoiceType
5CuotaTotaltotalTaxAmount
6ImporteTotaltotalAmount
7Huella (previous record)chainLink.previousHash or empty
8FechaHoraHusoGenRegistrogeneratedAt (ISO 8601 with offset)

RegistroAnulacion (five fields)

OrderField nameSource
1IDEmisorFacturaAnuladacancelledInvoiceId.issuerNif
2NumSerieFacturaAnuladacancelledInvoiceId.seriesNumber
3FechaExpedicionFacturaAnuladacancelledInvoiceId.issueDate
4Huella (previous record)chainLink.previousHash or empty
5FechaHoraHusoGenRegistrogeneratedAt

Normalisation

Before concatenation each value is normalised:

  • Trim leading and trailing whitespace.
  • Numeric values (CuotaTotal, ImporteTotal): trailing zeros after the decimal separator are irrelevant — 21.00 and 21 produce the same hash.
  • Dates are emitted in the wire form DD-MM-YYYY.
  • The bytes used for hashing are UTF-8 of the resulting string.

First record

For the very first record submitted by a taxpayer the Huella field of the previous record is empty — the concatenation contains …&Huella=&…. Set chainLink.first = true and omit the four previous* fields:

ts
const first: Invoice = {
  /* ... */
  chainLink: { first: true },
};

Subsequent records

For every subsequent record provide the full previous link:

ts
const next: Invoice = {
  /* ... */
  chainLink: {
    first: false,
    previousIssuerNif: 'B12345678',
    previousSeriesNumber: 'A/2026/0001',
    previousIssueDate: '2026-05-20',
    previousHash: '3C13742B…A8F1',
  },
};

In practice the SDK fills this in for you when you go through VerifactuClient — it remembers the last hash of the chain per-instance. You only deal with the link manually when you store records offline and resume the chain later.

Computing a hash manually

ts
import { computeRegistroAltaHash } from 'verifactu-sdk/hash';

const hash = computeRegistroAltaHash(record, null /* first record */);
// → "3C13742B...A8F1"  (64 uppercase hex chars)

The function is pure — no I/O, no side effects — and is exposed via verifactu-sdk/hash. It throws SchemaValidationError if you pass a malformed previousHash.

Verifying

Given a record and its predecessor, you can recompute the hash and compare:

ts
import { computeRegistroAltaHash } from 'verifactu-sdk/hash';

const expected = computeRegistroAltaHash(current, previous.hash);
if (expected !== current.hash) {
  throw new Error('Chain broken!');
}

The AEAT does this server-side on every submission. A mismatch raises error 2000 (admissible — the record is accepted but flagged for subsanación).

Reset / replay

If you lose the local chain state (database wipe, certificate rotation) you must re-query the AEAT for the last accepted record, take its Huella field and resume from there:

ts
const lastPage = await firstPage(client.queryInvoices({ year: '2026', period: '05' }));
const last = lastPage.records.at(-1);
// → resume the chain using last.invoiceId + the hash stored in your DB

Performance

The hash is a single SHA-256 over a short string (typically under 1 KB), so a single record hashes in microseconds. The bottleneck of the chain is the sequential AEAT call: the SDK serialises registerInvoice calls per-instance so the chain is never broken by concurrency.

Next

  • QR code — also relies on the invoice identifier.
  • Validations — rule 23 checks the hash shape locally.
  • Flow control — why submissions are serialised.

Released under the MIT license.