Your own PDF design as ZUGFeRD/Factur-X

How a designed invoice PDF becomes a hybrid e-invoice: PDF/A-3, embedded XML, XMP — and where real-world implementations go wrong.

Raw

Many teams have a carefully designed invoice PDF — logo, brand typography, a footer with payment terms — and now face the task of turning it into an e-invoice without giving up the design. That is exactly what ZUGFeRD and Factur-X were built for: hybrid invoices that look to humans like they always did, while carrying a structured XML for machines. The path is well specified, yet in practice it regularly fails on a handful of details.

Anatomy: PDF/A-3 plus embedded CII XML

A ZUGFeRD/Factur-X invoice consists of three layers:

The container is a PDF/A-3. Not PDF 1.4, not PDF 1.7, but the archival standard ISO 19005-3 — the only PDF/A flavour that permits arbitrary file attachments. PDF/A-3 also forbids encryption, JavaScript and external dependencies such as non-embedded fonts.

The embedded XML is a UN/CEFACT CII file with a normative filename that depends on standard and version: factur-x.xml for ZUGFeRD 2.1+ and Factur-X (xrechnung.xml in the XRECHNUNG profile), zugferd-invoice.xml for ZUGFeRD 2.0, ZUGFeRD-invoice.xml for the legacy ZUGFeRD 1.0. The attachment needs an AFRelationship entry — typically Alternative for the full profiles (the XML is an alternative representation of the same invoice), but Data or Source for the reduced MINIMUM and BASIC WL profiles.

The PDF’s XMP metadata declare, via the Factur-X extension schema, what is embedded: DocumentType (INVOICE), DocumentFileName (exactly the attachment name), Version and ConformanceLevel — that is, the profile, from MINIMUM to EXTENDED. Receiving systems read this declaration before they ever touch the XML.

The classic pitfalls

Almost every broken hybrid invoice falls into one of these patterns:

Tooling paths

Nobody needs to reinvent the wheel. In the Java world, mustangproject and PDFBox are established ways to convert an existing PDF to PDF/A-3 and attach the XML with AFRelationship and XMP; comparable libraries exist for .NET and Python. The principle is always the same: render the designed PDF as before, then in a second step convert the container, embed the XML, write the XMP.

One priority should be clear throughout: the machine-readable part is what recipients actually process — the design is only for the eye. The care belongs in the XML: correct totals arithmetic, complete mandatory fields, the right profile. A beautiful PDF with broken XML is not an e-invoice; a plain PDF with clean XML is.

Verification workflow: cross-check the result

Before anything ships, the generated PDF belongs in the Billhorse validator: it extracts the embedded XML straight from the PDF container and validates it against EN 16931 and the profile-specific rules — entirely in the browser, no upload. If no invoice XML is found inside the PDF, the result is BH-PDF-01 — the most common symptom of a generation path that simply forgot the embedding step. If the PDF is encrypted, that is reported explicitly as BH-PDF-02 rather than as a vague parse error. Test with real edge cases: an invoice with a discount, a credit note, an invoice with multiple VAT rates — that is where mapping bugs surface that the happy-path example never triggers.

Mandatory: visible part and XML must match

One point is not a matter of style but an obligation: the visible PDF and the embedded XML must describe the same invoice. Legally, the structured part is authoritative — if the visual rendering deviates, the XML governs. Divergence almost always arises when PDF and XML are produced by separate code paths: the PDF rounds differently, the XML doesn’t know about the discount, a line item is missing. The robust architecture renders both representations from the same data source — one invoice model, two outputs. Build it that way from the start and consistency never needs manual checking again.

Double-check the result

The Billhorse validator checks XRechnung, ZUGFeRD and Factur-X right in your browser — your file is never uploaded.

Open the validator

← All guides · Last updated: 2026-07-04