DEV Community

Boban Mitevski
Boban Mitevski

Posted on

Your XRechnung validator says "valid". Here's what that doesn't mean.

From January 2027, German companies with more than €800,000 turnover must send their business customers e-invoices (XRechnung or ZUGFeRD); from 2028, everyone must. So a lot of invoicing software is getting an XRechnung export right now, usually followed by a validator in CI. When it turns green, the job looks done.

I build a generator and validator for these formats, and in the past few weeks I kept running into cases where "valid" meant less than people assumed. Here are six, each reproduced on official artefacts: the KoSIT XRechnung 3.0.2 test suite (2026-08-31) and the Peppol BIS Billing 3.0.21 rules.

1. A validator only sees what's there

Take the official KoSIT test invoice 01.01a-INVOICE_ubl.xml. Its buyer name is, literally:

<cbc:RegistrationName>[Buyer name]</cbc:RegistrationName>
Enter fullscreen mode Exit fullscreen mode

It's a test invoice, so that's fine. But it shows the point: the rules check that a buyer name exists, not that it's a name. I took the same invoice and changed only the buyer's name and street:

Buyer name and street Result
left empty rejected: BR-07 "An Invoice shall contain the Buyer name (BT-44)", PEPPOL-EN16931-R008 "Document MUST not contain empty elements"
not set valid
[Buyer name] (original) valid

I found exactly this pattern in an open-source invoicing tool this week: missing customer fields were filled with a not set default, so an incomplete customer record produced an invoice every validator accepts.

The same goes for tax categories. A small German business under the §19 UStG exemption (Kleinunternehmer) should mark its 0 % lines as E (exempt, with an exemption reason). Another tool marks every 0 % line as Z (zero-rated). That invoice passes the XRechnung rules without even a warning, and it's still the wrong category.

Takeaway: never fill required fields with placeholders or defaults. Leave them empty so validation catches them, or better, refuse the export and tell the user which field is missing. And derive the VAT category from the tax situation, not from the rate.

2. Schema-valid isn't valid

XSD validation checks structure: element names, order, data types. The business rules live in Schematron. I changed one number in the same test invoice, the total VAT amount (BT-110), from 22.04 to 23.04. The file is still perfectly schema-valid. The Schematron rules reject it:

  • BR-CO-14: Invoice total VAT amount (BT-110) = Œ£ VAT category tax amount (BT-117)
  • BR-CO-15: Invoice total amount with VAT (BT-112) = total without VAT (BT-109) + total VAT (BT-110)

I've seen more than one project whose "validator" is well-formedness plus XSD. That catches typos in element names and very little else.

Takeaway: if your check doesn't run the EN 16931 Schematron (and for XRechnung the BR-DE rules), it isn't validating the invoice, only the XML.

3. The Schematron files alone don't reproduce KoSIT's verdict

For XRechnung, the reference is the KoSIT validator with its XRechnung configuration (validator-configuration-xrechnung). That configuration doesn't just bundle the Schematron files: its scenarios re-grade some rules. Comparing plain rule execution with KoSIT 1.6.3 and the 2026-08-31 configuration, we found for example:

  • UBL-CR-646 (sub invoice lines) is a warning in the plain rules, an error in KoSIT.
  • CII-SR-465 / CII-SR-466 (person name and department name in one contact) are errors in KoSIT.

The second one has real consequences: a popular .NET library's XRechnung CII output writes both PersonName and DepartmentName when you set a contact with a department, and the current KoSIT configuration rejects those invoices (ZUGFeRD-csharp discussion #986; the workaround is to set only one of the two).

Takeaway: if you run the Schematron files yourself instead of KoSIT, keep a small severity-override table keyed by rule ID, and re-check it at every KoSIT configuration release. With those overrides, our results matched KoSIT on all 80 standard and technical-case instances of the official test suite.

4. Which rule version is your validator running?

Mustang is a popular, well-maintained Java toolkit for ZUGFeRD, and it can validate XRechnung too. But its released versions (up to 2.26.0) still bundle XRechnung Schematron 2.4.0 from July 2025, while KoSIT has been on 2.6.0 since 2026-08-31. An update is in progress (mustangproject#1274). Until then, "Mustang says OK" isn't quite "KoSIT says OK" for XRechnung.

That's not a dig at Mustang. Every validator lags at some point, because rules get bugfix releases twice a year. The problem is not knowing which version you ran.

Takeaway: record the rule-set name and version in every validation report you store, and pin the versions in CI so an upgrade is a reviewed change.

5. Warnings are not failures

The same official test invoice 01.01a comes back with a warning:

BR-DE-TMP-32: Eine Rechnung sollte zur Angabe des Liefer-/Leistungsdatums entweder BT-72 "Actual delivery
date", BG-14 "Invoicing period" oder in jeder Rechnungsposition BG-26 "Invoice line period" enthalten.

It's still a valid invoice. KoSIT marks it acceptable. If your pipeline (or your "e-invoice OK" tag on incoming documents) treats every warning as a failure, you'll reject correct invoices.

Note the "entweder … oder": the rule is satisfied by one of the three options. A single delivery date or invoicing period at document level is enough; you don't need a date on every line.

Takeaway: map only errors (flag="fatal" in the Schematron) to "invalid", and show warnings separately.

6. Pin what isn't versioned

For Peppol BIS Billing, the authoritative rules are the Schematron files published on docs.peppol.eu/poacc/billing/3.0/files/. Two traps:

  • The URLs contain no version: each release overwrites the same files. A CI job that downloads them silently changes when Peppol releases. Pin them by SHA-256 so a new release fails your build until you've looked at it.
  • "3.0.21" is two different files. The GitHub repository (OpenPeppol/peppol-bis-invoice-3) got a 3.0.21 release on 2026-10-28 (note: no v, unlike v3.0.20, so a v* tag filter misses it). Its PEPPOL-EN16931-UBL.sch is not the file on docs.peppol.eu: it contains a hotfix to rule PEPPOL-EN16931-P0112 (invoice type code 389 only between German parties, 384 also between Slovak ones). Same version number, different verdicts. The checksum is the only reliable name.

Checklist

  • [ ] Required fields are never filled with placeholders or defaults; missing data stops the export.
  • [ ] Validation runs the Schematron rules, not just the XSD.
  • [ ] For XRechnung: KoSIT's configuration, or your own severity overrides that match it.
  • [ ] Every stored report names the rule set and its version.
  • [ ] Only errors fail; warnings are shown, not counted as failures.
  • [ ] Downloaded rule files are pinned by checksum.

Try it

To reproduce any of this: KoSIT's validator (Java, Apache-2.0) with validator-configuration-xrechnung is the reference for XRechnung, and Mustang covers ZUGFeRD PDFs. If you'd rather not install Java, the validator I build runs the XRechnung 3.0.2, Peppol BIS 3.0.21 and ZUGFeRD / Factur-X rules (PDFs included) over HTTP, free for test files:

curl -X POST https://valinvo.bmk-hub.com/v1/validate \
  -H 'X-Accept-Terms: 1.0' -H 'Content-Type: application/xml' \
  --data-binary @invoice.xml
Enter fullscreen mode Exit fullscreen mode
{"ruleSet": "xrechnung-3.0.2-ubl-invoice", "valid": true, "errors": [], "warnings": [{"rule": "BR-DE-TMP-32", "...": "..."}]}
Enter fullscreen mode Exit fullscreen mode

Every finding comes with its rule ID and XPath location. There's also a web version at valinvo.bmk-hub.com, and a GitHub Action (valinvo-validate-action) for CI.

Disclosure: I build Valinvo. All results above were run on 2026-10-29 and re-checked on 2026-11-01 against the official KoSIT test suite (2026-08-31) and the Peppol BIS 3.0.21 artefacts from docs.peppol.eu. I wrote this with the help of an AI assistant; the tests and facts were checked against the official files.

Top comments (0)