TutorialRechnungsvorlageAPI

Rechnungsvorlage per API anpassen: BlockTemplate-Tutorial

Wie du PDF-Rechnungen mit Logo, Markenfarben und dynamischen USt-Zeilen per JSON erzeugst. Inkl. § 14 UStG-Compliance und ZUGFeRD-Fallstricken.

Rechnungsvorlage per API anpassen: BlockTemplate-Tutorial
Patrick Jerominek

Patrick Jerominek

Cofounder xhub.io

14. April 2026
8 min Lesezeit

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:

json
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:

json
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:

json
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.

json
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":

json
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}}:

json
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):

bash
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:

bash
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:

  1. § 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.
  2. Leitweg-ID bei B2G. Rechnungen an Behörden brauchen die Leitweg-ID. Im Payload liegt sie unter countrySpecific.leitwegId (DE-spezifisch). Mit condition: "{{countrySpecific.leitwegId}}" rendert der Block nur bei B2G — dieselbe Vorlage bleibt für B2B nutzbar.
  3. ZUGFeRD ist nicht nur das PDF. Das BlockTemplate ist die sichtbare Ebene. Die strukturierte EN-16931-XML wird beim /generate automatisch 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:


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}}.

Artikel teilen
Patrick Jerominek

Geschrieben von

Patrick Jerominek

Cofounder xhub.io

Baut APIs, die Entwickler lieben. Schreibt über E-Invoicing, TypeScript und alles dazwischen.

Ähnliche Artikel

Bereit, E-Rechnungen zu meistern?

Starte in unter 5 Minuten mit Invoice-api.xhub. Keine Kreditkarte erforderlich.