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
}'
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
}
Fields worth knowing:
-
buyerReferenceis the Leitweg-ID on invoices to German public authorities, for example04011000-12345-03in the UBL test file. -
iban,bicandpaymentMeansCodecome from the payment instructions. Code58means SEPA credit transfer. -
validationWarningslists 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)
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:
- Email Trigger (IMAP) on your invoice mailbox, or a Google Drive / Dropbox trigger on an "Invoices" folder.
- Upload the attachment somewhere with a temporary link, for example a pre-signed S3 URL or a Drive file with a sharing link.
-
HTTP Request node:
POSTto the endpoint above with{"fileUrls": ["{{ $json.url }}"]}and your Apify token as a Header Auth credential (Authorization: Bearer …). -
IF node on
status:-
okandvalidationWarningCount= 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.
-
- Optional: a second IF node that flags invoices whose
payableAmountis 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.xmland 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)