B2Brouter is an e-document delivery network, not your invoicing system. That sounds obvious and it isn't: the sentence decides what you can delegate and what has to stay in Odoo. Under VERI*FACTU, the system that issues the invoice carries obligations that are not discharged by sending an XML to a third party, and the regulation says so down to the hour.
What B2Brouter solves, and what it does not
It is an e-document exchange platform: Peppol access point, FACe in Spain, SDI in Italy, Chorus Pro in France, KSeF in Poland, ZATCA and MyInvois beyond Europe. For Spain it adds VERI*FACTU, SII and TicketBAI. Technically, a REST API with one auth header (X-B2B-API-Key), date-based versioning (X-B2B-API-Version, currently 2026-06-26) and everything scoped under /accounts/{id}/…; their public OpenAPI document declares 56 paths and 81 operations.
What it does take off your hands is the certificate: they are registered as a social collaborator in tax administration, so remitting to the AEAT needs only their API key. What it does not take off your hands is your invoice numbering, your credit notes, your filing calendar and — the important one — your status as issuer.
Who the legal issuer is
Royal Decree 1007/2023 defines a sistema informático de facturación as the hardware and software used to issue invoices. Not the one that transports them. And article 9, in full, reads:
“Invoicing computer systems used by the taxpayers referred to in Article 3 of these Regulations must automatically generate an issuance invoicing record simultaneously with, or immediately prior to, the issuance of each invoice.” (Unofficial translation of the Spanish original.)
Article 9 of the Regulation approved by Royal Decree 1007/2023 (BOE-A-2023-24840).
Simultaneously with, or immediately before. There is no “immediately after”, no “later that night”, no “when the cron syncs”. If Odoo posts the invoice at 11:04 and a nightly job pushes it at 23:00 so a third party can generate the record, the design is wrong, even if the XML does reach the AEAT.
Delegation is allowed: article 6 lets the obligations “be materially discharged by the recipient of the transaction or by a third party”, but adds that this “does not release the taxable persons… from responsibility for compliance”. You can delegate the work; not the liability.
The practical consequence is a single one: the invoice must be born and numbered in one place only, and the record must be generated in that same act. If Odoo is where the invoice is posted, Odoo is the issuer. The rest is transport.
What has to stay in Odoo
- Numbering and series. A number assigned in two systems is an incident waiting for a date.
- Immutability. Article 8.2.a) requires any correction to be made “by means of at least one additional, subsequent billing record”. In Odoo, “reset to draft” on a registered invoice stops being an option.
- Traceability. Articles 1 and 8.1 demand integrity, retention, traceability and immutability. That is an event log, not a log that rotates every seven days.
- The declaración responsable. Article 13 requires it to appear “in writing and visibly within the computer system itself, in each of its versions”, and assigns it to the system's producer. If what issues is Odoo plus your modules, it is about that whole.
The price of getting it wrong is not theoretical: article 201 bis of the Spanish General Tax Act sets a €50,000 fine per tax year for merely holding non-certified invoicing systems where certification is required. The decree's fourth final provision sets the dates: corporate income tax payers before 1 January 2027; everyone else before 1 July 2027.

Facturae, FACe and private B2B: three different roads
Facturae and FACe are the public sector. Law 25/2013 requires “a structured format” signed with “an advanced electronic signature based on a recognised certificate” (art. 5) and creates the general entry point (art. 6). In practice the problem is never the format, it is the three DIR3 codes — accounting office, managing body and processing unit: miss one and the entry point rejects it, not the customer. In the B2Brouter API these are cin1, cin2 and cin3 under scheme 8014, transport es.face, format xml.facturae.3.2; if the imported XML already complies they send it as is, and if it arrives unsigned they sign it.
B2B e-invoicing is a different thing and it has not started yet. Royal Decree 238/2026 (BOE-A-2026-7295) sets the EN 16931 model and four syntaxes: CII, UBL, EDIFACT and Facturae. The clock does not start with the decree but with the ministerial order for the public e-invoicing solution — one year for companies invoicing over €8 million, two for everyone else — and the clock does not start until that order comes into force.
VERI*FACTU is neither a format nor something you send the customer: it is a parallel record of each invoice, generated at issuance. And the two regimes are not alternatives: article 2 bis(6) of Law 56/2007 requires e-invoicing software to also comply with article 29.2.j) of the General Tax Act, which is where VERI*FACTU hangs from. They stack. If you are starting from scratch, begin with what VERI*FACTU is and how to comply with Odoo.
What leaves Odoo and what comes back
Outbound goes the invoice as JSON, with numbering, dates, taxes, amounts and the recipient's routing codes. The detail that eats the most time is matching cents: if you let the platform recompute the taxable bases from quantity × price, a discounted line will end up a cent off from Odoo.
Inbound comes the status, and this is where the most common mistake lives. The API distinguishes 22 read-side states and only 9 you may set yourself:
| State | What it actually means | Who sets it |
|---|---|---|
sent | The document left through the transport | The network |
registered | A tax authority has responded | The tax authority |
accepted | Someone on the other side approved it | The customer |
refused / error | Rejection, or send failure | Either side |
Collapsing them into a single “sent” boolean is convenient on day one and expensive the day someone asks, eight months later, whether that invoice ever reached the administration. And a nuance you only learn in production: a successful response does not mean transmitted. The send call returns 204 and delivery is still asynchronous; and there are 200 responses that carry errors inside, in an errors[] nobody reads. The callback comes as a webhook signed with HMAC-SHA256, with no documented retry policy: a lost webhook is a status that never updates, so you need deduplication and a polling fallback cron.
The chained hash and why it is computed wrong
The alta record is not the invoice: it is eight fields chained to the previous one through a SHA-256 hash. Article 13.1 of Order HAC/1177/2024 lists their order — issuer's tax ID, number and series, issue date, invoice type, total tax, total amount, previous hash and date-time-zone — but paragraph 2 defers “the algorithm and encoding” to a technical document on the AEAT's site. The BOE gives you an ordered list; it does not give you the format.
If you stop at the BOE, the natural move is to concatenate the eight values and run SHA-256. That is exactly what we did in our own VERI*FACTU module, and it is wrong. The AEAT specification requires labelled key=value pairs joined by &:
IDEmisorFactura=B12345678&NumSerieFactura=FA/2026/0001&FechaExpedicionFactura=15-06-2026
&TipoFactura=F1&CuotaTotal=21.00&ImporteTotal=121.00&Huella=
&FechaHoraHusoGenRegistro=2026-06-15T11:04:33+02:00
SHA-256, hex, uppercase over that string. And watch the penultimate field: in the first record, Huella is empty. We had seeded it with 64 zeros, which is what you write by instinct. That was the second defect, found two days after the first.
What gave it away: nothing
The suite was green. A hash defect does not look like a normal defect: every hash is still a valid 64-character hex string, the chain links to itself without a gap, and the module's integrity check passes — because it recomputes with the same wrong formula. The test that should have caught it was this one:
def test_genesis_huella_anterior(self):
genesis = self.cadena.genesis_id
if genesis:
self.assertEqual(genesis.huella_anterior, '0' * 64,
'Genesis record must have all-zeros huella_anterior')
Two defects in four lines: the if genesis: turns it into an automatic pass when there is no genesis record, and the constant it asserts is precisely the wrong one. A vacuous test that also encoded the bug. It now reads:
def test_genesis_huella_anterior(self):
self._make_invoice()
genesis = self.cadena.genesis_id
self.assertTrue(genesis, 'Expected the first posted invoice to become the genesis record')
self.assertEqual(genesis.huella_anterior, '',
'Genesis record must have empty huella_anterior per AEAT spec')
The rule that stuck: a test that fails when you delete the code it protects is a test; everything else is decoration.
Check your hash against the AEAT's published worked example before the first invoice goes out: article 8.2.a) forbids rewriting records already generated, so a miscomputed chain is not fixed, it is carried.

When B2Brouter is overkill and when it saves you months
- It saves you months if you send to more than one country or network, you have public-sector customers with DIR3 codes, you would rather not hold a qualified certificate, or you want a layer that already speaks the four Crea y Crece syntaxes.
- It is overkill if you invoice only in Spain, with no public bodies as customers, and the only thing binding you is VERI*FACTU: there, a module that chains and remits from Odoo does the job without adding a network hop, a per-transaction cost or a second place to look when something breaks.
The incompatibility nobody tells you about
If you pick B2Brouter for VERI*FACTU, you cannot keep your own chain in Odoo at the same time. The setting is per account, not per invoice: once active it chains every invoice on that account. And if you import an XML that already carries a hash, they discard it: “this information will be ignored and our system will perform the chaining process regardless”. It is the right call — a chain with two authors is not a chain — but it means the two solutions are mutually exclusive per company: if they coexist, the AEAT receives two series of records for the same invoices.
In the connector we built against their API this is a hard exclusion: one or the other, never both. The module carries a single suite of 778 tests, the same one on all three branches, and each branch is closed by running it against a real Odoo of its own version: 19, 18 and 17. The send path is exercised against their test environment all the way to registered, refused and error paths included. That is how it turned out the module did not even install on 17: an inherited xpath pointed at the card kanban template, the 18-and-later name — 17 calls it kanban-box — and an xpath that cannot find its target fails the whole install. The 17 lane started zero tests. A module that compiles is not a module that installs.
Frequently asked questions
Does B2Brouter replace a VERI*FACTU module in Odoo?
It replaces it as the chaining and submission engine, not as the issuing system: article 9 of RD 1007/2023 requires the record to be generated simultaneously with, or immediately before, issuance, and what issues is still Odoo. On top of that, the two are mutually exclusive per company.
Can I push invoices to B2Brouter with a nightly cron?
For the document (Peppol, FACe, email), yes, within each channel's deadline. For the VERI*FACTU record, no: article 9 only allows “simultaneously with or immediately before” issuance.
Are Facturae and B2B e-invoicing the same thing?
No. Facturae signed with an advanced electronic signature is what Law 25/2013 requires for invoicing public administrations. The B2B e-invoice of RD 238/2026 accepts four syntaxes over the EN 16931 model, and its clock has not started yet.
Which status should I store in Odoo?
All three separately: transport (sent), tax authority (registered) and customer (accepted or refused). The API distinguishes 22 read-side states and only 9 that can be set manually.
Integrating B2Brouter with Odoo?
We look at your numbering, your series, the moment the record is generated and the statuses that come back, and tell you what stays in Odoo and what can move to the network. Book a call on Calendly or call us at +34 616 809 504.
