XRechnung & ZUGFeRD mit Python erstellen: Tutorial für EN 16931
Letzte Aktualisierung: August 2026 Voraussetzung: Python 3.9 oder neuer
Wer in Python eine KoSIT-konforme E-Rechnung nach EN 16931 erzeugen muss, landet schnell bei denselben drei Baustellen: XML-Bäume von Hand zusammensetzen (lxml, xml.etree), UN/ECE-Einheitencodes und BR-DE-Geschäftsregeln nachpflegen, und am Ende gegen die KoSIT-Schematron-Regeln validieren. Der Aufwand steckt weniger im ersten Wurf als in der Pflege: jedes Format-Update zieht Anpassungen nach sich.
Dieses Tutorial zeigt den anderen Weg — die Rechnungsdaten als Python-Dict an das offizielle SDK übergeben und XML und PDF fertig validiert zurückbekommen.
1. Installation
pip install factora
Das Paket liegt auf PyPI und braucht Python 3.9+. Einen Test-Key holst du dir in der Console; er beginnt mit fa_test_.
Wichtig für den Einstieg: Sandbox oder Produktivbetrieb entscheidet der Key, nicht ein Schalter im Code. Ein fa_test_-Key läuft durch dieselbe Pipeline wie ein fa_live_-Key — Validierung inklusive — speichert aber nichts und antwortet mit status: "sandbox".
2. Der erste Call
Eine Rechnung entsteht in genau einem POST auf /api/v1/invoices/atomic/. Das SDK kapselt den Endpoint in FactoraClient.create_atomic_invoice().
import base64
from pathlib import Path
from factora import FactoraAPIError, FactoraClient
client = FactoraClient(api_key="fa_test_DEIN_API_KEY")
invoice_payload = {
"api_mode": "atomic_single_post",
"invoice_header": {
"invoice_number": "INV-2026-001",
"invoice_date": "2026-08-06",
"payment_due_date": "2026-08-20",
"currency": "EUR",
"buyer_reference": "04011000-12345-34", # Leitweg-ID, BR-DE-15
"profile": "xrechnung", # oder "en16931"
},
"validation_totals": {
"net_amount": "1200.00",
"vat_amount": "228.00",
"gross_amount": "1428.00",
},
"seller_snapshot": {
"name": "TechSolutions GmbH",
"street": "Hauptstraße 42",
"zip": "20095",
"city": "Hamburg",
"country": "DE",
"vat_id": "DE123456789",
"contact": {
"name": "Max Mustermann",
"phone": "+49 40 123456",
"email": "rechnung@techsolutions.example",
},
},
"buyer": {
"name": "Behörde für Digitales",
"street": "Rathausmarkt 1",
"zip": "10117",
"city": "Berlin",
"country": "DE",
"contact": {"email": "einkauf@amt.example"},
},
"items": [
{
"description": "Software-Entwicklung",
"quantity": "10.000",
"unit": "HUR", # UN/ECE Rec. 20 — Stunde
"unit_price_net": "120.00",
"vat_rate": "19.00",
}
],
}
result = client.create_atomic_invoice(invoice_payload)
Drei Dinge daran sind erfahrungsgemäß die häufigsten Stolpersteine:
api_modeist Pflicht und hat genau einen gültigen Wert:atomic_single_post. Es ist ein Format-Diskriminator, kein Test-Schalter.profilegehört in deninvoice_header, nicht auf die oberste Ebene. Top-level gesendet wird es kommentarlos verworfen, und die Rechnung fällt still aufxrechnungzurück.validation_totalsist Pflicht. Die Summen kommen aus deinem System und werden gegen die Positionen gegengerechnet — sie sind eine Prüfsumme, keine Bequemlichkeit.
Beträge schreibst du als String. Das Feld ist serverseitig ein Decimal; Strings vermeiden Float-Rundungsfehler auf dem Weg durch JSON.
3. Was dabei passiert
Der Call macht drei Schritte in einem Durchlauf:
- Mapping der Daten auf das EN-16931-Modell inklusive der Geschäftsregeln des gewählten Profils.
- Erzeugung von XML und PDF.
- KoSIT-Validierung gegen die offiziellen Schematron-Regeln — vor der Finalisierung, nicht danach.
Der dritte Punkt ist der wichtigste: Ist der Validator nicht erreichbar, gibt es einen klaren Fehler statt einer ungeprüften Rechnung. Das ist bewusst so, denn eine Rechnung, die niemand geprüft hat, ist beim Empfänger genau so viel wert wie gar keine.
4. XRechnung und ZUGFeRD — du schaltest nicht um
Hier weicht die API von der Erwartung ab, die viele mitbringen: es gibt keinen Umschalter zwischen XRechnung und ZUGFeRD. Jede erfolgreiche Antwort enthält beides.
data = result["data"]
Path("rechnung.xml").write_bytes(base64.b64decode(data["xml_base64"]))
Path("rechnung.pdf").write_bytes(base64.b64decode(data["pdf_base64"]))
xml_base64— XRechnung 3.0.2pdf_base64— PDF/A-3B mit eingebettetem XML und vollständigen Factur-X-XMP-Metadaten
Ein Detail, das dich beim Auslesen sonst kostet: der Name der eingebetteten Datei hängt am Profil. Unter xrechnung heißt sie xrechnung.xml, unter en16931 heißt sie factur-x.xml — und im XRechnung-Profil ist der Name factur-x.xml ausdrücklich unzulässig. Wer den Anhang fest auf einen Namen sucht, findet ihn beim Profilwechsel nicht mehr. Lies stattdessen fx:DocumentFileName aus den XMP-Metadaten oder nimm einfach den ersten eingebetteten Anhang.
Du entscheidest also erst beim Versand, was der Empfänger bekommt: das reine XML für Behörden, die hybride PDF für Geschäftskunden, die zusätzlich etwas zum Ansehen wollen. Ein zweiter Call ist dafür nicht nötig.
Das Feld profile steuert etwas anderes — nämlich das semantische Regelwerk:
xrechnung— deutscher CIUS inklusive der BR-DE-Regeln (Default)en16931— europäisches Kernmodell ohne die deutschen Zusatzpflichten
Welches Profil tatsächlich gegriffen hat, steht immer in data["profile"]. Verlass dich darauf statt auf deine eigene Annahme — dann merkst du sofort, wenn ein Feld an der falschen Stelle stand.
5. Fehler lesen
Ein abgelehnter Request kommt als HTTP 400 mit strukturierten Fehlern zurück, nicht als halb angelegte Rechnung. Das SDK wirft dafür FactoraAPIError:
try:
result = client.create_atomic_invoice(invoice_payload)
except FactoraAPIError as exc:
print(f"HTTP {exc.status_code}: {exc}")
for error in (exc.response_data or {}).get("errors", []):
print(error["code"], error["severity"], error["message"])
else:
data = result["data"]
print(data["status"], data["profile"], data["total"])
Jeder Eintrag trägt code, severity und message; je nach Fehler zusätzlich field, rule, bt (der EN-16931-Geschäftsbegriff) und location.
Die Regelcodes sind der schnellste Einstieg in die Fehlersuche. Ein paar, die dir am Anfang häufig begegnen:
| Code | Bedeutung |
|---|---|
BR-DE-6 | Telefonnummer des Verkäufer-Kontakts fehlt (BT-42) |
BR-DE-7 | E-Mail-Adresse des Verkäufer-Kontakts fehlt (BT-43) |
BR-DE-15 | Leitweg-ID fehlt (BT-10) |
BR-DE-23-a | Zahlungsart Überweisung ohne Überweisungsdaten |
Alle vier stammen aus dem deutschen CIUS und tauchen deshalb nur unter profile: "xrechnung" auf. Unter en16931 gelten sie nicht.
6. Wiederholte Aufrufe absichern
Bei Live-Keys ist ein Idempotency-Key Pflicht, und das aus gutem Grund: Läuft ein Request in einen Timeout, weißt du nicht, ob die Rechnung angelegt wurde. Mit demselben Key bekommst du das ursprüngliche Ergebnis zurück statt einer zweiten Rechnung.
import uuid
result = client.create_atomic_invoice(
invoice_payload,
idempotency_key=str(uuid.uuid4()),
)
Erzeuge den Key pro fachlichem Vorgang, nicht pro HTTP-Versuch — sonst hebelst du den Schutz beim Retry genau dann aus, wenn du ihn brauchst.
Ressourcen
- SDK auf PyPI: pypi.org/project/factora
- Quellcode: github.com/factora-software/factora-python
- API-Referenz: console.factora.software/docs
Der Sandbox-Zugang ist kostenlos; abgerechnet wird erst der Live-Betrieb.
Häufig gestellte Fragen
Brauche ich einen kostenpflichtigen Zugang, um das Tutorial nachzuvollziehen?
Nein. Der Sandbox-Zugang ist kostenlos. Ein Key mit dem Präfix fa_test_ läuft durch dieselbe Pipeline inklusive KoSIT-Validierung, speichert aber nichts und antwortet mit dem Status "sandbox".
Wie schalte ich zwischen XRechnung und ZUGFeRD um?
Gar nicht — jede erfolgreiche Antwort enthält beides. Unter xml_base64 liegt die XRechnung 3.0.2, unter pdf_base64 ein PDF/A-3B mit eingebettetem XML und Factur-X-XMP-Metadaten. Der Name der eingebetteten Datei hängt am Profil: xrechnung.xml unter xrechnung, factur-x.xml unter en16931. Du entscheidest erst beim Versand, was der Empfänger bekommt.
Wozu dient das Feld profile dann?
Es wählt das semantische Regelwerk: xrechnung ist der deutsche CIUS inklusive der BR-DE-Regeln und der Default, en16931 das europäische Kernmodell ohne die deutschen Zusatzpflichten. Das Feld gehört in den invoice_header, nicht auf die oberste Ebene.
Warum muss ich validation_totals mitschicken, wenn die API die Summen selbst rechnen kann?
Weil es eine Prüfsumme ist. Die Summen aus deinem System werden gegen die Positionen gegengerechnet; weichen sie ab, bekommst du einen Fehler statt einer Rechnung, die rechnerisch etwas anderes sagt als deine Buchhaltung.
Wann brauche ich einen Idempotency-Key?
Bei Live-Keys ist er Pflicht. Er sichert den Fall ab, dass ein Request in einen Timeout läuft und du nicht weißt, ob die Rechnung angelegt wurde: Mit demselben Key kommt das ursprüngliche Ergebnis zurück statt einer zweiten Rechnung. Erzeuge ihn pro fachlichem Vorgang, nicht pro HTTP-Versuch.
Welche Python-Version brauche ich?
Python 3.9 oder neuer. Das SDK wird über pip install factora aus PyPI installiert.