Skip to main content
VeriFactu · AEAT Compliance

VeriFactu in Odoo: the chained hash, why it gets computed wrong, and how records are resubmitted

Spain's VeriFactu rules require your software to generate, as each invoice is issued, a tamper-proof chained record — never afterwards. Here are the deadlines, how the hash is computed field by field, and the three calculation mistakes we made ourselves.

VeriFactu Spain module in Odoo: chained billing record and AEAT submission

If you invoice out of Odoo in Spain — or you plan to before 2027 — VeriFactu is a legal obligation with a fine attached, and it reaches your billing software whether or not you invoice from Odoo daily. Here is how the hash chaining each record to the previous one is computed, the three calculation mistakes we made ourselves, and what a well-built system does when the AEAT rejects a record.

Key point: VeriFactu mandates neither e-invoicing nor a new invoice format. It requires the system that issues each invoice to generate, as it is issued — or immediately before, never after —, a tamper-proof record chained to the previous one through a hash. An overnight job does not comply.

What exactly is VeriFactu?

VeriFactu is the popular name for the technical requirements of the Reglamento de Sistemas Informáticos de Facturación (RRSIF), approved by Royal Decree 1007/2023, which develops article 29.2.j) of Spain's General Tax Law. It is not the Crea y Crece B2B e-invoicing mandate: that is a separate obligation, with its own timeline, about invoice format. VeriFactu governs how your software behaves internally.

There are two modes. Under VERI*FACTU, your Odoo sends every record to the AEAT in near real time and the invoice carries a QR code and the «VERI*FACTU» label. Under non-VERI*FACTU the record is generated and signed just the same, but kept locally.

The chained hash: what it is and how it is computed

The hash is a 64-character hexadecimal SHA-256 condensing a handful of invoice fields plus the previous record's hash. That «plus the previous hash» is the mechanism: if someone edits invoice 4,000, then 4,001 stops adding up, and every record after.

Which fields go in, and in which order, is set by article 13 of Orden HAC/1177/2024. A billing record takes eight:

  1. Issuer tax ID (IDEmisorFactura), no country prefix.
  2. Invoice number and series (NumSerieFactura).
  3. Issue date, in DD-MM-YYYY (FechaExpedicionFactura).
  4. Invoice type: F1, F2, F3 or R1 to R5 (TipoFactura).
  5. Total tax (CuotaTotal).
  6. Invoice total (ImporteTotal).
  7. Previous record's hash (Huella).
  8. Generation timestamp with zone, ISO 8601 (FechaHoraHusoGenRegistro).

A cancellation record uses five, and there is the first trap: those fields are called IDEmisorFacturaAnulada, NumSerieFacturaAnulada and FechaExpedicionFacturaAnulada, and the field name is part of the computation.

The BOE says which fields, not how: section 13.2 defers to the technical document on the AEAT's site, version 0.1.2 of 27 August 2024. That is where almost everyone gets lost, because the values are not glued one after another: they are concatenated as fieldName=value pairs joined by &, trimming the leading and trailing spaces of each value and with trailing zeros in numeric fields carrying no weight. The string goes in UTF-8, SHA-256 is applied, and the output is uppercase hexadecimal, 64 characters. On the first record the previous hash is present but empty:

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

SHA-256  →  3C464DAF61ACB827C65FDA19F352A4E3BDC2C640E9E9FC4CC058073F38F12F60

Our hash function returns those exact values for the document's two billing records: 3C464DAF… and F7B94CFD…. That is the only check worth anything: your hash being 64 characters proves nothing; returning the published hash for the published inputs does.

The three calculation mistakes we have seen (all three ours)

All three give an impeccable-looking hash and a self-consistent chain; what does not match is the AEAT's computation:

How it was computedHash you getWhy it happens
Values glued together, no field names, no separators9689947A…Reading only the BOE, you see fields, not labels. Fixed 13-06-2026.
First-record seed: 64 zerosBCA62A69…The AEAT example disproves it: the label is left empty. Fixed 15-06-2026.
Issue date in ISO form (2024-01-01)048C3862…Issue date in DD-MM-YYYY, generation in ISO: two formats in one string. Fixed 09-07-2026.

The third was not a misreading but a value changing type along the way: when importing historical data, lines travel through a JSON field and a date object comes back as the string «YYYY-MM-DD». Every import was chained wrong and then failed its own integrity check, which re-reads the date from the real column.

The second had a dedicated test — «the first record carries an empty previous hash» — and it was green. It tested nothing: it never posted an invoice, so the genesis record did not exist inside it and the assertion sat behind an «if the genesis exists» guard that never held. On top of that it expected 64 zeros: had it run, it would have failed.

And this does not announce itself: the AEAT document states that when the reported hash does not match the one it computes, the record is flagged as «accepted with errors». It is not a rejection, so unless your system watches for that status no alarm goes off.

Rejections, corrections and resubmission

The AEAT can accept the record, accept it with errors, or reject it. In our module every record is born chained — it already has its number, hash and previous hash — and moves on to submitted, with the XML envelope stored; accepted, with its CSV; rejected, with the error code and full response; or retrying and dead letter.

The queue retries with growing back-off — 30 seconds, 2 minutes, 10 minutes, 30 minutes, 2 hours and 12 hours — and on the sixth attempt it moves to dead letter. An HTTP 429 is not that record's error: it stops the whole company's queue until the wait expires.

Resubmission is the counter-intuitive part: it recomputes nothing. The same envelope is rebuilt from the same record, with the same number and the same two hashes. The chain was sealed when the invoice was issued and does not depend on the AEAT answering; recomputing the hash on resubmission would break everything after it because of a network outage.

If the content is what is wrong, it is not edited either. The AEAT schema carries two flags: Subsanacion (S/N), marking that this record corrects an earlier one, and RechazoPrevio, which on a billing record takes N, S and X — X is the record that exists in your system but the AEAT never had, the case of moving from non-VERI*FACTU to VERI*FACTU —; on a cancellation it only takes S or N.

And if the invoice is void, it is not deleted: a cancellation record is appended with its own hash. Deleting a record raises an error saying precisely that, and the four chain fields are write-protected, from code too. A direct UPDATE leaves the chain broken and visible, and the event log — 24 types, including restoration from backup and attempted alteration — is append-only.

Deadlines and penalty: from when, and how much

These are the deadlines in force after Royal Decree-Law 15/2025:

Who / scenarioDate or amountLegal basis
Corporate Income Tax payersSystems adapted before 1 January 2027RD 1007/2023, art. 3.1.a) and final provision 4
Self-employed and the other art. 3.1 taxpayersSystems operational before 1 July 2027RD 1007/2023, final provision 4
Producers and resellers of billing systemsAlready in force: adapted product, and a producer's responsible declaration per versionRD 1007/2023, final provision 4 and art. 13
Using a system without the required certification50,000 EUR per tax yearArt. 201 bis.2 and .4 LGT
Producing or selling a non-compliant system150,000 EUR per tax year with sales and per system typeArt. 201 bis.1 and .4 LGT

Article 201 bis LGT sets no ceiling, just fixed fines, and the infraction is the system itself: the AEAT does not need to find a manipulated invoice. If the only thing missing is the required certificate — under the RRSIF, the producer's responsible declaration —, the seller's fine drops to 1,000 EUR per system sold.

And it counts even if you rarely invoice from Odoo: issuing one invoice, credit note or export invoice from Odoo is enough to make that Odoo a billing software system subject to the regulation too. Native invoicing does not generate the chained record.

How to get VeriFactu-compliant in Odoo

The first step, before spending anything, is knowing where your setup stands: our Verifactu Readiness Check is a free audit of your Odoo invoicing, with concrete findings.

For compliance itself there is our flexigo_spain_verifactu module (VeriFactu Spain — AEAT RRSIF Compliance), for Odoo 17, 18 and 19, with 283 tests on each branch: chained record, timestamp, QR, real-time submission, event log and integrity verification. It is among our published modules, and we leave it configured through our implementation service.

If you also issue electronic invoices, what decides the architecture is who the legal issuer is: article 9 of the Regulation requires the record to be generated by the system that issues the invoice, «simultaneously or immediately before». It is worked through in B2Brouter and Odoo; the rest of the obligations are in solutions.

Frequently asked questions

How exactly is the VeriFactu hash computed?

With SHA-256 over fieldName=value pairs joined by &, using the eight fields of article 13 of Orden HAC/1177/2024 in order, trimming leading and trailing spaces in each value, in UTF-8, with the output in uppercase hexadecimal, 64 characters. On the first record, the previous-hash field is present and empty. To check it, reproduce the examples the AEAT publishes.

What are the exact VeriFactu deadlines in 2027?

Under final provision four of RD 1007/2023, as amended by RD-ley 15/2025: 1 January 2027 for Corporate Income Tax payers and 1 July 2027 for the other taxpayers listed in article 3.1, the self-employed included. Producers and resellers are already required to offer an adapted product.

Useful links inside FlexigoTech

Verifactu Readiness CheckFree audit of your OdooB2Brouter and OdooWho the legal issuer is, per article 9SERES from Odoo (in Spanish)EDI, FACe and the RD 238/2026 formatsVeriFactu for accounting firms (in Spanish)Several entities, several certificatesCompliance solutionsThe map of obligations we cover

What we do about this

VeriFactu moduleWhat it does, screenshots, versions and price.

Want to know if your Odoo complies with VeriFactu?

We start with the free audit, and on the first call we tell you where you stand and what is missing — including whether the hash your system generates matches the one the AEAT computes. Email comercial@flexigobe.com or call +34 616 809 504.

Talk to an engineer