Home/Blog/How Schematron validation works for e-invoices

How Schematron validation works for e-invoices

An accessible explanation of Schematron rule sets and why they are the primary quality gate for structured invoices.

If you have worked with PEPPOL or any structured invoice format, you have probably seen error messages that mention Schematron. A document passes XML schema validation but fails Schematron, or the other way around. Understanding what Schematron is and how it differs from XSD validation makes these error messages much easier to diagnose — and helps you build better invoice generation code.

What Schematron is

Schematron is a rule-based validation language for XML. Rather than defining what the document structure must look like (as XSD does), Schematron defines what conditions must hold true within a document that already has a valid structure.

Think of XSD as the grammar checker: it ensures the document is well-formed, that elements appear in the right places, and that field types match (a date field contains a date, an amount field contains a number). Schematron is the business logic checker: it ensures the numbers add up, that mandatory combinations of fields are present together, and that values from one part of the document are consistent with values elsewhere.

This is covered in more depth in the XSD vs Schematron post, but the short version is: you need both. Schematron catches the errors that XSD cannot express.

The structure of a Schematron file

A Schematron file (.sch) is itself an XML file. The key building blocks are patterns, rules, and assertions.

Patterns (<sch:pattern>) group related rules together. For PEPPOL BIS, patterns are often organised by document section — one pattern for header-level rules, one for tax totals, one for line items.

Rules (<sch:rule>) apply to a specific context in the document, defined by an XPath expression. For example:

<sch:rule context="cac:TaxTotal">
  ...
</sch:rule>

This rule applies to every TaxTotal element in the document.

Assertions (<sch:assert>) state what must be true within that context. If the assertion evaluates to false, the validator reports an error:

<sch:assert test="cbc:TaxAmount" flag="fatal">
  [BR-CO-14] Invoice total VAT amount must be provided.
</sch:assert>

Reports (<sch:report>) work in the opposite direction — they fire when the condition is true, typically to flag a warning rather than an error:

<sch:report test="cbc:TaxAmount = 0" flag="warning">
  Tax amount is zero. Verify this is intentional.
</sch:report>

The flag attribute indicates severity. PEPPOL BIS uses fatal for errors that make the invoice non-conformant and warning for issues that should be reviewed but do not fail validation.

How validators run Schematron

Schematron validation is not a single-pass operation. The process involves a transformation step:

  1. The Schematron file (.sch) is compiled into XSLT using a standard stylesheet (the "skeleton" XSLT). This produces a validator XSLT.
  2. The validator XSLT is applied to the invoice XML document.
  3. The output is an SVRL (Schematron Validation Report Language) XML file that lists every fired assertion and report, together with the location in the source document and the error message.
  4. Your tooling reads the SVRL and presents the errors to the user.

Most e-invoicing validators wrap this process so you never see the SVRL directly — you just see a list of error messages with codes. But if you are building your own validation pipeline, knowing that the output is SVRL means you can parse it programmatically to extract error codes, locations, and severities.

PEPPOL BIS Schematron rules in practice

The PEPPOL BIS Billing 3.0 Schematron is maintained by OpenPEPPOL and published alongside the specification. It contains around 200 rules covering:

  • Calculation checks — does the sum of line amounts equal the invoice total? Does tax base × rate equal the tax amount (within rounding tolerance)?
  • Mandatory field combinations — if a tax exemption reason is required by the tax category, is it present?
  • Code list validation — is the payment means code a valid UNCL4461 value? Is the country code a valid ISO 3166-1 alpha-2 value?
  • Format checks — does the IBAN look like a valid IBAN? Does the date use the correct ISO 8601 format?

Error codes follow the pattern BR-xx (Business Rule) for EN 16931 rules and PEPPOL-EN16931-Rxx for PEPPOL-specific extensions. When you receive an error like [BR-CO-10] Sum of Invoice line net amount must equal the Invoice net amount, the code BR-CO-10 maps directly to a rule in the specification document, so you can look up exactly what is being checked.

Swiss-specific Schematron rules

SwissDIGIN publishes its own Schematron on top of the PEPPOL BIS rules. These cover Swiss-specific constraints that the generic PEPPOL rules do not check:

  • UID format (CHE-\d{3}\.\d{3}\.\d{3} MWST)
  • QR-IBAN structure when a QR reference is present
  • Currency consistency for CHF domestic invoices
  • Swiss VAT rate values (8.1, 2.6, 3.8)

In a Swiss PEPPOL validation pipeline, you run the PEPPOL BIS Schematron first, then the SwissDIGIN Schematron. Both must pass for the invoice to be considered conformant. The SwissDIGIN validation tool does this automatically and groups errors by which rule set they come from.

Common Schematron errors and what causes them

Calculation mismatch (BR-CO-10, BR-CO-13): The most frequent error in practice. Your invoice generation code rounds intermediate values differently from how the Schematron expects them. The usual fix is to calculate totals from the line amounts rather than recalculating from rates — and to store amounts in the XML with exactly two decimal places.

Missing tax exemption reason (BR-E-10, BR-AE-10): When using tax categories E (exempt) or AE (reverse charge), an exemption reason text or code is mandatory. Many ERP systems omit this when the invoice generator is configured for standard-rate VAT and the exemption path is not tested.

Invalid code list value: Using a country code like CH when the field expects an ISO 3166-1 alpha-2 value is fine — CH is valid. But using SUI or a full country name will fail. The same applies to currency codes, unit codes (UN/ECE Recommendation 20), and payment means codes.

Duplicate tax subtotal: The Schematron requires exactly one TaxSubtotal per distinct tax category. If your invoice has two lines at 8.1% VAT, you must sum them into one TaxSubtotal, not create two separate blocks.

Testing Schematron before going to production

The practical approach is to run Schematron validation against test invoices in your development environment before connecting to a live PEPPOL network or submitting to a biller's portal. Tools for this:

  • SwissDIGIN validation tool — web-based, covers PEPPOL BIS + Swiss rules
  • Ecosio validator — online validator for PEPPOL BIS
  • phive (CLI) — the OpenPEPPOL command-line validator, runs the official rule set locally
  • The peppol-bis-invoice-3 Saxon XSLT — if you want to run validation in your own code using a Java or .NET XSLT processor

The post on testing your PEPPOL invoice against Schematron rules walks through running the official rules with phive and interpreting the output.