← Blog

Integrations

Peppol UBL quirks: 18 silent fails from a real rollout

A 23-person Arnhem installer's credit note returned 200 OK from the Access Point on a Friday, then vanished in the buyer's ERP. Here are the eighteen quirks we caught next.

Jacob Molkenboer· Founder · A Brand New Company· 21 Jun 2026· 9 min
Cream paper envelope sealed with dark green wax, chartreuse ribbon through the flap, brass tag and red rubber stamp on linen.

The credit note hit the Access Point at 16:47 on a Friday in March. The AS4 receipt came back with 200 OK and a SignalMessage two seconds later. By Monday morning the controller at the 23-person installer in Arnhem was on the phone with the main contractor's accounts payable team. No credit. No record. No bounce. The invoice the credit note was correcting sat paid in the buyer's ledger, untouched.

The credit note had reached the buyer's Access Point. It had passed AS4 transport. And then somewhere between the Peppol envelope and the buyer's Exact Globe import, the cac:OrderReference had been stripped, the matching engine couldn't find the original invoice, and a junior clerk had moved it to a "to review" queue that nobody read.

That was quirk number one of eighteen. We caught the other seventeen across the next six weeks of rolling out a facturatie-agent that watches the AP receipts, replays the UBL through three independent schematron rule sets, and flags any case where the field count between what we sent and what the buyer parsed differs by more than zero. Here is the cheatsheet, ranked by how quietly each failure mode breaks things.

Why 200 OK on Peppol is not a delivery confirmation

The Peppol four-corner model has the sender's AP hand the message to the receiver's AP over AS4. The 200 OK on AS4 means the receiving AP accepted the envelope and persisted it. It does not mean the UBL passed schematron, that the buyer's ERP parsed it, or that a human will ever see it. That gap, between transport acceptance and business acceptance, is where most of these quirks live.

The OpenPeppol AS4 profile is explicit on this point in its AS4 Profile 2.0 documentation. The Message Service Handler returns the receipt before the receiver's business application has touched the payload. If you treat the receipt as proof of delivery, you ship blind.

Warning

Treat the AS4 receipt as a transport acknowledgement, not a delivery confirmation. The 200 OK lives at the wrong layer to know whether the buyer's ERP parsed your document, accepted it, or quietly dropped fields on import.

Five silent strips that destroy traceability

These are the worst. The Access Point returns a clean receipt, the buyer's ERP imports the document, and a field you needed is gone. No log entry on either side names the loss.

1. OrderReference disappears on partial credit notes

UBL allows cac:OrderReference on a CreditNote. Some AP implementations normalise the document, see that the parent cac:OrderReference wraps a single empty cbc:SalesOrderID (because a partial credit doesn't always have one), and prune the whole element including the cbc:ID we needed for buyer matching. Always populate both children explicitly, or carry a non-empty default for SalesOrderID.

2. GLN buyer identifier dropped above €25,000 on ViewPoint-Bouw handover

This one took us a week to reproduce. ViewPoint-Bouw's EDI handover routes invoices above a configurable threshold through a second validation pass that re-serialises the buyer party block. The re-serialiser preserves cbc:EndpointID schemeID="0088" but drops cac:PartyIdentification/cbc:ID schemeID="0088" when the GLN value is identical to the EndpointID. The buyer's ERP joins on PartyIdentification, not EndpointID. Result: invoice arrives, supplier match fails, document lands in a manual queue.

3. PaymentID leading zeros stripped during canonicalisation

A payment reference of 0000000000123456 hits an AP that canonicalises whitespace and numeric strings as part of its XML signature pipeline. The reference now reads 123456. The buyer's bank reconciliation matches on the 16-digit form. Three invoices spent a week unmatched before we caught it.

4. Note elements truncated past 1024 characters without warning

The NLCIUS schematron warns on long cbc:Note, it does not error. Several APs pass it through. The buyer's ERP, configured for EN 16931 minimums, truncates at 1024 and writes the rest to a side table that nobody monitors. If your note carries the original work order detail, it's gone.

5. BillingReference dropped when its parent ordering block is empty

Same family as quirk 1. A credit note's cac:BillingReference/cac:InvoiceDocumentReference/cbc:ID is the one field that links it back to the original invoice. If the sibling cac:AdditionalDocumentReference is empty, some AP normalisers prune the empty parent and take BillingReference with it. The credit note arrives, the link to the original is gone, and the buyer's AP queue just shows "unallocated".

Four cases where 200 OK precedes a silent rejection

6. NLCIUS schematron passes, buyer's local CIUS rejects

Several Dutch buyers run a stricter local CIUS on top of NLCIUS, tighter on VAT category codes, allowance reasons, and contract reference format. The AP validates against NLCIUS, returns 200 OK, then the buyer's ERP silently rejects on the stricter rule set. No bounce reaches the sender because the rejection happens past the Peppol layer.

7. Schematron rule version drift between sending and receiving AP

Between January and March 2026 we saw AP pairs running NLCIUS 1.0.3.5 (sender) against 1.0.3.6 (receiver). The version delta added a rule on cac:TaxCategory for reverse-charge construction. Senders pass, receivers reject post-receipt. The Peppol BIS Billing 3.0 docs publish the canonical rule set; pin your validation to the version your buyers run, not to what your AP ships.

8. Duplicate ID accepted with different amounts

Send two invoices with the same cbc:ID and different cbc:PayableAmount. Both return 200 OK. Some APs deduplicate, some don't. The buyer's ERP either takes the first, takes the last, or imports both and lets a clerk choose. None of those are good outcomes, and the sender has no visibility into which happened.

9. Reverse-charge VAT misread as zero-rated

Construction in NL uses BTW-verlegd (reverse charge). The UBL category code is AE. Some buyer ERPs that haven't been updated read AE as a zero-rate alias and post the line VAT-free instead of as reverse-charge. The AP's schematron pass doesn't catch this; it's a downstream parser bug, but the invoice "succeeds" with the wrong tax treatment.

Five schematron and validator mismatches

10. AllowanceCharge at document level vs line level

EN 16931 permits both. NLCIUS permits both. Some buyer ERPs only honour line-level. A document-level discount of €450 vanishes; the line totals don't sum to the document total in their ledger and the import fails with a generic "totals mismatch" that the importer logs and nobody reads.

11. InvoicePeriod on line overrides document, except when it doesn't

UBL allows per-line cac:InvoicePeriod. Some receivers ignore line-level periods and use the document-level period for revenue recognition. If you mix monthly maintenance contract lines with one-off installation lines, your installer revenue lands in the wrong period and your buyer's accruals look wrong by a month.

12. BuyerReference vs OrderReference confusion at small buyers

Roughly half the Dutch buyer ERPs we touched route on cbc:BuyerReference (a free-form string set by the buyer for their own AP routing). The other half route on cac:OrderReference/cbc:ID. Get this wrong and your invoice lands in someone else's project folder with no error and no notification.

13. EndpointID schemeID inconsistency

NL: 0106 for KvK, 0190 for OIN, 0088 for GLN. We saw three buyers in two months whose configured EndpointID schemeID didn't match the one their AP advertised in SMP. Mail it to 0106, their AP only routes 0190, the AP returns 200 OK and queues it for manual triage that may or may not happen.

14. Embedded PDF attachment size limits vary by AP

UBL has no documented hard limit on cac:AdditionalDocumentReference/cac:Attachment/cbc:EmbeddedDocumentBinaryObject. APs disagree in practice. We've seen 5 MB, 10 MB, and 20 MB practical ceilings. Past the AP's ceiling, some reject (good), some strip the attachment and forward without it (bad).

Four ViewPoint-Bouw construction extensions that drift

The Dutch construction sector layers its own EDI requirements on top of Peppol via ViewPoint-Bouw and adjacent BouwConnect endpoints. These behave well when both sides are configured identically and ugly otherwise.

15. ProjectReference dropped when schemeID is missing

The construction profile requires cac:ProjectReference/cbc:ID with a schemeID identifying the project numbering authority. Omit the schemeID and most APs pass it; the receiving construction ERP rejects post-import and emails a clerk on the buyer's side, not the sender. The sender sees 200 OK and assumes the project ref made it.

16. ContractDocumentReference for framework agreements loses its typecode on re-serialisation

Framework agreements use cac:ContractDocumentReference with a typecode. The ViewPoint-Bouw re-serialiser at the buyer's side preserves the ID but drops the typecode, after which their ERP treats it as a single-shot order. Period invoicing breaks. The fix lives on the buyer's side, but you have to know to escalate.

17. Construction subcontracting party block ignored on smaller buyer ERPs

The construction extension carries a subcontracting party block for chain-liability reasons. Smaller buyer ERPs don't model it and silently drop it on import. For the installer this matters because their main contractor's AP forwards downstream to a chain-liability tracker that depends on the block being present.

18. Round-trip XML normalisation reorders AllowanceCharge children

UBL is element-order sensitive. The ViewPoint-Bouw re-serialiser, in older versions, reorders cac:AllowanceCharge children alphabetically. The buyer's downstream schematron then fails on element order, but the failure happens past the AP, so the sender sees 200 OK. Upgrade ViewPoint-Bouw or carry a schematron pre-check on send.

How we instrumented the agent to catch this

Catching these silently requires more than a send-and-forget pipeline. Every outbound document gets validated against three rule sets before the AP gets to 200-OK us into a false sense of safety, and a reconciliation job pulls AP-side and buyer-side document copies the next morning to diff them.

# Validate the UBL we're about to ship against three rule sets
# before the AP returns 200 OK on transport.
xmllint --schema UBL-Invoice-2.1.xsd invoice.xml --noout
saxon -s:invoice.xml -xsl:NLCIUS-1.0.3.6.xsl    -o:nlcius.svrl
saxon -s:invoice.xml -xsl:PEPPOL-BIS-3.0.xsl    -o:peppol.svrl
saxon -s:invoice.xml -xsl:BUYER-LOCAL-CIUS.xsl  -o:buyer.svrl

# Post-send: pull the AP-delivered copy and diff against what we sent.
curl -sS "$AP_API/messages/$MSG_ID/payload" -o delivered.xml
xmlstarlet c14n invoice.xml   > sent.canon.xml
xmlstarlet c14n delivered.xml > delivered.canon.xml
diff -u sent.canon.xml delivered.canon.xml || alert "silent strip on $MSG_ID"

One pass before you ship

If you only have time to add one check to your e-invoicing pipeline this week, add this: after every outbound document, fetch a copy of the UBL the buyer's AP actually delivered downstream (most buyers can give you this on request) and diff it against what you sent. Compare element counts, attribute presence on the buyer party block, and the values of cbc:ID on cac:OrderReference and cac:BillingReference. If those don't match byte-for-byte, you have a silent strip somewhere on the path.

When we built the facturatie-agent for the Arnhem installer, the surprise was not that quirks existed (Peppol is a federated network and federation breeds quirks) but that almost all of them lived past the AP receipt, in the gap between transport accepted and business accepted. We ended up running a nightly reconciliation that pulls AP-side and buyer-side document counts and field diffs into the same dashboard. The week we shipped it, the controller stopped calling the main contractor. The work is part of how we build AI agents for operations teams that move money.

Key takeaway

On Peppol, the 200 OK arrives before the buyer's ERP touches your UBL. Silent field strips on partial credits and GLN identifiers happen in that gap.

FAQ

Does a 200 OK from a Peppol Access Point mean my invoice was delivered?

No. AS4 returns 200 OK once the receiving AP persists the envelope. The buyer's ERP hasn't touched it yet, and many silent failures happen past that point.

Why does OrderReference disappear on partial credit notes?

Some AP implementations normalise UBL and prune parent elements with empty children. If cac:OrderReference wraps an empty SalesOrderID, the whole element including cbc:ID can be stripped.

What is NLCIUS and how does it relate to Peppol BIS Billing 3.0?

NLCIUS is the Dutch Core Invoice Usage Specification, a national tightening of EN 16931 used by Simplerinvoicing and the Dutch Peppol Authority. It runs alongside Peppol BIS Billing 3.0 validation.

How do I detect silent field strips on the Peppol network?

After every send, fetch the UBL the buyer's AP delivered downstream and diff it against what you sent. Compare element counts, attribute presence and ID values. Byte-level mismatches signal a strip.

integrationsai agentsautomationcase studyoperationsworkflow

Building something?

Start a project