Viele Rechnungs-APIs zwingen dich in starre Layouts: Logo oben links, keine Rabatt-Spalte, keine Kontrolle über Farben. Invoice-api.xhub geht den umgekehrten Weg — PDF-Rechnungen werden über ein JSON-basiertes BlockTemplate beschrieben. Das Ergebnis ist versioniert, diffbar und per CI-Pipeline reproduzierbar.
In diesem Tutorial zeige ich dir, wie du in 10 Minuten eine eigene Vorlage baust, dein Logo einbindest, Markenfarben per Design-Token anwendest und USt-Zeilen korrekt darstellst — inklusive der Fallstricke rund um § 14 UStG und ZUGFeRD.
📚 Vollständige Referenz: Die kompletten Schemas und Block-Typen findest du in der Rechnungsvorlagen-Dokumentation.
Das BlockTemplate in 30 Sekunden
Ein BlockTemplate ist ein JSON-Dokument mit fünf Bereichen:
1{2 "version": "1.0.0",3 "name": "meine-rechnung",4 "page": { /* Seitengröße, Ränder, Design-Tokens */ },5 "header": { /* optionaler Briefkopf — auf jeder Seite */ },6 "body": { "blocks": [ /* der eigentliche Inhalt */ ] },7 "footer": { /* optionaler Fußbereich */ },8 "styles": { /* benannte Presets */ }9}Der body enthält ein Array aus neun Block-Typen: text, table, keyvalue, summary, image, spacer, line, qrcode und columns. Jeder Block hat denselben Envelope { type, data } — und optional eine condition, die ihn bei leeren Feldern ausblendet.
Schritt 1 — Design-Tokens definieren
Statt Hex-Werte in jedem Block zu wiederholen, legst du sie einmal in page.colorScheme ab:
1{2 "page": {3 "size": "A4",4 "margins": { "top": 40, "right": 40, "bottom": 40, "left": 40 },5 "colorScheme": {6 "primary": "#0ea5e9",7 "text": "#111827",8 "muted": "#6b7280",9 "line": "#e5e7eb"10 },11 "fontSizes": { "body": 10, "heading": 18, "small": 8 }12 }13}Blöcke referenzieren das später so:
1{ "type": "text", "data": { "content": "RECHNUNG", "color": { "token": "primary" } } }Ändert sich die Markenfarbe, tauschst du genau einen Wert aus — die komplette Vorlage zieht mit.
Schritt 2 — Logo im Header
Zwei-Spalten-Header: Links Firmenname, rechts das Logo. repeatOnAllPages: true macht ihn zum Briefkopf auf jeder Seite.
1{2 "header": {3 "height": 90,4 "repeatOnAllPages": true,5 "blocks": [{6 "type": "columns",7 "data": {8 "columnGap": 20,9 "columns": [10 { "width": "*", "blocks": [11 { "type": "text", "data": { "content": "{{seller.name}}", "bold": true, "fontSize": { "token": "heading" } } }12 ]},13 { "width": "auto", "blocks": [14 { "type": "image", "data": { "src": "https://cdn.example.com/logo.png", "fit": [140, 70], "alignment": "right" } }15 ]}16 ]17 }18 }]19 }20}Das src kann eine URL sein, ein Base64-Data-URI oder — wie hier — ein Platzhalter, der pro Mandant aus dem Invoice-Payload kommt.
Schritt 3 — Positionen mit Rabatt-Spalte
Der table-Block bindet an {{items}}. Jede Spalte deklariert field, header, width und optional format: "currency" | "percent" | "number":
1{2 "type": "table",3 "data": {4 "dataSource": "{{items}}",5 "repeatHeader": true,6 "layout": "lightHorizontalLines",7 "headerFillColor": { "token": "primary" },8 "headerTextColor": "#FFFFFF",9 "columns": [10 { "field": "articleNumber", "header": "Artikel-Nr.", "width": "15%" },11 { "field": "description", "header": "Beschreibung", "width": "*" },12 { "field": "quantity", "header": "Menge", "width": "10%", "align": "center" },13 { "field": "unitPrice", "header": "Einzel", "width": "15%", "align": "right", "format": "currency" },14 { "field": "netAmount", "header": "Netto", "width": "15%", "align": "right", "format": "currency" }15 ]16 }17}Beachte: Die Spaltenbreiten akzeptieren Pixel, Prozente, auto oder * (füllt den Rest).
Schritt 4 — USt-Zeilen dynamisch
Das ist der Teil, den viele Templates hart verdrahten — und dabei scheitern, sobald eine Rechnung gemischte Steuersätze oder Reverse-Charge enthält. Der summary-Block mit dynamicRows iteriert über {{taxSummary}}:
1{2 "type": "summary",3 "data": {4 "alignment": "right",5 "headerRows": [6 { "label": "Netto gesamt", "value": "{{subtotal}}", "valueFormat": "currency" }7 ],8 "dynamicRows": {9 "dataSource": "{{taxSummary}}",10 "valueField": "taxAmount",11 "valueFormat": "currency",12 "labelTemplate": "USt {{taxRate}}%"13 },14 "footerRows": [15 { "label": "Gesamtbetrag", "value": "{{total}}", "bold": true, "valueFormat": "currency", "separator": true }16 ]17 }18}Eine Rechnung mit 7 % + 19 % erzeugt automatisch zwei Zeilen. Bei Reverse-Charge kommt taxAmount: 0 plus der Pflichthinweis als bedingter text-Block.
Schritt 5 — Vorlage verwenden
Zwei Wege, deine Vorlage an die API zu übergeben:
1. Als gespeichertes Template (empfohlen):
1curl -X POST https://service.invoice-api.xhub.io/api/v1/invoice/de/zugferd/generate \2 -H "Authorization: Bearer $XHUB_API_KEY" \3 -d '{4 "templateId": "b3c9a0d8-4f2e-4a1c-9e87-0d2f1a5b6c7d",5 "invoice": { /* Creator-Payload */ }6 }'2. Inline — für CI-Tests oder dynamische Layouts:
1curl -X POST https://service.invoice-api.xhub.io/api/v1/invoice/de/zugferd/generate \2 -H "Authorization: Bearer $XHUB_API_KEY" \3 -d '{4 "invoice": { /* ... */ },5 "formatOptions": { "template": { /* inline BlockTemplate */ } }6 }'Bei Mehrfach-Angabe gilt: templateId > formatOptions.template > Default.
Compliance-Fallstricke
Drei Dinge, die leicht übersehen werden:
- § 14 UStG-Pflichtangaben. Rechnungsnummer, Ausstellungsdatum, Leistungsdatum, USt-IdNr. bzw. Steuernummer, Entgelt pro Steuersatz — keines darf fehlen. Der Validator prüft das beim
/generate-Aufruf, aber ein defensives Template entfernt diese Blöcke schon gar nicht. - Leitweg-ID bei B2G. Rechnungen an Behörden brauchen die Leitweg-ID. Im Payload liegt sie unter
countrySpecific.leitwegId(DE-spezifisch). Mitcondition: "{{countrySpecific.leitwegId}}"rendert der Block nur bei B2G — dieselbe Vorlage bleibt für B2B nutzbar. - ZUGFeRD ist nicht nur das PDF. Das BlockTemplate ist die sichtbare Ebene. Die strukturierte EN-16931-XML wird beim
/generateautomatisch in die PDF/A-3 eingebettet. Beide Darstellungen müssen dieselben Zahlen zeigen — deshalb kommen beide aus demselben Invoice-Payload.
Die komplette Compliance-Matrix mit Reverse-Charge, Kleinunternehmer und Factur-X findest du in der Compliance-Dokumentation.
Fazit
BlockTemplate ist kein Design-Tool-Ersatz — es ist ein API-first-Ansatz für Teams, die ihre Rechnungen wie Code behandeln wollen: versioniert, review-bar, in CI getestet. Für einmalige Layout-Änderungen ist ein WYSIWYG-Editor vielleicht schneller. Für 50 Mandanten mit unterschiedlichen Logos und Farben, die über einen einzigen API-Call ausgerollt werden, ist JSON unschlagbar.
Weiterlesen:
- Rechnungsvorlagen-Übersicht — alle Block-Typen auf einen Blick
- Platzhalter-Referenz — jedes Feld aus dem Creator-Payload
- Branding-Guide — Design-Tokens im Detail
- Cookbook — SEPA-QR, Seitenzahlen, Skonto-Block, …
Häufige Fragen
Muss ich für jedes Mandanten-Layout eine eigene Vorlage anlegen?
Nein — eine Vorlage mit Design-Tokens reicht. Pro Mandant überschreibst du nur page.colorScheme und das Logo. Für 50 Mandanten bleibt also eine einzige Template-Struktur, lediglich die Tokens ändern sich.
Kann ich BlockTemplate versionieren und in CI testen? Ja. Weil die Vorlage ein JSON-Dokument ist, läuft sie durch Git wie Code: Diffs bleiben lesbar, Code-Review funktioniert, und ein CI-Snapshot-Test kann nach jedem Commit das gerenderte PDF gegen einen erwarteten Hash prüfen.
Wie unterscheiden sich templateId und formatOptions.template?
templateId referenziert eine in der Konsole oder per tRPC-Mutation gespeicherte Vorlage (empfohlen für stabile Produktion). formatOptions.template übergibt das BlockTemplate-JSON inline — praktisch für Tests, CI und dynamisch erzeugte Layouts. Bei gleichzeitiger Angabe gewinnt templateId.
Funktioniert BlockTemplate auch für Peppol und XRechnung — oder nur PDF/ZUGFeRD?
BlockTemplate steuert ausschließlich die visuelle PDF-Ebene. Die strukturierte XML für XRechnung oder Peppol BIS 3.0 kommt aus dem Invoice-Payload. Für ZUGFeRD und Factur-X wird die EN-16931-XML beim /generate-Aufruf automatisch in die PDF/A-3 eingebettet (Hybrid-Rechnung).
Wo finde ich alle verfügbaren Platzhalter?
In der Platzhalter-Referenz. Jedes Feld aus dem Creator-Payload ist als {{mustache}}-Platzhalter verfügbar — von {{invoiceNumber}} über {{seller.bankAccount.iban}} bis {{countrySpecific.leitwegId}}.

