E-Invoicing & ZUGFeRD: The Practical Guide
XRechnung or ZUGFeRD, EN 16931, validation and archiving — what actually matters technically once invoices have to be machine-readable.
Harald Schwankl
Dipl.-Ing., Fullstack Developer & AI Specialist
On this page
A PDF is not an e-invoice
The most common misconception is already in the word. Many companies have been emailing PDF invoices for years and consider themselves "electronic". The law disagrees.
An e-invoice is an invoice in a structured electronic format that allows automatic, electronic processing. What matters is not how the invoice travels, but whether a machine can read its fields without guessing.
A PDF is a picture of an invoice. A human reads the total; software has to guess it via text recognition, with all the errors that brings. Ending exactly that is the point of the change.
In practice: invoices become data, not documents. The readable document a human looks at is now a by-product. Adopt that perspective and the remaining requirements follow naturally.
The three misconceptions that cost money:
- "But we already send PDFs by email." — A PDF without embedded structured data does not meet the requirement.
- "This only affects large companies." — For receiving there is no turnover threshold. If you receive an invoice, you must be able to process it.
- "We'll just buy a tool." — A tool solves sending. Receiving, validation and archiving remain your job.
XRechnung or ZUGFeRD?
Two formats matter in Germany. Both satisfy the same European standard but differ significantly in handling.
XRechnung is pure XML. No layout, no presentation, just data. It is the standard when dealing with public authorities. The advantage is unambiguity: there is exactly one truth, and it lives in the XML. The drawback shows the moment a human needs to look at the invoice — that requires an additional presentation layer.
ZUGFeRD is a hybrid: a PDF/A-3 with the same XML embedded. The recipient sees a familiar document while their software reads the data from it. In practice this is the more common route because it does not force a break in existing workflows.
The profile is what matters. ZUGFeRD comes in several variants, and not every one is sufficient. Only from the profile matching EN 16931 onwards does the file contain all mandatory fields — in practice that means ZUGFeRD from version 2.0.1 in a profile at EN 16931 level or above. Lower profiles such as Minimum or Basic-WL are meant for internal use and do not qualify as an invoice.
Choosing:
- You invoice public authorities: XRechnung, because it is required there.
- Your customers are mixed and partly manual: ZUGFeRD from the appropriate profile.
- You are building a system that must receive both: you need both read paths anyway — the underlying data structure is identical.
That last point is easily missed. Thinking only about sending builds half a solution. Incoming invoices arrive in whatever format the sender chose, not yours.
Who must, and when
The transition is staggered. Knowing only the final deadline may leave you planning a year too late.
Receiving: since 1 January 2025, without exception. Every business in Germany must be able to receive and process e-invoices. No turnover threshold, no transition period. Small businesses under section 19 UStG are included — they must be able to receive even though they do not yet have to issue.
Sending: staggered by turnover.
- Until the end of 2026 every issuer may still use other invoice forms.
- From 1 January 2027 businesses with more than EUR 800,000 total turnover in the previous year must issue e-invoices in B2B.
- From 1 January 2028 the obligation applies to all businesses, regardless of turnover.
The threshold refers to the previous calendar year. Cross EUR 800,000 in 2026 and you are obliged from January 2027 — so the preparation happens in 2026, not 2027.
The practical fallacy: "We have until 2028." That holds only for sending, and only below the threshold. Receiving has been mandatory since the start of 2025, and that is precisely the part that does not work without your own preparation.
For business with public authorities the XRechnung obligation has applied considerably longer — anyone supplying there already knows the topic.
EN 16931: Where it breaks in practice
The European standard EN 16931 defines which fields an invoice must contain and in what form. It is the shared basis of both formats. Violate it and you produce a file that formally is not a valid invoice.
Migrations rarely fail on exotic fields. It is almost always the same places:
- Routing identifier — Mandatory when dealing with public authorities and it has to come from the customer. Without it the invoice is rejected even though the content is perfect.
- Tax categories per line — The tax rate belongs to each line, not to the total. Systems that internally know only one rate break here.
- Rounding differences — The standard requires sums to add up. One cent between the sum of lines and the invoice total is not cosmetic, it is an error.
- Structured payment terms — "Payable within 14 days" as free text is not enough where a due date is required.
- Units from a code list — "pcs." is not a valid unit. There is a fixed list, and only that counts.
Rounding deserves special attention because it appears in almost every grown system. If your system keeps net prices with four decimals and displays two, the sum of rounded lines does not necessarily equal the rounded total. On paper nobody notices. A validator reports it immediately.
The fix is not to ignore the validator but to adjust the calculation: line amounts are stored rounded, and the total is built from the rounded values. That is a change to business logic, not to the export.
Validate, don't hope
The most important advice from practice: build validation in before you send the first invoice, not afterwards.
An official validation toolset exists for XRechnung and ZUGFeRD that checks both schema conformity and business rules. That is exactly what runs in our demo at /ai-demo/e-invoice: upload a file, read the report.
Two error classes must be distinguished, and confusing them wastes a lot of time:
- Schema errors mean the file is structurally broken. A mandatory field is missing, an order is wrong, a data type does not fit. That is a technical problem in the export.
- Business rule violations mean the structure is fine but the content is inconsistent. Sums do not add up, a tax category contradicts the stated rate, a conditionally mandatory field is missing. That is a problem in business logic.
The second class is the unpleasant one. It cannot be solved with better XML; it requires changes where the invoice originates.
What belongs in a pipeline:
- Generate the invoice
- Validate against the schema
- Validate against the business rules
- Send only after both pass
- Archive the validation report alongside
Step five is the one people skip and the most valuable one. When somebody asks months later whether an invoice was correct, the report from back then is the answer.
Implementation in Python
Getting started is smaller than most fear. The common tasks — reading ZUGFeRD XML out of a PDF and producing conformant XML — need few building blocks.
Extracting XML from a ZUGFeRD PDF. The data sits as an attachment in the PDF/A-3. The filename is standardised, but it pays to search tolerantly:
from pypdf import PdfReader
ZUGFERD_NAMES = {"factur-x.xml", "zugferd-invoice.xml", "xrechnung.xml"}
def extract_invoice_xml(path: str) -> bytes | None:
"""Pull the embedded invoice out of a ZUGFeRD PDF."""
reader = PdfReader(path)
for name, contents in reader.attachments.items():
if name.lower() in ZUGFERD_NAMES and contents:
return contents[0]
return NoneIf the function returns None, it is an ordinary PDF — so not an e-invoice. This is exactly where incoming mail decides whether a document flows on automatically or lands on the "check manually" pile.
Getting the arithmetic right. The most common mistake in Python implementations is using floating point for money. Use Decimal and round deliberately:
from decimal import Decimal, ROUND_HALF_UP
def to_cents(amount: Decimal) -> Decimal:
return amount.quantize(Decimal("0.01"), rounding=ROUND_HALF_UP)
def sum_lines(lines: list[dict]) -> Decimal:
"""Sum of the ROUNDED line amounts — not the other way round.
Otherwise the total drifts by cents and the validator reports a
violation of the calculation rules.
"""
return sum(
(to_cents(Decimal(str(l["qty"])) * Decimal(str(l["unit_price"])))
for l in lines),
start=Decimal("0.00"),
)The comment in the code is the actual content of this section. Round first, then sum — never the reverse.
Wiring validation into the flow. The validator is a standalone program. It pays to run it as a service and make validation a fixed step rather than leaving it to developers:
async def release_invoice(xml: bytes) -> tuple[bool, str]:
"""Validate first, send second. Never the other way round."""
report = await validator.check(xml)
if not report.is_conformant:
# Do not send, but do not silently drop either:
# the report says which rule was violated.
return False, report.as_plain_text()
await archive.store(xml, report)
return True, "conformant"The underrated part: receiving
Most attention goes to sending; most work comes from receiving. The reason is simple: when sending, you choose the format. When receiving, the sender does.
An intake process therefore has to cope with everything that arrives:
- ZUGFeRD PDFs with embedded XML in various profiles
- plain XRechnung XML as an attachment
- an ordinary PDF from a supplier who has not migrated
- a file that claims to be conformant and is not
The last case is the interesting one. When a supplier sends a broken e-invoice you have a problem you did not cause but must solve if you want to keep the input VAT deduction.
A dependable intake process looks like this:
- Detect the attachment and determine its type
- Extract structured data if present
- Validate and record the result
- On errors: do not silently post the document, route it for clarification
- Report the result back to the supplier instead of repairing it internally
Point five is an organisational decision, not a technical one. Whoever quietly fixes broken incoming invoices permanently takes on somebody else's work — and loses the evidence of what the document originally looked like.
This is where automation pays off fastest: validation takes seconds and the reply to the supplier can be generated from a template. What remains is a human decision — and that is where it should stay.
Archiving & input VAT
Two points that are technically unspectacular and regularly expensive.
You archive the original. Not the printout, not the rendering, not a PDF you generated from the data. With ZUGFeRD the original is the PDF with the embedded XML; with XRechnung it is the XML. Archiving only the readable version means you have not retained the invoice in the required sense.
The retention period for accounting documents and invoices is eight years (section 147(3) AO, section 14b(1) UStG; shortened from ten to eight by the Fourth Bureaucracy Relief Act). That is long enough to influence technical decisions: a file format, a storage location and an access path have to stay stable for eight years — longer than most software products you use today. That argues for plain, open storage and against proprietary archive formats.
No conformant invoice, no input VAT deduction. That is the real economic lever here. A formally broken incoming invoice can cost the deduction retroactively during an audit. Which is why the validation report belongs in the archive: it proves the invoice was conformant at the time of receipt.
What belongs in storage:
- the original file, unmodified
- the validation report with a timestamp
- the extracted key data for searching
- a record of who released the document and when
The last point costs almost nothing and later answers the most uncomfortable question of all: who actually checked this?
Migration path
The migration can be ordered so that value arrives early and pain arrives late. Reverse the order and you do the work twice.
- Receiving first. It is the obligation without a threshold and the part that does not work without you. Only once incoming invoices are read and validated reliably is the rest worth doing.
- Validation as a service. Before anything is generated, it must be checkable. Otherwise you develop blind.
- Straighten out the arithmetic. Rounding, tax categories per line, units from the code list. This is the actual work and it touches your business logic, not the export.
- Generation. Only now, with working validation behind you, does the outgoing format take shape.
- Archiving. Original plus validation report, in storage that outlives the software.
The most common mistake is starting with step four because it is the most visible. The result is invoices that look good and fail validation.
If you want to know where you stand today, the fastest test is the most honest one: take a real invoice from your system and have it validated. It takes a minute and says more than any self-assessment.
Check your invoices for conformity
Upload a real invoice to our demo and see within seconds whether it passes the official validator. For migrating your accounting system, let's talk.