Rechnungen per API erstellen — Schritt für Schritt
Dieses Tutorial führt Sie durch den kompletten Rechnungsworkflow mit der docs101 REST API — vom Anlegen eines Kunden bis zum Download eines ZUGFeRD-konformen PDFs und dem Export nach DATEV. Am Ende haben Sie eine funktionierende Integration, die EU-konforme Rechnungen programmatisch erstellt.
Voraussetzungen
- Ein aktiver Pro-Plan mit API-Key (siehe Authentifizierung)
curloder Python 3.8+ mit derrequests-Bibliothek- Basis-URL:
https://docs101.com/api/v1
Python-Setup
Alle Python-Beispiele in diesem Tutorial verwenden ein gemeinsames Setup. Definieren Sie diese Variablen einmalig am Anfang Ihres Skripts:
import requests
API_KEY = "ak_live_YOUR_KEY_HERE"
BASE_URL = "https://docs101.com/api/v1"
HEADERS = {
"X-API-Key": API_KEY,
"Content-Type": "application/json",
}
Schritt 1: Template auswählen
Jede Rechnung benötigt ein Template, das das visuelle Layout definiert. Rufen Sie die verfügbaren Templates ab und merken Sie sich die id des gewünschten Templates.
curl -H "X-API-Key: ak_live_YOUR_KEY_HERE" \
https://docs101.com/api/v1/template/
response = requests.get(f"{BASE_URL}/template/", headers=HEADERS)
templates = response.json()
for tpl in templates:
print(f"ID: {tpl['id']} — {tpl['name']}")
Sie können ein Standard-Template pro Kunde setzen (über default_ftl_template_id) oder das Template beim Erstellen jeder Rechnung übergeben.
Schritt 2: Kunden anlegen
Erstellen Sie einen Kundendatensatz, an den Ihre Rechnungen adressiert werden.
curl -X POST https://docs101.com/api/v1/customer/ \
-H "X-API-Key: ak_live_YOUR_KEY_HERE" \
-H "Content-Type: application/json" \
-d '{
"organization_name": "Acme Corp",
"email": "billing@acme.com",
"contact_type": "B2B",
"vat_id": "DE123456789",
"language": "English"
}'
customer_data = {
"organization_name": "Acme Corp",
"email": "billing@acme.com",
"contact_type": "B2B",
"vat_id": "DE123456789",
"language": "English",
}
response = requests.post(f"{BASE_URL}/customer/", headers=HEADERS, json=customer_data)
result = response.json()
customer_id = result["id"]
print(f"Kunde erstellt: {customer_id}")
Erwartete Antwort (201):
{
"message": "Customer successfully created",
"id": 42
}
Kundenfelder
| Feld | Typ | Pflicht | Beschreibung |
|---|---|---|---|
organization_name | String | Ja (B2B) | Firmenname |
first_name / last_name | String | Ja (B2C) | Vor-/Nachname bei Privatpersonen |
email | String | Nein | E-Mail-Adresse |
contact_type | String | Nein | B2B (Standard) oder B2C |
vat_id | String | Nein | EU-Umsatzsteuer-Identifikationsnummer |
language | String | Nein | Rechnungssprache: English, German |
payment_term_id | Integer | Nein | Zahlungsziel-ID |
default_ftl_template_id | Integer | Nein | Standard-Template für diesen Kunden |
salutation | String | Nein | MR, MRS, MS, OTHER |
Das Feld salutation erwartet Großbuchstaben: MR, MRS, MS, OTHER. Kleinbuchstaben führen zu einem Validierungsfehler.
Schritt 3: Rechnungsadresse hinzufügen
Jede Rechnung benötigt eine Rechnungsadresse beim Kunden.
curl -X POST https://docs101.com/api/v1/customer/42/addresses \
-H "X-API-Key: ak_live_YOUR_KEY_HERE" \
-H "Content-Type: application/json" \
-d '{
"company": "Acme Corp",
"address_line_1": "Musterstraße 1",
"postal_code": "10115",
"city": "Berlin",
"country_code": "DE",
"is_default": true
}'
address_data = {
"company": "Acme Corp",
"address_line_1": "Musterstraße 1",
"postal_code": "10115",
"city": "Berlin",
"country_code": "DE",
"is_default": True,
}
response = requests.post(
f"{BASE_URL}/customer/{customer_id}/addresses",
headers=HEADERS,
json=address_data,
)
print(f"Adresse erstellt: {response.status_code}")
Adressfelder
| Feld | Typ | Pflicht | Beschreibung |
|---|---|---|---|
address_line_1 | String | Ja | Straße und Hausnummer |
city | String | Ja | Stadt |
country_code | String | Ja | ISO 3166-1 Alpha-2-Code (z.B. DE, AT, FR) |
company | String | Nein | Firmenname in der Adresse |
address_line_2 | String | Nein | Adresszusatz |
postal_code | String | Nein | Postleitzahl |
state_province | String | Nein | Bundesland / Region |
is_default | Boolean | Nein | Als Standardadresse setzen |
Schritt 4: Rechnung erstellen
Erstellen Sie eine Rechnungsentwurf, der mit Ihrem Kunden verknüpft ist.
curl -X POST https://docs101.com/api/v1/invoice/ \
-H "X-API-Key: ak_live_YOUR_KEY_HERE" \
-H "Content-Type: application/json" \
-d '{
"customer_id": 42,
"benefit_period_start": "2026-04-01",
"benefit_period_end": "2026-04-30",
"invoice_format": "ZUGFERD"
}'
invoice_data = {
"customer_id": customer_id,
"benefit_period_start": "2026-04-01",
"benefit_period_end": "2026-04-30",
"invoice_format": "ZUGFERD",
}
response = requests.post(f"{BASE_URL}/invoice/", headers=HEADERS, json=invoice_data)
result = response.json()
invoice_id = result["invoice_id"]
print(f"Rechnung erstellt: {invoice_id}")
Erwartete Antwort (201):
{
"message": "Invoice successfully created",
"invoice_id": 17
}
Rechnungsfelder
| Feld | Typ | Pflicht | Beschreibung |
|---|---|---|---|
customer_id | Integer | Ja | Kunden-ID aus Schritt 2 |
benefit_period_start | Date | Nein | Leistungszeitraum Start (YYYY-MM-DD) |
benefit_period_end | Date | Nein | Leistungszeitraum Ende |
invoice_format | String | Nein | PDF, ZUGFERD, FACTURAPA, PEPPOL, FACTURAE |
invoice_number | String | Nein | Wird automatisch generiert, wenn leer |
meta | String | Nein | Freitext-Metadaten |
Das invoice_date wird automatisch beim Finalisieren (Schritt 7) gesetzt — es ist kein Feld im Erstellungs-Payload.
Schritt 5: Positionen hinzufügen
Fügen Sie eine oder mehrere Positionen zur Rechnung hinzu. Rufen Sie diesen Endpoint einmal pro Position auf.
curl -X POST https://docs101.com/api/v1/invoice/17/positions \
-H "X-API-Key: ak_live_YOUR_KEY_HERE" \
-H "Content-Type: application/json" \
-d '{
"title": "Pro Plan — Monthly Subscription",
"description": "April 2026",
"quantity": 1.0,
"unit_id": "HUR",
"unit_net_amount": 25.00,
"single_net_amount": 25.00,
"tax_rate": 0.19,
"tax_treatment_id": "standard"
}'
position_data = {
"title": "Pro Plan — Monthly Subscription",
"description": "April 2026",
"quantity": 1.0,
"unit_id": "HUR",
"unit_net_amount": 25.00,
"single_net_amount": 25.00,
"tax_rate": 0.19,
"tax_treatment_id": "standard",
}
response = requests.post(
f"{BASE_URL}/invoice/{invoice_id}/positions",
headers=HEADERS,
json=position_data,
)
print(f"Position hinzugefügt: {response.status_code}")
Positionsfelder
| Feld | Typ | Pflicht | Beschreibung |
|---|---|---|---|
title | String | Ja | Positionsbezeichnung |
quantity | Float | Ja | Menge |
unit_id | String | Ja | Einheitencode — z.B. HUR (Stunde), C62 (Stück), MON (Monat). Siehe Einheitencodes |
unit_net_amount | Float | Ja | Nettopreis pro Einheit |
single_net_amount | Float | Ja | Nettobetrag der Position (Menge × Einzelpreis, vor Rabatt) |
tax_rate | Float | Nein | Steuersatz als Dezimalzahl: 0.19 = 19%, 0.07 = 7%, 0 = steuerfrei |
tax_treatment_id | String | Nein | z.B. standard, reverse_charge, intra_community, export_outside_eu |
description | String | Nein | Zusatzbeschreibung |
discount_type | String | Nein | PERCENTAGE oder FIXED_AMOUNT |
discount_value | Float | Nein | Rabattwert (z.B. 10 für 10 % oder 50 für 50 EUR) |
Für grenzüberschreitende B2B-Rechnungen innerhalb der EU mit gültiger USt-IdNr. des Kunden verwenden Sie tax_treatment_id: "intra_community" mit tax_rate: 0 (Reverse Charge). Für inländische Rechnungen (gleiches Land) verwenden Sie tax_treatment_id: "standard" mit dem geltenden lokalen Steuersatz. Alle verfügbaren Steuerbehandlungen liefert GET /api/v1/invoice/tax-treatments?locale=de.
single_net_amount ist der Gesamtnettobetrag der Position (Menge × Einzelpreis). Der Server berechnet die Rechnungssummen automatisch nach jeder hinzugefügten Position.
Schritt 6: Rechnung validieren
Vor dem Finalisieren prüfen Sie, ob alle Pflichtfelder vorhanden sind.
curl -H "X-API-Key: ak_live_YOUR_KEY_HERE" \
https://docs101.com/api/v1/invoice/17/validate
response = requests.get(
f"{BASE_URL}/invoice/{invoice_id}/validate", headers=HEADERS
)
validation = response.json()
if validation["valid"]:
print("Rechnung ist gültig — bereit zum Finalisieren")
else:
print("Validierungsfehler:")
for error in validation["errors"]:
print(f" - {error}")
Erfolgreiche Antwort (200):
{
"valid": true,
"errors": [],
"checks": ["..."],
"vat_check": { "status": "valid" }
}
Die Validierung prüft alle Felder, die für die PDF-Generierung erforderlich sind — Rechnungsadresse, Bankverbindung, Positionen und mehr. Eine fehlgeschlagene Validierung verhindert die Finalisierung in Schritt 7.
Schritt 7: Finalisieren — PDF generieren (asynchroner Job)
Die PDF-Generierung ist ein asynchroner Prozess. Sie senden einen Job-Auftrag und fragen dann den Status ab.
Job starten:
curl -X POST https://docs101.com/api/v1/invoice/17/job \
-H "X-API-Key: ak_live_YOUR_KEY_HERE" \
-H "Content-Type: application/json" \
-d '{"task": "process_invoice_flag_invoice_send_job"}'
job_response = requests.post(
f"{BASE_URL}/invoice/{invoice_id}/job",
headers=HEADERS,
json={"task": "process_invoice_flag_invoice_send_job"},
)
job_id = job_response.json()["job_id"]
print(f"Job gestartet: {job_id}")
Erwartete Antwort (201):
{
"message": "Job successfully added",
"job_id": "abc123-..."
}
Job-Status abfragen (Polling):
curl -H "X-API-Key: ak_live_YOUR_KEY_HERE" \
https://docs101.com/api/v1/invoice/17/job/abc123-...
import time
while True:
status_response = requests.get(
f"{BASE_URL}/invoice/{invoice_id}/job/{job_id}", headers=HEADERS
)
status = status_response.json()["status"]
print(f"Job-Status: {status}")
if status == "finished":
break
elif status == "failed":
print(f"Job fehlgeschlagen: {status_response.json()}")
break
time.sleep(2)
Mögliche Statuswerte: queued, started, finished, failed
Die PDF-Generierung dauert typischerweise 3 -- 10 Sekunden. Implementieren Sie ein Polling mit 2-Sekunden-Intervall und einem Timeout von 60 Sekunden. Der Taskname process_invoice_flag_invoice_send_job generiert das PDF und setzt den Rechnungsstatus von DRAFT auf OPEN.
Bei Überschreitung des monatlichen Rechnungslimits (3/Monat bei Free, 100/Monat bei Pro) antwortet der Endpoint mit 402 und einer strukturierten Fehlermeldung mit error_code: "INVOICE_LIMIT_REACHED", usage.used, usage.limit und usage.reset_date.
Schritt 8: PDF herunterladen
Sobald der Job abgeschlossen ist, rufen Sie den Download-Link ab.
curl -H "X-API-Key: ak_live_YOUR_KEY_HERE" \
https://docs101.com/api/v1/invoice/17/pdf-link
response = requests.get(
f"{BASE_URL}/invoice/{invoice_id}/pdf-link", headers=HEADERS
)
pdf_data = response.json()
print(f"PDF-URL: {pdf_data['url']}")
print(f"Dateiname: {pdf_data['filename']}")
Antwort:
{
"url": "https://...",
"filename": "INV-2026-000001.pdf"
}
Die URL ist eine temporäre (presigned) S3-URL, gültig für wenige Minuten. Für die ZUGFeRD-XML separat:
curl -H "X-API-Key: ak_live_YOUR_KEY_HERE" \
https://docs101.com/api/v1/invoice/17/xml-link
Schritt 9: Rechnungsstatus aktualisieren
Nach dem Versand an Ihren Kunden aktualisieren Sie den Status.
Als versendet markieren:
curl -X PUT https://docs101.com/api/v1/invoice/17/status \
-H "X-API-Key: ak_live_YOUR_KEY_HERE" \
-H "Content-Type: application/json" \
-d '{"status": "SENT"}'
requests.put(
f"{BASE_URL}/invoice/{invoice_id}/status",
headers=HEADERS,
json={"status": "SENT"},
)
Als bezahlt markieren:
curl -X PUT https://docs101.com/api/v1/invoice/17/status \
-H "X-API-Key: ak_live_YOUR_KEY_HERE" \
-H "Content-Type: application/json" \
-d '{
"status": "PAID",
"paid_date": "2026-04-15",
"payment_method": "BANK_TRANSFER",
"payment_reference": "SEPA-2026-04-15"
}'
requests.put(
f"{BASE_URL}/invoice/{invoice_id}/status",
headers=HEADERS,
json={
"status": "PAID",
"paid_date": "2026-04-15",
"payment_method": "BANK_TRANSFER",
"payment_reference": "SEPA-2026-04-15",
},
)
Der Statusfluss ist: DRAFT → OPEN (nach dem Job in Schritt 7) → SENT → PAID. Sie können auch jede nicht finalisierte Rechnung über POST /api/v1/invoice/{id}/cancel stornieren.
Schritt 10: Nach DATEV exportieren (Optional)
Für die Übergabe an die Buchhaltung können Sie Rechnungen im DATEV-Format exportieren. Dies umfasst einen einmaligen Konfigurationsschritt, gefolgt vom eigentlichen Export.
DATEV konfigurieren (einmalig)
curl -X PUT https://docs101.com/api/v1/datev/config \
-H "X-API-Key: ak_live_YOUR_KEY_HERE" \
-H "Content-Type: application/json" \
-d '{
"consultant_number": 1234567,
"client_number": 12345,
"chart_of_accounts": "SKR03",
"fiscal_year_start_month": 1
}'
datev_config = {
"consultant_number": 1234567,
"client_number": 12345,
"chart_of_accounts": "SKR03",
"fiscal_year_start_month": 1,
}
requests.put(f"{BASE_URL}/datev/config", headers=HEADERS, json=datev_config)
Export starten
curl -X POST https://docs101.com/api/v1/exports/job \
-H "X-API-Key: ak_live_YOUR_KEY_HERE" \
-H "Content-Type: application/json" \
-d '{
"task": "process_datev_export_job",
"start_date": "2026-01-01",
"end_date": "2026-03-31",
"include_master_data": true,
"include_documents": true,
"only_new": true
}'
export_data = {
"task": "process_datev_export_job",
"start_date": "2026-01-01",
"end_date": "2026-03-31",
"include_master_data": True,
"include_documents": True,
"only_new": True,
}
response = requests.post(f"{BASE_URL}/exports/job", headers=HEADERS, json=export_data)
export_job_id = response.json()["job_id"]
Fragen Sie den Export-Job über GET /api/v1/exports/job/{job_id} ab, bis status: "finished". Die abgeschlossene Antwort enthält ein result-Objekt mit download_ids (ein Array von Download-IDs).
Export herunterladen
curl -H "X-API-Key: ak_live_YOUR_KEY_HERE" \
https://docs101.com/api/v1/downloads/{download_id}/download
# Nach Bestätigung des Abschlusses durch Polling:
result = export_status_response.json()["result"]
for download_id in result["download_ids"]:
response = requests.get(
f"{BASE_URL}/downloads/{download_id}/download", headers=HEADERS
)
download_url = response.json()["url"]
print(f"DATEV-Export: {download_url}")
Die Antwort enthält eine presigned S3-URL für die DATEV-ZIP-Datei.
Fehlerbehandlung
| Status | Bedeutung | Typische Ursache |
|---|---|---|
200 | Erfolg | — |
201 | Ressource erstellt | Kunde, Rechnung, Position oder Job erstellt |
400 | Validierungsfehler | Fehlende Pflichtfelder oder ungültiger Payload |
401 | Nicht authentifiziert | API-Key fehlt oder ungültig |
402 | Limit erreicht | Monatliches Rechnungslimit überschritten |
404 | Nicht gefunden | Ungültige Ressourcen-ID |
422 | USt-Override nötig | Kunden-USt-IdNr. ungültig — erneut senden mit vat_override_confirmed: true |
Für asynchrone Jobs (PDF-Generierung, DATEV-Export) implementieren Sie exponentielles Backoff beim Polling. Prüfen Sie das Feld status — bei failed enthält die Job-Antwort eine error_message mit Details.
Vollständiges Beispiel
Dieses Skript führt den gesamten Workflow von Schritt 2 bis 9 aus:
import requests
import time
API_KEY = "ak_live_YOUR_KEY_HERE"
BASE_URL = "https://docs101.com/api/v1"
HEADERS = {
"X-API-Key": API_KEY,
"Content-Type": "application/json",
}
# Schritt 2: Kunden anlegen
customer = requests.post(
f"{BASE_URL}/customer/",
headers=HEADERS,
json={
"organization_name": "Acme Corp",
"email": "billing@acme.com",
"contact_type": "B2B",
"vat_id": "DE123456789",
"language": "English",
},
).json()
customer_id = customer["id"]
print(f"Kunde erstellt: {customer_id}")
# Schritt 3: Rechnungsadresse hinzufügen
requests.post(
f"{BASE_URL}/customer/{customer_id}/addresses",
headers=HEADERS,
json={
"company": "Acme Corp",
"address_line_1": "Musterstraße 1",
"postal_code": "10115",
"city": "Berlin",
"country_code": "DE",
"is_default": True,
},
)
print("Adresse hinzugefügt")
# Schritt 4: Rechnung erstellen
invoice = requests.post(
f"{BASE_URL}/invoice/",
headers=HEADERS,
json={
"customer_id": customer_id,
"benefit_period_start": "2026-04-01",
"benefit_period_end": "2026-04-30",
"invoice_format": "ZUGFERD",
},
).json()
invoice_id = invoice["invoice_id"]
print(f"Rechnung erstellt: {invoice_id}")
# Schritt 5: Position hinzufügen
requests.post(
f"{BASE_URL}/invoice/{invoice_id}/positions",
headers=HEADERS,
json={
"title": "Pro Plan — Monthly Subscription",
"description": "April 2026",
"quantity": 1.0,
"unit_id": "HUR",
"unit_net_amount": 25.00,
"single_net_amount": 25.00,
"tax_rate": 0.19,
"tax_treatment_id": "standard",
},
)
print("Position hinzugefügt")
# Schritt 6: Validieren
validation = requests.get(
f"{BASE_URL}/invoice/{invoice_id}/validate", headers=HEADERS
).json()
if not validation["valid"]:
print(f"Validierung fehlgeschlagen: {validation['errors']}")
exit(1)
print("Rechnung validiert")
# Schritt 7: Finalisieren (PDF generieren)
job = requests.post(
f"{BASE_URL}/invoice/{invoice_id}/job",
headers=HEADERS,
json={"task": "process_invoice_flag_invoice_send_job"},
).json()
job_id = job["job_id"]
print(f"PDF-Generierung gestartet: {job_id}")
for _ in range(30):
time.sleep(2)
status = requests.get(
f"{BASE_URL}/invoice/{invoice_id}/job/{job_id}", headers=HEADERS
).json()["status"]
if status == "finished":
print("PDF generiert")
break
elif status == "failed":
print("PDF-Generierung fehlgeschlagen")
exit(1)
else:
print("Timeout bei der PDF-Generierung")
exit(1)
# Schritt 8: PDF herunterladen
pdf = requests.get(
f"{BASE_URL}/invoice/{invoice_id}/pdf-link", headers=HEADERS
).json()
print(f"PDF bereit: {pdf['filename']} — {pdf['url']}")
# Schritt 9: Als versendet markieren
requests.put(
f"{BASE_URL}/invoice/{invoice_id}/status",
headers=HEADERS,
json={"status": "SENT"},
)
print("Rechnung als versendet markiert")
Weiterführende Links
- API-Schnellstart — API-Key generieren
- Authentifizierung — API-Key-Nutzung mit curl, Python, JavaScript, PHP
- API-Versionierung — Nicht-brechende vs. brechende Änderungen
- Swagger UI — Vollständige interaktive API-Referenz
- USt-Kategorie-Codes — Steuerklassifizierungscodes
- Einheitencodes — Maßeinheiten (HUR, C62, MON usw.)
- ZUGFeRD erklärt — Hintergrund zum ZUGFeRD-E-Rechnungsformat
- Reverse Charge — Wann und wie Reverse Charge angewendet wird