Why three stages, not one
"Is this invoice valid?" is not one question. It is three, answered in order, and each finds a different class of error. Knowing which stage failed turns a message you have to decode into one you can act on.
Stage 1 — syntax
Is the document well-formed XML at all? Unclosed tags, broken encoding, a truncated upload. This stage knows nothing about invoices; it would treat any XML the same way.
Failures here are rare and almost always a transport problem, not a data problem.
Stage 2 — schema
Does the document match its XSD? Does cbc:IssueDate really carry a date, does
cac:AccountingSupplierParty sit where it is allowed to sit, is the cardinality
respected? This stage also decides whether the document is in the right
syntax — CII and UBL are two different XSDs for the same business content.
Failures here almost always mean the generator puts a field in the wrong place or with the wrong type.
Stage 3 — business rules
Most documents that fail, fail here, and the message is hardest to read here. Business rules are written in Schematron and test factual claims with XPath expressions:
1<assert test="cac:Party/cac:Contact" flag="fatal" id="DE-R-002">2 The group "SELLER CONTACT" (BG-6) shall be provided.At this point the document is impeccable XML and schema-conformant. One mandatory business detail is missing — and that alone is enough to reject it.
Three rule sets apply at once
A German B2G invoice is not checked against one rule set but against several stacked on top of each other:
| Layer | Example rule | Origin |
|---|---|---|
| European standard | BR-11 — the buyer address needs a country code | EN 16931 |
| Arithmetic rules of the standard | BR-CO-10 — line totals = net amount | EN 16931 |
| National specialisation | DE-R-001 — payment instructions (BG-16) are mandatory | XRechnung / CIUS |
A document must pass all of them. That is why validating against the European standard alone is not sufficient when the recipient is a German public authority.
How to actually locate a failure
A usable message names three things: the rule ID, the affected field in BT/BG notation, and the severity. The rule ID lets you look up the XPath expression that failed — and that expression tells you exactly where in the document the check looked.
The most common time sink is not mapping the BT number onto your own data model.
BT-10 is the routing ID in a German public-sector context; searching your ERP
for "routing" finds it, searching for "BT-10" does not.
Validation is not a one-off
Rule sets get new versions, and severities move with them. An invoice that passed with a warning last year can fail hard at the same spot this year. If you generate e-invoices, validate against the version your recipient runs — not the one that was current when you built the integration.