Eine gültige E-Rechnung (XRechnung oder ZUGFeRD/Factur-X) erzeugst du aus Python am robustesten über eine E-Rechnung API statt durch eigenes XML-Bauen: Du sendest strukturierte Rechnungsdaten per requests, der Dienst rechnet die Summen, vergibt die Nummer und rendert das EN-16931-konforme Dokument. Der Flow sind drei Calls — anlegen, ausstellen, XML oder PDF abholen — die sich mit requests in wenigen Zeilen abbilden lassen.

XRechnung ist reines XML nach EN 16931, ZUGFeRD/Factur-X ein PDF mit eingebettetem XML. In Python kannst du dieses XML selbst zusammenbauen — und Codelisten, Geschäftsregeln und Profilstände jährlich nachpflegen — oder die Erzeugung an eine E-Rechnung API delegieren. Dieser Beitrag zeigt den API-Weg mit requests.

Der Flow: drei Calls

  1. Anlegen — du schickst Kunde und Positionen, der Server rechnet Summen und USt.
  2. Ausstellen — die Rechnungsnummer wird atomar vergeben, das XML gerendert und gegen die EN-16931-Geschäftsregeln validiert.
  3. Abholen — das reine XRechnung-XML (CII) oder das hybride ZUGFeRD/Factur-X-PDF.

Base-URL und Test-Key (sk_test_…) kommen aus der Umgebung, damit sich der Code später ohne Änderung auf produktiv umstellen lässt — beides bekommst du mit dem Sandbox-Zugang.

Ein kleiner Client

Python
import os
import requests

BASE_URL = os.environ["FORTLAUF_API_URL"]
API_KEY = os.environ["FORTLAUF_API_KEY"]

session = requests.Session()
session.headers.update({"Authorization": f"Bearer {API_KEY}"})


def api(method: str, path: str, **kwargs) -> requests.Response:
    res = session.request(method, f"{BASE_URL}{path}", timeout=30, **kwargs)
    res.raise_for_status()
    return res

1) Rechnung anlegen

Beträge und Mengen gehen als DECIMAL-Strings über die Leitung — nie als float, sonst holt dich die Fließkomma-Rundung ein. Intern rechnest du am besten mit decimal.Decimal und serialisierst als String. Der Idempotency-Key macht einen wiederholten Anlege-Call zum Replay statt zum Duplikat.

Python
import uuid

draft = api(
    "POST",
    "/invoices",
    headers={"Idempotency-Key": str(uuid.uuid4())},
    json={
        "currency": "EUR",
        "documentTarget": "b2g",
        "buyer": {
            "name": "Gemeinde Görrenbach — Bauamt",
            "addressLine1": "Rathausgasse 2",
            "postalCode": "55483",
            "city": "Görrenbach",
            "countryCode": "DE",
        },
        "buyerElectronicAddress": "<LEITWEG-ID>",
        "buyerReference": "<LEITWEG-ID>",  # Leitweg-ID (BT-10), nur B2G
        "invoiceTypeCode": "380",
        "lineItems": [
            {
                "description": "Ingenieurleistung Straßenkataster, Los 2",
                "quantity": "42",
                "unitCode": "HUR",
                "unitPriceNet": "95.00",
                "taxRatePercent": "19",
            }
        ],
    },
)

invoice_id = draft.json()["id"]  # Status "draft", Summen serverseitig berechnet

2) Ausstellen

Python
api("PATCH", f"/invoices/{invoice_id}/finalize")
# → Status "issued", Nummer vergeben, XRechnung-XML (CII) gerendert + validiert

3) XML oder PDF abholen

Python
# reines XRechnung-XML (CII)
xml = api("GET", f"/invoices/{invoice_id}/xml").content
with open(f"rechnung-{invoice_id}.xml", "wb") as fh:
    fh.write(xml)

# oder das hybride ZUGFeRD/Factur-X-PDF (PDF/A-3 mit eingebettetem XML)
pdf = api("GET", f"/invoices/{invoice_id}/pdf").content
with open(f"rechnung-{invoice_id}.pdf", "wb") as fh:
    fh.write(pdf)

Wie das XML technisch ins PDF kommt, zeigt ZUGFeRD-PDF/A-3 erzeugen.

Worauf es in Python ankommt

  • Beträge als Strings, intern Decimal. Nie float für Geld — "1250.00", nicht 1250.0. Der Server rechnet in ganzzahligen Minor-Units und rundet einmal; das vermeidet die Summen-Inkonsistenzen, die der Validator sonst anmahnt (KoSIT-Validierung erklärt).
  • Idempotenz im Task-Worker. In Celery-, RQ- oder Cron-Jobs kann ein Task doppelt laufen; ein stabiler Idempotency-Key pro Rechnung hält den Nummernkreis lückenlos.
  • raise_for_status() nicht vergessen. Ein 422 aus der Validierung soll deinen Job hart stoppen, nicht still durchrutschen — der kleine Client oben erzwingt das.
  • Kein Format selbst pflegen. Codelisten, Geschäftsregeln und der Übergang zu XRechnung 4.0 liegen hinter der API; deine Python-Integration schickt „Kunde, Positionen, Steuersätze“ und bleibt stabil.

Der gezeigte Code ist illustrativ: Die vollständige Dokumentation steht dir in der API-Referenz zur Verfügung. Base-URL und Test-Key bekommst du mit dem Sandbox-Zugang im Entwickler-Bereich. Ob du die E-Rechnung selbst baust oder einbindest, ordnet E-Rechnung im eigenen SaaS: bauen oder einbinden? ein.

Selbst ausprobieren? Der Zugang ist kostenlos und ohne Karte — Produkt-Einstieg für Entwickler →

Oder den eigenen Abrechnungsfluss konkret durchsprechen: 30 Min, technisch, mit dem Gründer →

Häufige Fragen

Wie erstelle ich eine E-Rechnung mit Python?
Über drei HTTP-Calls gegen eine E-Rechnung API: POST zum Anlegen (der Server rechnet die Summen), PATCH zum Ausstellen (Nummer wird vergeben, XML gerendert und validiert) und GET zum Abholen des XRechnung-XML oder des ZUGFeRD-PDF. In Python genügt dafür die requests-Bibliothek — kein eigenes XML-Templating, keine Schematron-Validierung im eigenen Code.
Wie vermeide ich Rundungsfehler bei Beträgen in Python?
Schicke Beträge und Mengen als DECIMAL-Strings ("1250.00"), nie als float. Intern arbeitest du am besten mit decimal.Decimal statt mit Fließkomma; beim Serialisieren nach JSON gibst du den String weiter. Der Server rechnet die Summen und die USt in ganzzahligen Minor-Units und rundet einmal — so entsteht keine Differenz zwischen deinem Client und dem Beleg.
Wie mache ich die Erzeugung idempotent?
Setze beim Anlegen einen Idempotency-Key-Header mit einem pro Rechnung stabilen Wert (z. B. uuid.uuid4() oder deine eigene Beleg-ID). Ein wiederholter POST mit demselben Key ist dann ein Replay statt eines Duplikats — wichtig in Celery-Tasks, Cron-Jobs oder bei Webhook-Retries.