DEV Community

PatrickPi1312
PatrickPi1312

Posted on • Originally published at einvoice.installateur1210.at

EU e-invoicing for developers: XRechnung vs ZUGFeRD vs ebInterface vs Peppol

If you build accounting, ERP, shop or billing software and sell into Europe, e-invoicing has probably landed on your roadmap whether you wanted it or not. Germany's B2B mandate, Austria's e-Rechnung, and the EU-wide Peppol push all point the same way: structured e-invoices are becoming the default, and PDFs-by-email are on the way out.

The confusing part isn't the concept. It's that "e-invoice" means four or five different things depending on the country and the day. This post is the map I wish I'd had.

What "e-invoice" actually means

An e-invoice is not a PDF. A PDF is a picture of an invoice for humans. An e-invoice is structured data a machine can read without OCR or guesswork. That distinction is the whole point of the regulation: automated processing, fewer errors, less fraud.

The good news: underneath all the acronyms there is one shared model — the European standard EN 16931. It defines the semantics: what fields an invoice must contain (seller, buyer, invoice number, dates, line items, VAT breakdown, totals) and business rules between them (e.g. grand total = net + tax). Everything below is a flavour of the same core.

The two syntaxes

EN 16931 can be expressed in two XML syntaxes:

  • UBL 2.1 (OASIS Universal Business Language) — the more common one, used by Peppol and XRechnung.
  • UN/CEFACT CII (Cross Industry Invoice) — used inside ZUGFeRD/Factur-X.

Same information, different element names. In UBL the invoice number is cbc:ID; in CII it's ram:ID inside rsm:ExchangedDocument. If you parse by local element name and map to a neutral model, you can support both with one code path — more on that below.

The format zoo

Format Country Syntax Shape
XRechnung Germany 🇩🇪 UBL or CII Pure XML. A German CIUS (national profile) of EN 16931, maintained by KoSIT.
ZUGFeRD / Factur-X Germany 🇩🇪 / France 🇫🇷 CII Hybrid: a PDF/A-3 with the XML embedded inside the PDF. Human-readable and machine-readable in one file.
ebInterface Austria 🇦🇹 own XML schema Austria's national standard (maintained by AUSTRIAPRO), predates EN 16931. Still widely used in AT.
Peppol BIS Billing 3.0 EU-wide 🇪🇺 UBL Not a file format so much as an interoperability profile transported over the Peppol network. The de-facto EU lingua franca.

A few things that trip people up:

  • XRechnung is not a file type, it's a profile. An XRechnung document can be UBL or CII. Don't assume the syntax from the name.
  • ZUGFeRD is a PDF. The structured data lives in an embedded XML attachment (CII). To process it you first extract that XML, then treat it like any CII invoice. ZUGFeRD even has an XRECHNUNG profile so one file can satisfy both worlds.
  • ebInterface is its own thing. Austrian invoices you receive may well be ebInterface, not EN 16931. If your parser only knows UBL/CII, it will silently choke on AT invoices.
  • Peppol is the network, BIS Billing 3.0 is what flows over it. You send via an Access Point using the 4-corner model; the payload is EN 16931 UBL.

Why the deadlines matter (the short version)

  • Germany: since 1 Jan 2025, every B2B business must be able to receive EN 16931 e-invoices. Issuing becomes mandatory for larger companies from 2027 and for everyone from 2028. Accepted formats: XRechnung and ZUGFeRD (both EN 16931-compliant).
  • Austria: B2G has required structured invoices for years (ebInterface or Peppol via USP / e-rechnung.gv.at).
  • EU (ViDA): the "VAT in the Digital Age" package pushes structured e-invoicing and digital reporting across the bloc over the coming years.

Translation for us: a lot of software suddenly needs to read and write these formats, and most teams don't want to become EN 16931 experts to do it.

Doing it in code

The pragmatic approach for reading invoices is to normalise on parse: walk the XML by local element name, detect the syntax, and emit one neutral JSON shape regardless of whether the source was UBL, CII or ebInterface. Here's the idea in Python:

import xml.etree.ElementTree as ET

def local(tag):  # strip namespace
    return tag.split("}")[-1] if "}" in tag else tag

def detect(root):
    ns = root.tag.split("}")[0].lower()
    name = local(root.tag)
    if name == "Invoice" and "ebinterface" in ns: return "ebInterface (AT)"
    if name == "Invoice": return "UBL (XRechnung/Peppol)"
    if name == "CrossIndustryInvoice": return "CII (ZUGFeRD)"
    return "unknown"
Enter fullscreen mode Exit fullscreen mode

From there you collect the fields you care about across the syntaxes (invoice number, dates, seller/buyer, VAT, totals, lines), validate the EN 16931 required fields, and check the arithmetic. Generating is the reverse: take a neutral JSON object and emit valid UBL with the right CustomizationID/ProfileID for your target profile (Peppol for EU/AT, XRechnung for DE).

If you'd rather not build and maintain all of that, I put the whole thing behind a small API while solving it for my own trades business in Vienna. Free demo key demo, EU-hosted, no data stored:

# Generate a Peppol/EU invoice (works for Austria too)
curl -X POST "https://einvoice.installateur1210.at/v1/einvoice/generate?key=demo" \
  -H "Content-Type: application/json" \
  -d '{
    "invoice_number": "AT-2026-001",
    "issue_date": "2026-07-12",
    "currency": "EUR",
    "seller": "My Company GmbH", "seller_country": "AT", "seller_vat": "ATU12345678",
    "buyer": "Client Ltd", "buyer_country": "DE",
    "total_net": 100, "tax_amount": 20, "total_gross": 120, "vat_rate": 20,
    "line_items": [{"name": "Consulting", "quantity": 1, "line_amount": 100}]
  }'

# Validate an incoming XML against EN 16931 (UBL, CII or ebInterface)
curl -X POST "https://einvoice.installateur1210.at/v1/einvoice/validate?key=demo" \
  -H "Content-Type: application/xml" --data-binary @incoming-invoice.xml

# Parse any of the formats into clean JSON
curl -X POST "https://einvoice.installateur1210.at/v1/einvoice/parse?key=demo" \
  -H "Content-Type: application/xml" --data-binary @incoming-invoice.xml
Enter fullscreen mode Exit fullscreen mode

The generate endpoint takes a profile (peppol for EU/Austria, xrechnung for Germany, or plain en16931), so the same JSON can target different countries.

Gotchas worth knowing before you ship

  1. Don't infer syntax from the format name. Detect it from the XML root/namespace.
  2. ZUGFeRD = extract the embedded XML first. The PDF wrapper is not the data.
  3. Austria will send you ebInterface. Handle it or you'll drop real invoices.
  4. Validate the maths, not just the schema. A schema-valid invoice can still have net + tax ≠ gross; EN 16931 business rules catch that.
  5. Currency and country codes are required fields, and buyers increasingly need a valid VAT ID and buyer reference — don't leave them blank.

E-invoicing looks intimidating from the outside, but it's really one semantic model wearing several national costumes. Map the costumes once and the rest is plumbing.

Which country's format is giving you the most trouble? I'm actively adding coverage — happy to hear what to support next.

Top comments (0)