Why a second layer of checking
An XSD can do a lot: it fixes which elements may appear, in what order, how often, and with what data type. What it cannot do is make a statement about relationships.
"The total net amount must equal the sum of the line net amounts" is not a question of structure. Both fields are present, both are of the right type, both sit in the right place. The invoice can still be wrong.
That is what Schematron is for.
What a rule looks like
Schematron rules are remarkably readable once you know the pattern:
1<rule context="cac:AccountingSupplierParty/cac:Party">2 <assert test="cac:Contact" flag="fatal" id="DE-R-002">3 The group "SELLER CONTACT" (BG-6) shall be provided.4 </assert>5</rule>Four parts, and each is useful:
context— where in the document the rule applies. Here: at the seller's party.test— the XPath expression that must be true. Here: acac:Contactmust exist.flag— the severity.fatalrejects,warningnotes.id— the identifier the violation is reported under.
The text between is the message you see in the result. It names the field in BT/BG notation, because the same rule has to hold for CII and UBL — the element names differ, the BG number does not.
A rule that calculates
1<assert test="xs:decimal(cbc:LineExtensionAmount) =2 round(10 * 10 * sum(//cac:InvoiceLine/cbc:LineExtensionAmount)) div 100"3 flag="fatal" id="BR-CO-10">4 Sum of Invoice line net amount (BT-106) = Σ Invoice line net amount (BT-131).5</assert>Here you can see why an XSD would not have sufficed: the test sums across all lines and compares the result with a field somewhere else entirely.
🔵 You can also see why rounding is so often the problem. The expression rounds
explicitly to two decimal places. Summing unrounded values and rounding only the
result lands a cent off on invoices with many lines — and the rule is fatal.
The rule families
A document is never checked against a single rule set but against a stack. The prefix tells you who wrote the rule:
| Prefix | Origin | Example |
|---|---|---|
BR- | core of EN 16931 | BR-11 country code in the buyer address |
BR-CO- | arithmetic rules of the standard | BR-CO-10 line totals |
BR-DE-, DE-R- | German add-ons | DE-R-001 payment instructions |
PEPPOL- | the Peppol framework | rules for BIS Billing |
UBL-CR-, CII-SR- | syntax-specific | code lists, attributes |
A document must pass all that apply. That is why checking against EN 16931 is not enough when the destination is a German public authority.
Reading a message
1[BR-DE-2] fatal2The group "SELLER CONTACT" (BG-6) shall be provided.Step 1 — severity. fatal? Then that is the reason for rejection. warning?
Then the cause is elsewhere.
Step 2 — prefix. BR-DE- says: German add-on rule. Your document may satisfy
the European standard perfectly and fail only the national tightening.
Step 3 — BT/BG number. BG-6 is the seller's contact group. Search your
system for whatever fills that group — not for the element name.
Step 4 — if needed, read the test. Where the message stays ambiguous, the XPath expression is the binding answer. It says exactly where the check looked.
Why rule sets have versions
Schematron rule sets are maintained, and a new version changes not only rules but
occasionally severities. A rule that was a warning last year can be fatal
this year.
An uncomfortable but important sentence follows for practice: an invoice that went through is no assurance for the next one. Validate against the version your recipient runs — not the one that was current when you built your integration.