The most expensive place to validate your invoice output is your customer's accounting system. By the time DATEV or a public-sector portal refuses the file, the invoice is late, the bug report is second-hand and the rule code arrives in German. Every one of those rejections is reproducible at build time, because the receiving side runs published, versioned rule sets: an XML schema, the EN 16931 business rules, the German CIUS rules and, for hybrid files, PDF/A-3 container checks. Wire the same layers into your tests and the rejection email never gets written.
The four layers, and what each one catches
| Layer | What it checks | Typical catch |
|---|---|---|
| 1 · XML schema (XSD) | Structure: elements, order, types | A misspelled element, a date in the wrong slot |
| 2 · EN 16931 business rules | Semantics: BR and BR-CO rules on totals, VAT, code lists | Line amounts that do not sum to the header, a VAT category without an exemption reason |
| 3 · German CIUS rules (BR-DE) | XRechnung profile only: German mandatory fields | Missing buyer reference, missing seller contact, the cash-discount pattern |
| 4 · Container (hybrid files) | PDF/A-3 conformance, the embedded XML, XMP metadata | A valid XML inside a non-conformant PDF, a stripped or misnamed attachment |
Two consequences follow. First, "the XML is schema-valid" clears layer 1 of 4, which is why it proves so little. Second, the layers are independent: a perfect XRechnung XML can ship in a broken container, and a perfect container can carry an invoice whose totals contradict themselves.
Generator bugs that pass three layers
These are the defects we see most in generated files, each of them invisible to the schema:
- Totals derived twice. The line amounts are rounded for display, then the header total is computed from the unrounded values. Sum exactly the numbers you emit, then never re-derive them; the totals chain is checked as a chain.
- VAT rounded per line. The category tax amount must come from the category's taxable base, rounded once. Per-line rounding drifts a cent and fails the check.
- Culture-sensitive formatting. Amounts must use a dot decimal separator with two decimals, whatever the server's locale; CII dates use format 102, plain YYYYMMDD. A German locale on the build server is a classic silent corrupter.
- Empty optional elements. Emitting
<ram:Email></ram:Email>because the field was blank turns an optional element into a failing mandatory-content check. Omit what you do not have. - A default profile that is too small. MINIMUM and BASIC WL validate happily and are still booking aids, not invoices. Default to EN 16931.
- Duplicate attachment names. Every embedded document must have a unique file name (BR-DE-22, an error). Attaching two timesheets both called scan.pdf sinks the invoice.
- A stale specification identifier. BT-24 tells the receiver which rule set to apply; ship the identifier of the profile you actually implement.
Testing it: golden files plus rule runs
The pattern that works is unspectacular:
- Golden files. A handful of representative invoices (plain domestic, reverse charge, credit note, discounts, an attachment) rendered by your code and committed. A diff on the golden file turns any generator change into a visible review.
- Rule runs on every golden file. Validate against the official artefacts, not a hand-rolled subset: the EN 16931 schematron plus, for XRechnung output, the KoSIT rules with their error-or-warning classification. The ZUGFeRD validator runs exactly these published rule sets and reports findings in plain English, which makes it a quick oracle for a failing case even if your CI runs the schematron directly.
- Edge-case invoices as regression tests. Zero-rated lines, a 0.01 rounding case, umlauts and a 35-character buyer reference. Cheap to add, and each one encodes a rejection you never want twice.
When a customer still reports a rejection, ask for the code, not the prose: the German codes are stable identifiers, and the rejection guide translates the common ones.
What to do now
- Add the four layers to your test suite in order; each one is a published artefact, not guesswork.
- Commit golden files for your real invoice shapes and validate them on every build.
- Kill the two rounding bugs (re-derived totals, per-line VAT) by construction: one rounding site, reused everywhere.
- Default the profile to EN 16931 and set BT-24 from the same constant your XMP writer uses.
Further reading
What Germany expects from invoices in general, deadlines included, is mapped in the English e-invoicing guide. The container side, embedding the XML into PDF/A-3 without breaking either, has its own pitfalls and its own article. And running these layers as a service call instead of your own stack, with curl and C# samples, is what the ZUGFeRD API page covers; the endpoint itself is documented in the validate/zugferd reference.
Legal position as at September 2026, last reviewed on 25 September 2026. Based on the European standard EN 16931 with its published validation artefacts, the KoSIT rule set for XRechnung 3.0 and the ZUGFeRD and Factur-X specifications.
Frequently asked questions
Which rule sets should my pipeline run?
The published ones: the XSD schemas of your syntax, the EN 16931 schematron, and for XRechnung output the KoSIT schematron with its error-or-warning classification. Hybrid output adds PDF/A-3 conformance and the embedding checks.
The XML validates against the schema. Why is it still rejected?
Because the schema is one layer of four. The business rules check the semantics on top of the structure, the German rules add mandatory fields the schema knows nothing about, and hybrid files add the container.
Do I need PDF/A checks for XRechnung output?
No. An XRechnung is pure XML; there is no container. The container layer applies to hybrid formats like ZUGFeRD and Factur-X.
How exact do the totals have to be?
Exact. The totals chain is arithmetic over the values in the file, and the per-category VAT is expected from the category base, rounded once to two decimals. Any second rounding site will eventually drift a cent and fail.
Do the German rules apply to every invoice I generate?
Only to the XRECHNUNG profile. A ZUGFeRD file in the EN 16931 profile for a business customer is checked against layers 1, 2 and 4, without the BR-DE set. The severity discipline still pays off: several German rules are warnings that strict receivers treat as errors.