DEV Community

facturata
facturata

Posted on • Originally published at facturata.com

Your Factur-X passes XSD and still gets rejected. Here is why.

If you are adding Factur-X or ZUGFeRD support to an app before the French 2026 e-invoicing deadline, you will meet this bug that is not a bug: a file that is perfectly well formed, opens fine in a PDF reader, passes the XSD, and still gets refused by the receiving platform.

The format is the easy part. The EN 16931 business rules are what actually reject invoices.

Format validation is not conformance

A Factur-X file is a PDF/A-3 with a CII XML (the EN 16931 semantic model) embedded inside it. ZUGFeRD 2.x in Germany is the same file. Two layers decide whether it is accepted:

  1. Structure: the XSD. It checks that elements exist and are well typed. Most tools get this right.
  2. Business rules: a Schematron layer of roughly 200 rules (BR-, BR-CO-, BR-S-*), plus national rules on top. This is where valid-looking files die.

The XSD says "this is a syntactically correct CII document." It says nothing about whether your totals add up.

The rules that bite most often

These are the ones I see fail again and again, with the reason:

  • BR-CO-15: the grand total with VAT must equal the total without VAT plus the total VAT. The classic cause is rounding line by line and then summing in the wrong order.
  • BR-CO-10 and BR-CO-13: the sum of line net amounts must equal the document line total. One rounded line, one mismatch.
  • BR-S-08: for each standard-rated VAT breakdown, the taxable base must equal the sum of the line nets at that rate. Multi-rate invoices break this constantly.
  • BR-CO-17: for a VAT breakdown, the tax amount must equal the taxable amount times the rate divided by 100, rounded to two decimals. A half-cent drift is enough.
  • BR-S-05 and its siblings: use a 0 percent or exempt rate and forget the exemption reason, and the file is rejected.

A concrete rounding trap. Take three lines at 19.99 with 20 percent VAT:

round each line first:  net 19.99, VAT 4.00  ->  3 lines: net 59.97, VAT 12.00
sum then tax:           net 59.97 * 0.20 = 11.994  ->  11.99
Enter fullscreen mode Exit fullscreen mode

Round in the wrong place and BR-CO-17 or BR-CO-15 fails, even though every number looks reasonable.

Validate before you send

Do not let the receiver be your validator. Options, open-source first:

  • The KoSIT validator, the reference validation used in Germany, runs the official Schematron and returns the exact failing rules.
  • Mustangproject (Java, Apache-2.0) can generate and validate Factur-X or ZUGFeRD.
  • Whatever validator you use, pin the rule-set version. The EN 16931 artefacts revise roughly every six months. Valid last quarter is not valid today, and the exchange carries no version handshake, so the receiver's rule set quietly sets your real deadline.

During the transition, validate outgoing files against both the current and the previous rule set, and treat "valid at sender, rejected at receiver" as a metric to watch, not a one-off surprise.

A minimal check in code

If you already produce a Factur-X PDF, extract the embedded XML and run it through a Schematron validator. A generate-and-validate API call, JSON in and a validated Factur-X out, looks like this:

curl -s https://api.facturata.com/v1/invoices \
  -H "Content-Type: application/json" \
  -H "X-API-Key: $KEY" \
  -d @invoice.json | jq -r '.facturx_pdf_base64' | base64 -d > invoice.pdf
Enter fullscreen mode Exit fullscreen mode
import base64, requests

r = requests.post(
    "https://api.facturata.com/v1/invoices",
    headers={"X-API-Key": KEY},
    json=invoice,  # your invoice as JSON
)
data = r.json()
open("invoice.pdf", "wb").write(base64.b64decode(data["facturx_pdf_base64"]))
# totals and VAT are recomputed and validated server-side before you get the file
Enter fullscreen mode Exit fullscreen mode

The endpoint is not the point. The point is that totals and VAT should be recomputed and passed through the XSD and the full Schematron before a file ever leaves your system.

Test the ugly cases early

One clean invoice passes almost anywhere. The rejections live in the cases teams test last:

  • credit notes and corrections
  • prepayments and deposits, then the final invoice that nets them out
  • multi-rate VAT on one document
  • reverse charge and self-billing
  • exemptions with the correct reason code

Full disclosure: I build Facturata, a free EN 16931 validator and a generate API, so these rules are what I work on all day. Whatever stack you pick, hosted or open-source, test those five cases before September, not after the first rejection.

Top comments (0)