DEV Community

Fast Data
Fast Data

Posted on

How to parse XRechnung and ZUGFeRD e-invoices to JSON with Python or n8n

Since 1 January 2025, every business in Germany has to be able to receive electronic invoices in B2B. Being able to send them becomes mandatory step by step in 2027 and 2028. In practice this means your inbox now contains three kinds of invoice files that look nothing alike:

  • XRechnung in UBL syntax: an XML file in the OASIS Universal Business Language schema.
  • XRechnung in CII syntax: an XML file in the UN/CEFACT Cross Industry Invoice schema, with different element names for the same data.
  • ZUGFeRD / Factur-X: a normal-looking PDF with a CII XML file embedded inside it.

All three carry the same EN 16931 data (seller, buyer, totals, VAT and lines), but parsing them means writing two XML mappings plus PDF attachment extraction. This guide shows how to get one flat JSON format for all of them.

Disclosure: I built the tool used below, the XRechnung & ZUGFeRD E-Invoice Parser on Apify. The last section shows what doing it yourself involves.

Step 1: parse three different invoice files with one call

I used three public test files: two from the official XRechnung test suite of KoSIT (the German coordination office for IT standards), one UBL and one CII, plus a ZUGFeRD PDF from the open-source Mustang project.

curl -X POST "https://api.apify.com/v2/acts/fast_data_asap~xrechnung-zugferd-einvoice-parser/run-sync-get-dataset-items?token=YOUR_APIFY_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "fileUrls": [
      "https://raw.githubusercontent.com/itplr-kosit/xrechnung-testsuite/master/src/test/business-cases/standard/01.01a-INVOICE_ubl.xml",
      "https://raw.githubusercontent.com/itplr-kosit/xrechnung-testsuite/master/src/test/business-cases/standard/02.01a-INVOICE_uncefact.xml",
      "https://raw.githubusercontent.com/ZUGFeRD/mustangproject/master/library/src/test/resources/EN16931_Einfach.pdf"
    ],
    "includeLineItems": true
  }'
Enter fullscreen mode Exit fullscreen mode

I ran exactly this on 8 November 2026. It took 4.8 seconds, and all three came back with status: "ok":

File Detected format Invoice no. Net VAT Gross
01.01a-INVOICE_ubl.xml XRechnung-UBL 123456XX 314.86 22.04 336.90
02.01a-INVOICE_uncefact.xml XRechnung-CII 1234567 10,781.25 2,048.44 12,829.69
EN16931_Einfach.pdf ZUGFeRD/Factur-X (PDF) 471102 473.00 56.87 529.87

You don't tell it which format a file is. It detects UBL, CII and PDF-with-XML on its own and reports the format, syntax and profile (XRECHNUNG, EN16931 and so on).

Step 2: what one row looks like

Every invoice becomes the same shape, whatever the source. Here is the ZUGFeRD PDF, shortened:

{
  "status": "ok",
  "format": "ZUGFeRD/Factur-X",
  "sourceType": "PDF",
  "pdfAttachmentName": "factur-x.xml",
  "profile": "EN16931",
  "invoiceNumber": "471102",
  "issueDate": "2024-11-15",
  "currency": "EUR",
  "seller": {
    "name": "Lieferant GmbH",
    "vatId": "DE123456789",
    "city": "München",
    "country": "DE"
  },
  "buyer": { "name": "Kunden AG Mitte", "city": "Frankfurt" },
  "paymentTerms": "Zahlbar innerhalb 30 Tagen netto bis 15.12.2024, 3% Skonto innerhalb 10 Tagen bis 25.11.2024",
  "taxBasis": 473,
  "taxTotal": 56.87,
  "payableAmount": 529.87,
  "vatBreakdown": [
    { "category": "S", "rate": 7, "taxable": 275, "tax": 19.25 },
    { "category": "S", "rate": 19, "taxable": 198, "tax": 37.62 }
  ],
  "lineItems": [
    { "name": "Trennblätter A4", "quantity": 20, "netPrice": 9.9, "lineTotal": 198, "vatRate": 19 },
    { "name": "Joghurt Banane", "quantity": 50, "netPrice": 5.5, "lineTotal": 275, "vatRate": 7 }
  ],
  "validationWarningCount": 0
}
Enter fullscreen mode Exit fullscreen mode

Fields worth knowing:

  • buyerReference is the Leitweg-ID on invoices to German public authorities, for example 04011000-12345-03 in the UBL test file.
  • iban, bic and paymentMeansCode come from the payment instructions. Code 58 means SEPA credit transfer.
  • validationWarnings lists problems such as missing required fields (for example a missing Leitweg-ID on an XRechnung) and totals that don't add up (line sum vs. net total, net + VAT vs. gross). It is a plausibility check, not the official KoSIT validator.

Step 3: book it in Python

Once every format looks the same, the accounting logic is short. This turns the parsed invoices into one booking line per VAT rate, which is the shape most accounting imports want:

import httpx

ENDPOINT = (
    "https://api.apify.com/v2/acts/"
    "fast_data_asap~xrechnung-zugferd-einvoice-parser/run-sync-get-dataset-items"
)

def parse_invoices(urls, token):
    r = httpx.post(
        ENDPOINT,
        params={"token": token},
        json={"fileUrls": urls, "includeLineItems": False},
        timeout=300,
    )
    r.raise_for_status()
    return r.json()

def booking_lines(invoices):
    for inv in invoices:
        if inv["status"] != "ok":
            print("needs manual check:", inv["fileUrl"], inv.get("error"))
            continue
        for vat in inv["vatBreakdown"]:
            yield {
                "supplier": inv["seller"]["name"],
                "supplierVatId": inv["seller"].get("vatId"),
                "invoiceNumber": inv["invoiceNumber"],
                "date": inv["issueDate"],
                "net": vat["taxable"],
                "vatRate": vat["rate"],
                "vat": vat["tax"],
            }

invoices = parse_invoices(["https://example.com/invoice.pdf"], "YOUR_APIFY_TOKEN")
for line in booking_lines(invoices):
    print(line)
Enter fullscreen mode Exit fullscreen mode

For the ZUGFeRD test PDF this prints two lines: 275.00 net at 7 % and 198.00 net at 19 %.

Step 4: automate the inbox in n8n or Make

A typical accounts-payable flow:

  1. Email Trigger (IMAP) on your invoice mailbox, or a Google Drive / Dropbox trigger on an "Invoices" folder.
  2. Upload the attachment somewhere with a temporary link, for example a pre-signed S3 URL or a Drive file with a sharing link.
  3. HTTP Request node: POST to the endpoint above with {"fileUrls": ["{{ $json.url }}"]} and your Apify token as a Header Auth credential (Authorization: Bearer …).
  4. IF node on status:
    • ok and validationWarningCount = 0: write the row to Google Sheets, your database or your accounting tool's API.
    • anything else: send it to a Slack channel or task list for manual review.
  5. Optional: a second IF node that flags invoices whose payableAmount is above an approval limit.

PDFs without an embedded XML (ordinary scanned invoices) come back as an error row. They are not e-invoices and still need OCR or manual entry, so route them to the manual branch too.

What it costs, and what doing it yourself involves

The parser charges $0.003 per successfully parsed invoice. Files that fail to download, PDFs without XML and non-invoice files are free, so a thousand invoices cost about $3.

If you would rather write it yourself, plan for:

  • two complete XPath mappings, UBL and CII, with their different namespaces and element names,
  • extracting the XML attachment from PDF/A-3 files (factur-x.xml, zugferd-invoice.xml, xrechnung.xml and older names),
  • profile detection, credit notes (type code 381) and invoices without line items,
  • numbers as strings with varying precision.

Doable, but it is a week of edge cases rather than an afternoon. The README has the full field list.

Test files: KoSIT XRechnung test suite and Mustang project (both public on GitHub), parsed on 8 November 2026. This article is about the technical side of e-invoices, not tax advice.

Top comments (0)