XRechnung & ZUGFeRD for Developers: EN 16931 Without the Pain
As of: 01.09.2026
You've been asked to "just add XRechnung export." Here's the mental model that saves you a week of spec reading — and the traps that make validators reject your files.
The layer cake
- EN 16931 — the semantic model: ~160 business terms (BT-1 invoice number, BT-10 buyer reference, BG-23 VAT breakdown…). Format-agnostic.
- Syntax — two XML bindings exist: UN/CEFACT CII and UBL. Germany accepts both; ZUGFeRD always uses CII. Pick CII if you want one syntax for both formats.
- CIUS — country profiles that tighten the base standard. XRechnung is Germany's CIUS: it upgrades optional fields to mandatory (buyer reference, seller contact, payment details) via the BR-DE rule set.
- Packaging — XRechnung ships as bare XML; ZUGFeRD/Factur-X embeds the same CII XML into a PDF/A-3 attachment with XMP metadata.
Validation is two different things
| Layer | Checks | Tooling |
|---|---|---|
| XSD schema | structure: elements, order, types | any XML library |
| Schematron | business rules: sums, code lists, BR-DE-* | the official KoSIT validator (Java) |
Passing XSD means little — most real-world rejections are Schematron rule violations. Wire the KoSIT validator into CI; treat it as the reference implementation of "valid."
The mistakes everyone makes once
- Rounding (BR-CO- rules).* Round each line total to 2 decimals first, then sum. Summing raw floats and rounding once at the end produces 1-cent mismatches that fail BR-CO-15. Same for each VAT group:
tax = round(basis × rate / 100, 2)per group, not on the grand total. - BR-DE-15: XRechnung requires a buyer reference (BT-10). B2G it's the Leitweg-ID; B2B any agreed reference works. Omit it and the file is invalid — even though EN 16931 itself doesn't require it.
- VAT category vs rate consistency: category
Sneeds rate > 0;E(exempt),AE(reverse charge),K(intra-community) need rate 0 plus an exemption reason (BT-120). - Floats aren't the enemy, string formatting is. Amounts must serialize with exactly 2 decimals (
1802.85, not1802.8499999). - ZUGFeRD is not "attach XML to PDF". The attachment must be declared in the PDF/A-3
/AFstructure withAFRelationship, plus Factur-X XMP metadata naming the profile. Standard PDF libs won't do this for you.
Don't hand-roll the XML
Mature open-source libraries exist and are battle-tested against KoSIT:
- PHP:
horstoeko/zugferd(CII, all profiles, PDF embedding) — what our engine uses - Java: Mustang project
- .NET: ZUGFeRD-csharp
- Python/JS: options exist but are thinner — validate aggressively
Build your invoice as a plain data object, map it to the library's builder, validate with XSD locally and KoSIT in CI. Total: a few hundred lines instead of a few thousand.
Or skip the integration entirely
If you just need compliant files (or an API later) without owning the mapping: NurRechnung generates XRechnung 3.x and ZUGFeRD from one data set, validated against the official rules — free validator and viewer included for debugging your own output.
Format engineering guide — not tax advice.