Cross-posting checklist (nicht mitveröffentlichen): dev.to zuerst, 24h später Medium, Hashnode, Indie Hackers.
Auf allen Kopiencanonical_urlauf dieselbe URL setzen. Zwei Optionen — vorher entscheiden:
(a) Canonical auf die eigene Domain (empfohlen, wenn der Artikel auch im Blog auf invoice-api.xhub.io steht) — dann zählt die SEO-Kraft dort.
(b) Canonical auf die dev.to-Fassung, wenn es keine eigene Blog-Version gibt.
Aktuell im Frontmatter: Quickstart-Doku. Das ist nur richtig, wenn der Artikel als Ergänzung dieser Seite gelten soll — sonst auf (a) oder (b) ändern.
German e-invoicing in a few lines of code
If you build billing software, an ERP integration, or a marketplace that touches German customers, you have a deadline: 31 December 2026.
That's when the first transition period ends. Until then you may still send paper or a plain PDF in B2B — with the recipient's consent. From 1 January 2027 companies above €800,000 in prior-year revenue must issue structured e-invoices. Everyone below that threshold gets one more year: from 1 January 2028 it applies without exception.
(Receiving is a separate matter and already settled — every German company has had to be able to accept e-invoices since January 2025.)
Most teams estimate a quarter for this. Here's why they're wrong — and what the work actually looks like.
What the standard actually asks for
The relevant norm is EN 16931, the European semantic standard for electronic invoicing. In Germany it shows up in two flavours:
XRechnung is pure XML. No visual layout — it's built for machines. It's the standard in public-sector business (B2G), routed via a Leitweg-ID.
ZUGFeRD is a hybrid: a normal PDF/A-3 with the same structured data embedded as XML. Humans read the PDF, machines parse the XML. From version 2.x its profiles are aligned with France's Factur-X — the two are close cousins rather than unrelated standards, which matters once you cross the border.
Both are valid e-invoices. Both satisfy EN 16931. The difference is whether your recipient wants something to look at.
Why teams over-estimate this
The naive plan looks like this:
- Research the EN 16931 semantic model
- Map your domain objects onto it
- Generate valid XML
- Build a validation pipeline against KoSIT schemas
- Handle edge cases — credit notes, reverse charge, intra-community supply, rounding
- Maintain all of it as the specification moves
Steps 1 and 4 are where the quarter goes. The semantic model has hundreds of business terms with conditional cardinalities. KoSIT validation isn't a schema check — it's Schematron rules on top of the XSD, and a document can be schema-valid but rule-invalid.
None of that is interesting work. It's also not differentiating work: your competitors' XRechnung is byte-for-byte as compliant as yours.
The version that isn't a project
Post JSON, get back a validated document:
curl -X POST https://service.invoice-api.xhub.io/api/v1/invoice/de/xrechnung/generate \
-H "Authorization: Bearer sk_test_abc123..." \
-H "Content-Type: application/json" \
-d '{
"invoice": {
"invoiceNumber": "RE-2026-001",
"type": "invoice",
"issueDate": "2026-04-10",
"dueDate": "2026-05-10",
"currency": "EUR",
"seller": {
"name": "Meine Firma GmbH", "vatId": "DE123456789",
"street": "Musterstraße 1", "city": "Berlin",
"postalCode": "10115", "countryCode": "DE"
},
"buyer": {
"name": "Kunde AG", "vatId": "DE987654321",
"street": "Kundenweg 42", "city": "München",
"postalCode": "80331", "countryCode": "DE"
},
"countrySpecific": { "buyerReference": "BUYER-REF-001" },
"items": [{
"position": 1, "description": "Beratungsleistung",
"quantity": 10, "unit": "HUR", "unitPrice": 150.00,
"taxRate": 19, "netAmount": 1500.00,
"taxAmount": 285.00, "grossAmount": 1785.00
}],
"subtotal": 1500.00, "total": 1785.00,
"taxSummary": [{ "taxRate": 19, "netAmount": 1500.00, "taxAmount": 285.00 }],
"paymentTerms": { "dueDays": 30 }
}
}'
The same call in TypeScript — invoice is the object from the payload above:
const response = await fetch(
'https://service.invoice-api.xhub.io/api/v1/invoice/de/xrechnung/generate',
{
method: 'POST',
headers: {
'Authorization': 'Bearer sk_test_abc123...',
'Content-Type': 'application/json'
},
body: JSON.stringify({ invoice })
}
);
if (!response.ok) {
throw new Error(`API returned ${response.status}`);
}
const result = await response.json();
if (!result.success || result.errors.length > 0) {
throw new Error(`Invalid invoice: ${JSON.stringify(result.errors)}`);
}
// Decode base64 → UTF-8. Don't use atob() here: it returns a Latin-1
// binary string and will mangle "Musterstraße" / "München".
const xml = new TextDecoder().decode(
Uint8Array.from(atob(result.data), c => c.charCodeAt(0))
);
// In Node:
// const xml = Buffer.from(result.data, 'base64').toString('utf8');
And in Python:
import base64
import requests
response = requests.post(
'https://service.invoice-api.xhub.io/api/v1/invoice/de/xrechnung/generate',
headers={'Authorization': 'Bearer sk_test_abc123...'},
json={'invoice': invoice} # requests sets Content-Type automatically
)
response.raise_for_status()
result = response.json()
if not result['success'] or result['errors']:
raise ValueError(f"Invalid invoice: {result['errors']}")
xml = base64.b64decode(result['data']).decode('utf-8')
Reading the response
{
"success": true,
"format": "xrechnung",
"filename": "invoice-2026-001.xml",
"mimeType": "application/xml",
"hash": "sha256:abc123...",
"data": "PD94bWwgdmVyc2lvbj0iMS4wIj8+...",
"errors": [],
"warnings": []
}
Three fields matter in practice:
-
data— the document itself, base64-encoded. XML for XRechnung, PDF bytes for ZUGFeRD. -
hash— SHA-256 of the document. Store it; it's what makes your archive defensible later. -
errors/warnings— validation results in the same response. You know at request time whether the invoice holds up, not when your customer's accounts-payable system rejects it three weeks later.
Treat a non-empty errors array as a hard failure in your pipeline. Warnings are usually about optional business terms and are safe to log.
Switching format is switching endpoint
The same payload produces ZUGFeRD instead of XRechnung by changing the path segment. Today that covers XRechnung, ZUGFeRD and plain PDF.
The reason this matters is what comes next. Sell across the EU and each market wants its own thing:
- France: Factur-X
- Italy: FatturaPA, submitted through the SdI system
- Spain: Facturae
- Belgium, the Netherlands, Bulgaria, Romania: UBL
- Austria: ebInterface · Czechia: ISDOC · Hungary: NAV
Those seven are on the roadmap rather than live today — check the formats endpoint for the current state before you plan around one.
The architectural point stands regardless of vendor: if your invoice model is coupled to one output format, every new market is a new project. If it isn't, it's a config change. Design for that even if you only ship Germany today.
Things worth getting right regardless of tooling
Whatever you build on, these bite eventually:
Invoice numbering. German §14 UStG requires a sequential number that is assigned only once. If two orders complete simultaneously and you derive the number from a COUNT(*), you will eventually issue a duplicate — which is the part that actually breaks. Use a dedicated table with a transactional counter, not your primary key and not a timestamp.
Credit notes. Every refund needs its own document with its own number from the same sequence. Retrofitting this is unpleasant — model it from the start.
Archiving. Germany's GoBD rules expect the invoice to remain readable and unaltered for years. Store the exact bytes you sent plus the hash, not a re-rendered version.
Don't block checkout. Generating an invoice means an external call. Push it to a queue or worker. Nobody should wait on your compliance layer to complete a purchase.
Try it without signing up
There's a playground that runs without an account, and test keys (sk_test_...) are free — test-mode calls don't cost anything, so you can wire up your integration and run it in CI before deciding anything.
If you're on WooCommerce, there's also a GPLv2 plugin on wordpress.org that does this at the order level, no code required.
The deadline is real, and it's closer than most roadmaps assume. The implementation doesn't have to eat a quarter.
Built by BeeBack UG in Frankfurt — invoice-api.xhub.io.
Top comments (0)