Zum Hauptinhalt springen

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)
  • curl oder Python 3.8+ mit der requests-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

FeldTypPflichtBeschreibung
organization_nameStringJa (B2B)Firmenname
first_name / last_nameStringJa (B2C)Vor-/Nachname bei Privatpersonen
emailStringNeinE-Mail-Adresse
contact_typeStringNeinB2B (Standard) oder B2C
vat_idStringNeinEU-Umsatzsteuer-Identifikationsnummer
languageStringNeinRechnungssprache: English, German
payment_term_idIntegerNeinZahlungsziel-ID
default_ftl_template_idIntegerNeinStandard-Template für diesen Kunden
salutationStringNeinMR, MRS, MS, OTHER
Anrede-Werte

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

FeldTypPflichtBeschreibung
address_line_1StringJaStraße und Hausnummer
cityStringJaStadt
country_codeStringJaISO 3166-1 Alpha-2-Code (z.B. DE, AT, FR)
companyStringNeinFirmenname in der Adresse
address_line_2StringNeinAdresszusatz
postal_codeStringNeinPostleitzahl
state_provinceStringNeinBundesland / Region
is_defaultBooleanNeinAls 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

FeldTypPflichtBeschreibung
customer_idIntegerJaKunden-ID aus Schritt 2
benefit_period_startDateNeinLeistungszeitraum Start (YYYY-MM-DD)
benefit_period_endDateNeinLeistungszeitraum Ende
invoice_formatStringNeinPDF, ZUGFERD, FACTURAPA, PEPPOL, FACTURAE
invoice_numberStringNeinWird automatisch generiert, wenn leer
metaStringNeinFreitext-Metadaten
Rechnungsdatum

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

FeldTypPflichtBeschreibung
titleStringJaPositionsbezeichnung
quantityFloatJaMenge
unit_idStringJaEinheitencode — z.B. HUR (Stunde), C62 (Stück), MON (Monat). Siehe Einheitencodes
unit_net_amountFloatJaNettopreis pro Einheit
single_net_amountFloatJaNettobetrag der Position (Menge × Einzelpreis, vor Rabatt)
tax_rateFloatNeinSteuersatz als Dezimalzahl: 0.19 = 19%, 0.07 = 7%, 0 = steuerfrei
tax_treatment_idStringNeinz.B. standard, reverse_charge, intra_community, export_outside_eu
descriptionStringNeinZusatzbeschreibung
discount_typeStringNeinPERCENTAGE oder FIXED_AMOUNT
discount_valueFloatNeinRabattwert (z.B. 10 für 10 % oder 50 für 50 EUR)
Steuerbehandlung

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.

Beträge

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" }
}
Immer validieren

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

Asynchrone Verarbeitung

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.

Rechnungslimit

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",
},
)
Rechnungslebenszyklus

Der Statusfluss ist: DRAFTOPEN (nach dem Job in Schritt 7) → SENTPAID. 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

StatusBedeutungTypische Ursache
200Erfolg
201Ressource erstelltKunde, Rechnung, Position oder Job erstellt
400ValidierungsfehlerFehlende Pflichtfelder oder ungültiger Payload
401Nicht authentifiziertAPI-Key fehlt oder ungültig
402Limit erreichtMonatliches Rechnungslimit überschritten
404Nicht gefundenUngültige Ressourcen-ID
422USt-Override nötigKunden-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")