The PDF stopped being the invoice: eight months of Belgian B2B e-invoicing
Belgium's structured e-invoicing mandate landed on 1 January. Switching it on was an afternoon; everything it exposed was the quarter that followed. What Peppol actually addresses, why the failures live in your contact records rather than your ERP, what UBL refuses to carry, and why 'sent' stopped meaning 'delivered'.
Belgium's B2B e-invoicing mandate came into force on 1 January 2026. Every accountancy newsletter in the country wrote the countdown post, and by December most of our clients had done what those posts asked: switched something on, sent a test invoice to their own accountant, watched it arrive, and ticked the box.
Then January happened, and we spent a quarter on the part nobody wrote a countdown post about.
The switching-on is genuinely easy. For a client already on a maintained Odoo it's an afternoon, and we've done enough of them now to quote it as one. What isn't easy is everything the switch-on exposes: nine years of contact records typed by fourteen different people, an invoice layout carrying information that has nowhere to go in a structured document, and a delivery channel that can fail four hours after the "sent" toast disappeared. None of that is on the compliance checklist, and all of it is what the first quarter was actually about.
What changed, precisely
The mandate gets summarised as "no more PDF invoices," which is close enough to be useful and wrong in a way that costs people money.
What changed is which artefact is the invoice. Since 1 January, for a domestic supply between two Belgian VAT-registered businesses, the legal document is a structured file — Peppol BIS Billing 3.0 by default, a UBL profile conforming to the European standard EN 16931 — delivered over the Peppol network. Anything human-readable is a rendering of that file. You can still produce a PDF, still attach it, still email it to the person who asked. It's simply no longer the thing being sent, in the same way a printed bank statement isn't your balance.
That distinction sounds academic until the first dispute. When a customer says the price was wrong, the answer is now in an XML document that two access points have timestamped, not in whichever version of the PDF got forwarded around. On balance that's an improvement. It's also a change in what your finance team is looking at when they investigate something, and nobody warned them.
Three scoping facts worth stating plainly, because they're the ones people get wrong in meetings:
- B2C is untouched. Consumers get whatever you gave them before.
- B2G was already there. Public-sector invoicing has run over Peppol for years; if you invoice government you did this a while ago and this mandate is smaller than you think.
- The exemptions are narrow. There are carve-outs — certain VAT-exempt activities, bankrupt taxable persons, non-established suppliers, and a flat-rate scheme that's being wound down anyway — and every one of them is a question for your accountant rather than for us. Assume you're in scope until someone qualified tells you otherwise.
And the half that gets least attention while causing the most support calls: receiving is mandatory too. Being able to send a compliant invoice is one obligation; being reachable so your suppliers can send you one is a separate obligation with the same deadline. We had two clients in January whose own invoicing worked perfectly and whose suppliers couldn't deliver to them, because nobody had registered them as a recipient. The complaint arrives as "our supplier says they sent it and we never got it," which sounds like an email problem and isn't.
Four corners, and why the address isn't an e-mail address
Peppol is a four-corner model, and the shape is worth ten seconds because it explains most of the failure modes.
You are corner one. Your access point — a certified service provider — is corner two. Your customer's access point is corner three, and your customer is corner four. You hand your invoice to your provider; your provider looks the recipient up in a distributed registry and delivers it to whichever provider that recipient uses. Neither of you needed to agree on anything bilaterally, which is the entire point of the network and the reason it beat every "log into our supplier portal" scheme that came before it.
The lookup is the part that changes how you think about a customer record. You are not addressing a mailbox that will accept whatever is typed into it. You're addressing a participant — an identifier registered on the network, publishing a declared list of document types it can receive. In Belgium the identifier that matters is scheme 0208, the enterprise number from the KBO/BCE:
<cac:AccountingCustomerParty>
<cac:Party>
<cbc:EndpointID schemeID="0208">0123456789</cbc:EndpointID>
<cac:PartyLegalEntity>
<cbc:RegistrationName>Voorbeeld NV</cbc:RegistrationName>
<cbc:CompanyID schemeID="0208">0123456789</cbc:CompanyID>
</cac:PartyLegalEntity>
<cac:PartyTaxScheme>
<cbc:CompanyID>BE0123456789</cbc:CompanyID>
<cac:TaxScheme><cbc:ID>VAT</cbc:ID></cac:TaxScheme>
</cac:PartyTaxScheme>
</cac:Party>
</cac:AccountingCustomerParty>Ten digits. If they're wrong, nothing lands in a human's inbox to be squinted at. The lookup fails and the invoice is rejected before it goes anywhere — or, in the case we like least, it succeeds against a different company, and now your customer's invoice is sitting in a stranger's accounts payable.
Note also that the enterprise number and the VAT number are the same digits wearing different prefixes, which is exactly the kind of near-identity that survives a data migration and then doesn't. Older setups addressed participants by VAT number under scheme 9925; the enterprise number under 0208 is what you want going forward. If your customer file was built before this mattered, it will contain both, plus a healthy population of neither.
Your master data is the project
Here is the finding we'd put in front of anyone still planning this, and it's the whole post if you only read one section: the work is not in the ERP, it's in the contact records, and the volume of it is a function of how old your customer file is.
An invoice that fails validation fails for one of a small number of reasons, and in our experience almost none of them are about invoicing:
| What fails | What it actually is |
|---|---|
| Recipient not found on the network | Enterprise number missing, mistyped, or still holding a VAT number in a field that wants a KBO number |
| Delivered to the wrong company | A digit transposed years ago into a field nobody validated because nothing used it |
| Rejected on validation | No enterprise number at all — a contact created for a delivery address, now being invoiced |
| Duplicate or ambiguous customer | Three records for the same company, two of them with a number and one of them being the one sales uses |
Every row there is a data-quality problem that existed on 31 December and hurt nobody, because nothing in the building read those fields. The mandate turned a decorative field into a routing key overnight. That's the actual event.
So the first thing we do on these projects is not configuration, it's a dry run: take the entire active customer base, resolve every one of them against the network, and produce a list. That list is the project plan. It's usually somewhere between five and fifteen percent of active customers, it's boring to fix, and fixing it is the difference between going live smoothly and spending January in the accounts-receivable inbox. If you're also mid-migration from another system, fold this into the same pass — it's the same discipline we describe in the data playbook for moving off Teamleader or Exact, just with one more column that now has to be right.
Two structural things come out of that exercise that are worth designing rather than patching:
Validate the number at entry, not at invoicing. A checksum on the Belgian enterprise number is a few lines and catches the transposition at the moment someone can still ask the customer. Catching it eight weeks later, on an invoice, means it's now also a dunning conversation.
Decide which record is the invoiced party. Companies with several sites tend to accumulate one contact per site, and the mandate has an opinion about this that your CRM doesn't: there is one legal entity with one enterprise number, and delivery addresses are a different concept. If you run several entities yourself, that separation needs to be clean on your side too — multi-company in Odoo already forces most of it, and this is one more reason not to fudge it.
What UBL will not carry
The second surprise, and the one that produces the most disappointed silence in a meeting: a structured invoice carries structured fields, and your invoice layout is carrying things that aren't.
Look at a typical Belgian SME invoice and you'll find a free-text block above the lines ("as agreed in our call of 12 May, phase 2 of..."), a project reference that lives in the footer, a delivery note number, occasionally a small table of hours that isn't the invoice lines but explains them. In a PDF all of that is just ink. In UBL, each one either maps to a defined field or it doesn't exist.
Most of it does map, once someone spends an hour with the standard: order references, contract references, delivery dates, a per-line note, a document-level note, buyer accounting references. The work is deciding which field, once, deliberately — not discovering per customer that the reference they need in their AP system was the one you put in a free-text note.
For everything that genuinely doesn't map, there's an escape hatch, and it deserves a warning label. UBL lets you embed a binary document — in practice, your PDF:
<cac:AdditionalDocumentReference>
<cbc:ID>INV-2026-0417</cbc:ID>
<cbc:DocumentDescription>Human-readable rendering</cbc:DocumentDescription>
<cac:Attachment>
<cbc:EmbeddedDocumentBinaryObject
mimeCode="application/pdf"
filename="INV-2026-0417.pdf">JVBERi0xLjcKJc...</cbc:EmbeddedDocumentBinaryObject>
</cac:Attachment>
</cac:AdditionalDocumentReference>That's legitimate and we do send it. What it is not is a place to put data. The whole value of the mandate — for you, not for the tax authority — is that your customer's system can book the invoice without a human retyping it. The moment the thing that matters is only in the attached PDF, you've rebuilt emailing a PDF with more XML around it, and you've done it in a format where the recipient's automation will confidently ignore the part you cared about. If a piece of information changes what the customer owes or how they book it, it goes in a field. The attachment is for the human who wants to see a letterhead.
Which, incidentally, is the argument for still caring about that letterhead. The rendering is now genuinely optional, and it's still what a person looks at when they're deciding whether to pay you — so the work in making QWeb PDFs that clients sign didn't become obsolete on 1 January. It just stopped being compliance work and went back to being design work, which is a better place for it.
While you're in there: put the structured payment communication in the structured field. The +++123/4567/89012+++ reference that Belgian banks reconcile on belongs in PaymentMeans, not only in a note, and getting that right is what lets the reconciliation on the other end happen without a human.
<cac:PaymentMeans>
<cbc:PaymentMeansCode>30</cbc:PaymentMeansCode>
<cbc:PaymentID>+++123/4567/89012+++</cbc:PaymentID>
<cac:PayeeFinancialAccount>
<cbc:ID>BE68539007547034</cbc:ID>
</cac:PayeeFinancialAccount>
</cac:PaymentMeans>"Sent" is not "delivered", and the gap is hours
This is the one that bit us, so we'll be specific.
Sending is asynchronous. Your ERP hands the document to your access point and gets an acknowledgement that means the access point has it — not that the customer's provider accepted it, and certainly not that their accounting system booked it. What comes back later is a transport-level response and, sometimes, a business-level one saying the recipient's system rejected the document. That can arrive minutes later. It can arrive hours later.
If nothing in your setup watches for that, you have built a system with a silent failure mode. The invoice shows as sent in the ERP. The customer never received it. Nobody finds out until the payment doesn't arrive and someone chases it, which in a 30-day-terms business means five weeks of latency on a failure you could have known about the same afternoon.
Our rule after January: treat the Peppol channel as an integration, not as a print queue. Which concretely means three things.
The status on the invoice is a real state machine, not a boolean — queued, accepted by our access point, delivered, rejected — and the rejected state is visible to a human who can act on it, not buried in a technical log. Second, an alert fires on rejections and on documents that have sat in an intermediate state past a threshold, into the same place every other integration failure goes; ours land in GlitchTip alongside the rest of the errors, because a separate dashboard for one integration is a dashboard nobody opens. Third — and this is the cheap one everybody skips — somebody looks at a "not delivered" list once a week. Automation catches the failures it was told about. The weekly look catches the ones it wasn't.
The corollary is that month-end close now depends on a network you don't run. Build the buffer in. Sending everything on the last afternoon of the month worked when sending meant SMTP.
Credit notes, corrections and the document you can't unsend
A PDF invoice with a wrong line got fixed by a phone call and a second PDF. That option is gone, and it's gone in a way that's better in the long run and irritating in the short one.
Once a structured invoice is delivered, it's in the customer's system and quite possibly already booked. The correction path is a credit note referencing the original document, then a new invoice — the accounting-correct sequence that your finance team already knew about and that the sales team had been routing around for years with reissued PDFs. The mandate ends the routing-around.
Three practical consequences. Your credit note has to carry the reference to the invoice it corrects, in the field for that, or the customer's automation can't match them. Your invoice numbering has to be genuinely sequential and gapless, because a "we'll just reissue it under the same number" is now a document that exists twice on a network with timestamps. And any process where invoices get generated automatically — subscriptions and renewals especially, where dunning and recurring billing fire without a human in the loop — needs its approval step honoured, because "generated, reviewed tomorrow" and "generated, delivered instantly" are different risk profiles. The same goes for anything that turns timesheets into invoices: the review gate that felt like bureaucracy is now the last point at which a mistake is cheap.
Odoo, specifically
For the Odoo clients we run, this is the least dramatic part.
Recent Odoo versions ship Peppol support natively, and Odoo itself is a certified access point — you register your company as a participant from within the ERP, and invoices go out over the network from the same button that used to email a PDF. There's a small credit-based charge per document and a verification step when you register. You can also point Odoo at a third-party access point instead, and there are decent reasons to: an existing provider relationship, a group with entities in several countries, or a volume where the per-document economics change.
The configuration itself is short. What takes the time is the list above — the contact data, the field mapping, and deciding what your correction process looks like now. Budget accordingly, and budget it in the right column: this is a data project with a configuration step at the end, not a configuration project.
Two Odoo-specific notes worth having in advance.
Check your customisations against the export before you go live, not after. If someone added fields to account.move over the years — and someone always did — they're invisible to the standard UBL export unless they were mapped there deliberately. That's usually a small module rather than a big one, and it's exactly the boundary we describe in Odoo Studio versus custom modules: a field added by clicking is a field the export doesn't know about. If you do need to extend the export, extend it in a proper module that survives the upgrade rather than by editing anything in place — the format gets revised, and you want to inherit those revisions rather than fork away from them.
Don't reach for a community module before checking the standard behaviour. The OCA ecosystem has e-invoicing modules that predate native support and solve problems Odoo now solves itself; installing one on a current version is how you end up with two exporters disagreeing. Our criteria for letting community code touch production apply here with the volume turned up, because the output of this particular code is a legal document.
When the invoice doesn't start in the ERP
The harder architecture is for clients whose invoices originate somewhere else: a webshop, a booking platform, a field-service app, a custom application that's the actual system of record for what was sold.
The wrong instinct is to give that system its own connection to the network. Two systems issuing invoices onto Peppol under one enterprise number means two numbering sequences, two ideas of what got sent, and a reconciliation problem you'll be solving for years.
One system issues invoices. Everything else feeds it. In practice the ERP is that system, because that's where the accounting has to end up regardless, and the shop or the app posts the commercial event through the API layer — for us, usually the NestJS service that sits in front of Odoo, which is also where the enterprise number gets validated on the way in rather than on the way out. That layer is where you enforce the rule the network is about to enforce for you: no customer without a valid identifier gets as far as an invoice. If you're integrating against Odoo directly instead, the same discipline applies at whichever external API surface you're calling.
For a webshop this has a pleasant side effect. B2B and B2C customers now genuinely diverge at checkout — one of them needs a validated enterprise number and gets a structured invoice, the other doesn't — and that's a decision better made at the point of sale than by a nightly job trying to infer it. On a headless storefront in front of Odoo it's one more field and one more branch, and it's the same instinct as picking the right payment methods per market: local rules belong in the checkout, not in a batch job downstream.
What's coming, and what to design for now
Two things are visible on the horizon and neither is settled enough to build against in detail.
Belgium has signalled a move toward e-reporting — transaction data flowing to the tax authority on something much closer to real time — and the EU's VAT in the Digital Age package sets a direction for cross-border digital reporting later in the decade. Dates have moved before and may move again, so we don't design for a specific one.
We do design for the direction, and the direction is unambiguous: the structured document becomes the record, and more of it becomes visible to more parties, sooner. Which means the things worth doing now are the things that were already worth doing. Clean identifiers on every business customer. Data in fields rather than in prose. One system that issues invoices. Monitoring on the channel. Do those because they make this quarter work, and the next requirement is a configuration change rather than a project.
Where we push back
Not everything asked for in the name of this mandate is worth building.
We've been asked several times for a "Peppol dashboard" — a screen showing every document, its status, retry buttons, the lot. For a company sending fifty invoices a month, that's a UI to look at nothing, three times a week. The list of documents that need human attention is the thing to build, and on those volumes it's a filter and an alert, not a dashboard.
We push back on custom validation layers, too. The access point validates against the standard; writing your own pre-flight checks against a schema someone else maintains means maintaining a second, worse copy of a spec that gets revised. Validate the things that are yours — the enterprise number, the numbering sequence, the approval state — and let the network validate its own format.
And we push back hardest on treating this as an IT project. The decisions that matter are which reference goes in which field, who owns the customer identifier, and what happens when an invoice is rejected. Those belong to finance, with us implementing — which is why this now has a standing section in the fit-gap analysis at the start of every Odoo project, rather than being a task at the end of one.
The short version
The mandate isn't the hard part; it's a deadline attached to a format. The hard part is that a field nobody validated became a routing key, that information which lived in prose now needs a home, and that "sent" acquired a gap between itself and "delivered" that nothing in your process was watching.
Eight months in, the clients who found January uneventful are the ones who did the customer-data pass in November. That's the whole difference. It isn't a technology gap and it isn't budget — it's whether someone ran the list before the deadline ran it for them.
If you're on Odoo and it's working, good; check the rejection list this week and see whether anyone's reading it. If invoices in your business start life somewhere other than the ERP, that's the conversation worth having — it's the one architecture decision here that's expensive to reverse. It's what we do in Odoo implementations and in the custom development and integrations around them, and increasingly it's what shapes the custom applications we build in front of an ERP: the invoice is a data structure now, and the systems that feed it have to act like it.
Want us to publish something specific?
Tell us what you'd like to read and we'll add it to our writing queue.
Get the next one in your inbox
New articles, videos and the occasional engineering note — a short mail when there’s something worth reading, nothing else.