Überblick
Basis-URL: https://api.normapi.de. Requests und Antworten sind UTF-8; Fehler kommen als RFC-9457-Problem-JSON. Zum Prüfen und Auslesen ist keine Authentifizierung nötig — es gelten die Fair-Use-Limits. Einen eigenen Schlüssel mit eigenem Kontingent (Authorization: Bearer nk_live_…) legen Sie selbst an: Konto anlegen — Magic-Link oder GitHub, keine Kreditkarte. Prüfen und Auslesen bleiben kostenlos und zählen nie aufs Kontingent.
| Endpunkt | Zweck |
|---|---|
POST /v1/validate | Eine Rechnung prüfen: XRechnung-XML (UBL oder CII) oder ZUGFeRD/Factur-X-PDF. |
POST /v1/parse | Eine eingegangene Rechnung als JSON auslesen — in genau der Form, die POST /v1/invoices annimmt. |
POST /v1/invoices | Aus Rechnungsdaten (JSON) eine validierte XRechnung (UBL oder CII) oder ein ZUGFeRD-PDF erzeugen — auf unserer Seite oder in Ihrem eigenen PDF. |
Dieselben drei als Werkzeuge für KI-Agenten, in Claude, ChatGPT, Cursor oder VS Code: der MCP-Server.
POST /v1/validate
Das Dokument ist der rohe Request-Body — kein Multipart, kein Base64. Der Dateityp wird am Inhalt erkannt, nie am Content-Type-Header. Bei ZUGFeRD/Factur-X-PDFs wird die eingebettete Rechnung extrahiert und geprüft.
curl -X POST https://api.normapi.de/v1/validate \
-H 'Content-Type: application/xml' \
-H 'Accept-Language: de' \
-H 'X-Document-Name: rechnung.xml' \
--data-binary @rechnung.xml{
"acceptable": false,
"wellFormed": true,
"schemaValid": true,
"schematronValid": false,
"businessRulesEvaluated": true,
"rulesetVersion": "v2026-08-31",
"scenario": "EN16931 XRechnung (UBL Invoice)",
"findings": [
{
"code": "BR-CO-16",
"severity": "ERROR",
"text": "[BR-CO-16]-Amount due for payment (BT-115) = ...",
"origin": "SCHEMATRON",
"location": "/Q{urn:oasis:...:Invoice-2}Invoice[1]/Q{...}LegalMonetaryTotal[1]",
"line": null, "column": null,
"test": "(exists(cbc:PrepaidAmount) and not(exists(cbc:Payable...",
"explanation": {
"meaning": "Der Zahlbetrag (BT-115) muss rechnerisch aufgehen: ...",
"cause": "...",
"fix": "Rechnen Sie Beträge durchgängig mit Dezimalarithmetik, ...",
"url": "https://normapi.com/de/fehler#BR-CO-16"
}
}
]
}Die Bedeutung der Felder — drei davon werden regelmäßig missverstanden:
| Feld | Bedeutung |
|---|---|
acceptable | Das Urteil. Die eine Antwort, auf die eine Annehmen/Ablehnen-Entscheidung gehört. |
schematronValid | Kein Urteil. „Die Fachregeln haben gar nichts gemeldet.“ Das Regelwerk gibt auf völlig gültigen Rechnungen Hinweise aus — jeder Hinweis setzt dieses Feld auf false, während acceptable true bleibt. |
businessRulesEvaluated | False, wenn ein Schema-Fehler die Prüfung vorzeitig beendet hat. Eine leere Fehlerliste heißt dann „nicht geprüft“, nicht „nichts gefunden“. |
scenario | Welches Regelszenario geurteilt hat — „EN16931 XRechnung (UBL Invoice)“, „EN16931 (CII)“ für reine EN-16931-Profile (typisch B2B-ZUGFeRD), sowie „EN16931 Factur-X/ZUGFeRD BASIC (CII)“ und „… EXTENDED (CII)“ für die beiden Hybrid-Profile. Die offizielle KoSIT-Konfiguration kennt für BASIC und EXTENDED kein Szenario; NormAPI ergänzt sie, damit die Dokumente gegen den EN-16931-Kern geprüft werden statt ungeprüft durchzulaufen. null: kein Szenario passte, nichts wurde geprüft, acceptable false ist dann eine Ablehnung des Urteils, kein Urteil. |
findings[] | Jede Meldung mit Regelcode, Schweregrad (ERROR / WARNING / INFORMATION), Regeltext, XPath-Fundstelle und der fehlgeschlagenen Prüfung. Die Fundstelle steht in Clark-Notation — jeder Schritt trägt seinen Namensraum als Q{…}-Präfix statt eines Kürzels wie cac:, weil ein Präfix ohne die Deklaration des Dokuments nichts bedeutet. Der Schweregrad ist der, den die offizielle XRechnung-Konfiguration für das Szenario setzt: BR-CL-23 etwa ist in der Regel selbst fatal, in einer XRechnung aber nur eine Warnung — so passt er zum Urteil in acceptable. |
findings[].explanation | Die Regel in Klartext: was sie verlangt (meaning), warum sie typischerweise auslöst (cause), wie Sie den Fehler beheben (fix), dazu der Link auf ihre Erklärung im Fehlercode-Nachschlagewerk (url). Sprache per Accept-Language: de für Deutsch, fr für Französisch, sonst Englisch; Content-Language nennt die gewählte. Jede Fachregel ist erklärt; null steht bei Meldungen, die keine Fachregel sind, etwa Schema-Meldungen. Der Regeltext in text bleibt unübersetzt. |
POST /v1/parse: Rechnungen auslesen
Der Rückweg zu POST /v1/invoices: Eine eingegangene E-Rechnung — XRechnung oder jede andere EN-16931-Rechnung als UBL oder CII, ein ZUGFeRD- oder Factur-X-PDF — kommt als JSON zurück. Der Endpunkt nimmt genau das an, was POST /v1/validate annimmt: das Dokument als rohen Body, am Inhalt erkannt, bis 5 MB. Aus einem PDF wird die eingebettete Rechnung gelesen. Auslesen ist kostenlos, braucht keinen Schlüssel und zählt nie aufs Kontingent.
curl -X POST https://api.normapi.de/v1/parse \
-H 'Content-Type: application/pdf' \
--data-binary @eingang.pdf{
"syntax": "cii",
"documentKind": "invoice",
"source": { "kind": "pdf", "attachment": "xrechnung.xml" },
"specificationId": "urn:cen.eu:en16931:2017#compliant#urn:xeinkauf.de:kosit:xrechnung_3.0",
"businessProcess": "urn:fdc:peppol.eu:2017:poacc:billing:01:1.0",
"profile": "xrechnung",
"invoice": {
"invoiceNumber": "RE-2026-1001",
"issueDate": "2026-10-01",
"typeCode": "380",
"dueDate": "2026-10-31",
"currency": "EUR",
"buyerReference": "4500012345",
"seller": { "name": "Muster Software GmbH", "vatId": "DE123456789", ... },
"buyer": { "name": "Beispiel Handel AG", ... },
"payment": { "meansCode": "58", "iban": "DE17100500000123456789", ... },
"lines": [
{ "id": "1", "name": "Softwareentwicklung ERP-Schnittstelle",
"quantity": 24, "unit": "HUR", "unitPrice": 95.00,
"vatCategory": "S", "vatRate": 19, ... },
...
],
...
},
"stated": {
"lineTotal": "2940.00",
"netTotal": "2940.00",
"vatTotal": "558.60",
"grossTotal": "3498.60",
"amountDue": "3498.60",
"vatBreakdown": [
{ "category": "S", "rate": "19.00",
"taxableAmount": "2940.00", "vatAmount": "558.60", ... }
],
"lines": [{ "netAmount": "2280.00", ... }, { "netAmount": "660.00", ... }],
...
},
"notMapped": [],
"validation": null
}| Feld | Bedeutung |
|---|---|
invoice | Die Rechnung in genau der Form, die POST /v1/invoices annimmt: auslesen, ändern, neu erzeugen — ohne Umbau dazwischen. Jedes Feld ist vorhanden; was das Dokument nicht enthält, ist null. Es gelten die Standardwerte der Erzeugung, etwa typeCode 380 und leere Listen. Nennt die Norm einen Wert einmal je Steuergruppe, steht er hier an jedem Mitglied: der Befreiungsgrund (BT-120) an jeder Position, jedem Nachlass und Zuschlag dieser Gruppe. Ein Nachlass mit Prozentsatz hat percent und baseAmount, amount ist null — den Betrag nennt stated. |
stated | Was NormAPI beim Erzeugen selbst berechnet und darum nicht aus der Anfrage nimmt: die Summen (BT-106 bis BT-115), die Steueraufschlüsselung (BG-23), das Positionsnetto (BT-131) und die Beträge der Nachlässe und Zuschläge. Als Zeichenketten, genau so, wie sie im Dokument stehen — 336.9 bleibt 336.9, ohne Umweg über Gleitkommazahlen. Ob sie aufgehen, beantwortet das Regelwerk: ?validate=true. |
notMapped[] | Jeder Wert des Dokuments, den die Antwort nicht enthält, als Pfad ohne Positionen und mit Anzahl: /Invoice/cac:InvoiceLine/cac:Item/cac:CommodityClassification/cbc:ItemClassificationCode. Das sind Angaben, für die die Rechnungsdaten keinen Platz haben (etwa die Warenklassifikation oder ein abweichender Zahlungsempfänger), Werte, die sich nicht lesen lassen (ein Datum, das keines ist), und Wiederholungen: Hat das Dokument mehrere Werte, wo die Daten einen fassen, wird der erste gelesen und der Rest hier gemeldet. Bei jeder Rechnung, die NormAPI erzeugt hat, ist die Liste leer. Verloren geht nichts, ohne dass Sie es sehen. |
profile | Der profile-Wert von POST /v1/invoices, der genau diese Spezifikationskennung (BT-24) erklärt — damit erzeugen Sie die Rechnung nach denselben Regeln neu. null bei jeder anderen Kennung, etwa XRechnung 2.x oder ZUGFeRD BASIC. |
syntax, source, documentKind | ubl oder cii; xml oder pdf, beim PDF mit dem Namen der eingebetteten Datei; invoice oder creditNote — in CII entscheidet der Rechnungstyp (BT-3), weil beide dasselbe Wurzelelement haben. |
validation | Mit ?validate=true das Prüfergebnis, genau wie POST /v1/validate es liefert, mit Erklärungen in der Sprache aus Accept-Language. Ohne den Parameter null: Dann läuft kein Regelwerk. |
import { generateInvoice, parse } from '@normapi/client'
import { readFile } from 'node:fs/promises'
const { invoice, stated, notMapped, profile } = await parse(await readFile('eingang.pdf'))
console.log(invoice.invoiceNumber, invoice.seller?.name, stated.amountDue)
for (const entry of notMapped) {
console.log(`nicht übernommen: ${entry.path} (${entry.count}×)`)
}
// Eine eigene Rechnung korrigieren: auslesen, ändern, neu erzeugen —
// nach denselben Regeln, auf die sich das Original beruft.
const { xml } = await generateInvoice(
{
...invoice,
invoiceNumber: 'RE-2026-1001-K',
typeCode: '384',
precedingInvoices: [{ number: invoice.invoiceNumber, issueDate: invoice.issueDate }],
},
{ apiKey: process.env.NORMAPI_KEY, ...(profile === null ? {} : { profile }) },
)POST /v1/invoices
Rechnungsdaten als JSON, validierte XRechnung 3.0.2 zurück — UBL standardmäßig, UN/CEFACT CII mit ?syntax=cii, oder ein komplettes ZUGFeRD-Hybrid-PDF (PDF/A-3 mit eingebetteter Rechnung und menschenlesbarer Seite) mit ?syntax=zugferd. Alle Summen berechnet der Server — Positionsnetto, Steueraufschlüsselung, Gesamtbeträge — in Dezimalarithmetik, sodass die BR-CO-Summenregeln konstruktionsbedingt stimmen. Der klassische E-Rechnungs-Fehler ist Gleitkomma-Arithmetik beim Aufrufer; eine API, die fertige Summen entgegennimmt, würde genau diesen Fehler reproduzieren.
Keine Rechnung verlässt die API ungeprüft: Jedes erzeugte Dokument durchläuft vor der Antwort das offizielle Regelwerk. Eine 200 trägt also ein Dokument, das die Prüfung bestanden hat — beschreibt Ihr JSON eine Rechnung, die die Regeln nicht zulassen, kommt eine 422 mit den Findings. Jedes nennt neben der Regel in fields die Felder Ihres JSON, die sie betrifft, etwa buyer.vatId oder lines[1].unit — auch ein fehlendes Feld wie buyerReference.
Für Unternehmen: profile=en16931. Ohne Angabe entsteht eine XRechnung, wie Behörden sie verlangen. Sie fügt der EN 16931 drei Dinge hinzu: die Käuferreferenz (bei Behörden die Leitweg-ID, BR-DE-15), einen Ansprechpartner des Verkäufers (BR-DE-2) und die elektronischen Adressen beider Seiten. Zwischen Unternehmen genügt nach § 14 UStG die Norm selbst: Mit ?profile=en16931 dürfen diese Angaben fehlen, geprüft wird gegen die EN-16931-Regeln (Szenario „EN16931 (UBL Invoice)“ oder „EN16931 (CII)“), und ein ZUGFeRD-PDF trägt das Profil EN 16931.
curl -X POST https://api.normapi.de/v1/invoices \
-H 'Content-Type: application/json' \
--data-binary @rechnung.json -o rechnung.xml
# UN/CEFACT CII statt UBL:
curl -X POST 'https://api.normapi.de/v1/invoices?syntax=cii' ...
# ZUGFeRD-Hybrid-PDF (application/pdf):
curl -X POST 'https://api.normapi.de/v1/invoices?syntax=zugferd' \
-H 'Content-Type: application/json' \
--data-binary @rechnung.json -o rechnung.pdf
# Für ein Unternehmen: EN 16931, Leitweg-ID, Ansprechpartner und E-Mail-Adressen optional
curl -X POST 'https://api.normapi.de/v1/invoices?syntax=zugferd&profile=en16931' ...{
"invoiceNumber": "RE-2026-0815",
"issueDate": "2026-08-13",
"dueDate": "2026-09-12",
"deliveryDate": "2026-08-10",
"currency": "EUR",
"buyerReference": "04011000-12345-03",
"seller": {
"name": "Muster Software GmbH",
"vatId": "DE123456789",
"electronicAddress": "rechnung@muster-software.de",
"address": { "street": "Beispielstraße 12", "city": "Berlin",
"postcode": "10115", "country": "DE" },
"contact": { "name": "Maria Muster", "phone": "+49 30 1234567",
"email": "maria@muster-software.de" }
},
"buyer": {
"name": "Beispiel Handel AG",
"electronicAddress": "einkauf@beispiel-handel.de",
"address": { "street": "Handelsweg 3", "city": "Hamburg",
"postcode": "20095", "country": "DE" }
},
"payment": { "meansCode": "58", "iban": "DE02120300000000202051",
"reference": "RE-2026-0815" },
"paymentTerms": "Zahlbar innerhalb von 30 Tagen ohne Abzug.",
"lines": [
{ "name": "Softwarelizenz Jahresabo", "quantity": 3, "unit": "C62",
"unitPrice": 199.00, "vatCategory": "S", "vatRate": 19 }
]
}Die Antwort ist das XML selbst (Content-Type application/xml) — direkt in eine Datei umleiten und fertig. Zwei Header tragen die Provenienz:
| Header | Inhalt |
|---|---|
X-Normapi-Ruleset | Regelwerksstand der Prüfung, z. B. v2026-08-31 |
X-Normapi-Scenario | „EN16931 XRechnung (UBL Invoice)“ — die Regeln, die das Dokument bestanden hat |
X-Normapi-Quota-Used | Erzeugte Rechnungen in diesem Monat — nur bei Schlüsseln mit Kontingent |
X-Normapi-Quota-Limit | Das Kontingent des Tarifs. Ist es erreicht, antwortet /v1/invoices mit 402 |
X-Normapi-Daily-Remaining | Ohne Schlüssel: wie viele Rechnungen heute noch möglich sind, diese schon abgezogen. Dazu X-Normapi-Daily-Limit, das Tageslimit |
X-Request-Id | Die Kennung dieses Aufrufs, an jeder Antwort — nennen Sie sie bei Rückfragen |
Idempotent-Replayed | true bei der Wiederholung eines Aufrufs, der unter demselben Idempotency-Key schon gelungen war — dieselbe Rechnung noch einmal, nicht noch einmal gezählt (Wiederholungen) |
X-Normapi-Ignored-Fields | Felder Ihrer Daten, die kein Teil einer Rechnung kennt und die daher in keinem Dokument stehen, z. B. „lines[0].articleNumber“ — nur wenn es welche gibt, auch an 400 und 422. Ein falsch geschriebenes Feld erklärt oft ein fehlendes |
X-Normapi-Customer | Der Kunde, für den der Aufruf gezählt wurde — nur wenn Sie einen genannt haben (Kunden). Dazu X-Normapi-Customer-Used und -Limit, wenn er eine Obergrenze hat |
X-Normapi-Letterhead | Bei einem ZUGFeRD-PDF auf unserer Seite, wenn Ihr Konto einen Briefkopf hat: seine Kennung, wenn das PDF darauf gedruckt wurde, sonst none. Dazu X-Normapi-Letterhead-Skipped mit dem Grund (Briefkopf) |
Gutschriften, Korrekturen, Teil- und Schlussrechnungen wählen Sie mit typeCode (BT-3); der Bezug auf die frühere Rechnung steht in precedingInvoices (BG-3). Eine 381 schreibt die API in UBL als CreditNote-Dokument, wie es das Regelwerk verlangt. Beträge bleiben positiv — die Belegart sagt, in welche Richtung das Geld fließt.
"typeCode": "381",
"precedingInvoices": [
{ "number": "RE-2026-0815", "issueDate": "2026-08-13" }
]Nachlässe und Zuschläge gibt es auf Rechnungsebene (allowances, charges, mit eigener Steuerkategorie) und je Position (ohne Steuerkategorie — es gilt die der Position). Ein Betrag oder ein Prozentsatz; den Betrag aus dem Prozentsatz rechnet der Server, kaufmännisch gerundet, ebenso alle Summen und die Steueraufschlüsselung.
"allowances": [
{ "amount": 9.80, "reason": "Treuerabatt", "reasonCode": "95",
"vatCategory": "S", "vatRate": 7 }
],
"charges": [
{ "amount": 12.50, "reason": "Versandkosten", "reasonCode": "FC",
"vatCategory": "S", "vatRate": 19 }
],
"lines": [
{ "name": "Softwarelizenz Jahresabo", "quantity": 3, "unit": "C62",
"unitPrice": 199.00, "vatCategory": "S", "vatRate": 19,
"allowances": [
{ "percent": 10, "reason": "Mengenrabatt", "reasonCode": "95" }
] }
]Zahlungswege: Überweisung (58, 30) mit Konto, SEPA-Lastschrift (59) mit Mandat, Kartenzahlung (48, 54, 55) mit Karte — und jeder andere UNTDID-4461-Code ohne weitere Angaben. Zu jedem Weg gehört nur seine eigene Gruppe: Wer per Lastschrift einzieht, lässt die eigene IBAN weg (BR-DE-25-b).
"payment": {
"meansCode": "59",
"reference": "RE-2026-0815",
"directDebit": {
"mandateReference": "MANDAT-2026-001",
"creditorId": "DE98ZZZ09999999999",
"debitedAccount": "DE02120300000000202051"
}
}Ihr eigenes PDF
Wenn Ihre Software ihre Rechnungen schon selbst gestaltet, bleibt das Layout: Sie schicken Ihr PDF mit, und NormAPI bettet das geprüfte XML darin ein. Die Anfrage ist multipart, mit den Rechnungsdaten als Teil invoice (dasselbe JSON wie oben) und Ihrem PDF als Teil pdf, bis 5 MB. Die Syntax ist zugferd, die einzige, die ein PDF tragen kann, und darf daher fehlen. Nötig ist ein API-Schlüssel; der Aufruf zählt wie jede andere erzeugte Rechnung einmal.
curl -X POST https://api.normapi.de/v1/invoices \
-H 'Authorization: Bearer nk_live_…' \
-F 'invoice=@rechnung.json;type=application/json' \
-F 'pdf=@ihre-rechnung.pdf;type=application/pdf' \
-o rechnung.pdfZweimal geprüft, bevor es zurückgeht: das XML gegen das Regelwerk, wie jede erzeugte Rechnung, und die fertige Datei mit veraPDF gegen PDF/A-3. Zwei häufige Gründe richtet NormAPI vorher selbst, ohne dass sich die Seite ändert: Helvetica und Arial ohne eingebettete Schrift bekommen die maßgleiche Liberation Sans eingebettet, und Bilder verlieren die Angaben, die PDF/A verbietet (Interpolate, Alternates, OPI). Eine Datei, die danach kein PDF/A-3 werden kann, kommt als 422 zurück; ihre violations nennen die verletzten Abschnitte der ISO 19005-3. Meist ist eine andere Schrift nicht eingebettet: Exportieren Sie das PDF in der Software, die es erzeugt, als PDF/A (1b, 2b oder 3b) und schicken Sie es erneut. Ein PDF, das schon eine E-Rechnung enthält, wird abgelehnt statt überschrieben. Jede Ablehnung Ihres PDFs trägt neben dem Text ein Feld reason, auf das Ihr Programm prüfen kann: not-pdfa, uncheckable, unreadable, already-invoice, not-rewritable, too-large oder not-pdf.
{
"type": "https://normapi.de/problems/pdf-not-pdfa",
"title": "PDF cannot become a PDF/A-3",
"status": 422,
"reason": "not-pdfa",
"violations": [
{ "clause": "6.2.11.4.1", "test": 1, "occurrences": 1,
"description": "The font programs for all fonts used for rendering within a conforming file shall be embedded ..." }
]
}Ihr Briefkopf
Ein ZUGFeRD-PDF auf unserer Seite (?syntax=zugferd, ohne Ihr eigenes PDF) druckt NormAPI auf den Briefkopf Ihres Kontos, wenn es einen hat, mit der Anschrift des Empfängers im Fenster des Briefumschlags. Sie laden den Briefkopf einmal als PDF hoch, im Konto unter Briefkopf. Hochladen, Ersetzen und Löschen gibt es nur dort; die API hat dafür keinen Endpunkt. Der Briefkopf ist in jedem Tarif enthalten, auch im kostenlosen, und die Rechnungen darauf zählen wie jede andere erzeugte Rechnung. XRechnung (UBL oder CII) hat keine Seite und bleibt unverändert.
Hochgeladen wird eine PDF-Datei bis 5 MiB mit einer oder zwei Seiten im A4-Hochformat, mit Bildern bis 10 Megapixel in Farbe (30 in Graustufen) und Zeichnungen bis 512 KiB je Seite. Die erste Seite ist der Briefkopf der ersten Rechnungsseite, die zweite der Briefkopf der Folgeseiten; hat die Datei nur eine Seite, gilt sie für alle Seiten. Im Konto legen Sie auch fest, ob der Briefkopf für alle Rechnungen des Kontos gilt oder nur für einen Verkäufer (USt-IdNr., Steuernummer oder Name; es genügt, wenn eine Angabe mit der Rechnung übereinstimmt).
# Auf dem Briefkopf des Kontos, wenn es einen hat (der Standard):
curl -X POST 'https://api.normapi.de/v1/invoices?syntax=zugferd' \
-H 'Authorization: Bearer nk_live_…' \
-H 'Content-Type: application/json' \
--data-binary @rechnung.json -o rechnung.pdf -D -
# Im Standardlayout von NormAPI:
curl -X POST 'https://api.normapi.de/v1/invoices?syntax=zugferd&letterhead=none' ...Der Query-Parameter letterhead entscheidet je Aufruf:
| Wert | Bedeutung |
|---|---|
auto | Der Standard: der Briefkopf Ihres Kontos, wenn keiner der Gründe unten dagegen spricht |
none | Das Standardlayout von NormAPI |
Der Parameter wirkt bei Aufrufen mit API-Schlüssel; ohne Schlüssel gibt es kein Konto und keinen Briefkopf. Jeder andere Wert ist ein 400, das letterhead nennt, auch bei UBL und CII, wo der Wert sonst keine Wirkung hat. Im Aufruf mit Ihrem eigenen PDF wird er nicht gelesen: Dann ist Ihr PDF das Layout.
Zwei Antwort-Header sagen, was geschehen ist. Beide kommen nur bei ZUGFeRD auf unserer Seite und nur, wenn Ihr Konto einen Briefkopf hat; ohne Briefkopf und bei XML fehlen sie.
# Das PDF steht auf dem Briefkopf:
X-Normapi-Letterhead: lh_9f2c4e1a7b3d45f6a8c0e2b4d6f81357
# Standardlayout, mit Grund:
X-Normapi-Letterhead: none
X-Normapi-Letterhead-Skipped: requested| X-Normapi-Letterhead-Skipped | Grund |
|---|---|
requested | Der Aufruf schickte letterhead=none |
customer-call | Der Aufruf gilt einem Ihrer Kunden (Header X-Normapi-Customer oder ein an einen Kunden gebundener Schlüssel, Kunden): Ihr Briefkopf steht nie auf den Rechnungen Ihrer Kunden |
seller-mismatch | Der Briefkopf gilt für einen Verkäufer, und der Verkäufer der Rechnung ist ein anderer |
needs-confirmation | Die Prüfung des Briefkopfs hat Hinweise ergeben, die im Konto noch bestätigt werden müssen („Trotzdem verwenden“) |
recheck-failed | Die erneute Prüfung nach einem Update von NormAPI ist fehlgeschlagen; das Konto zeigt den Grund |
render-failed | Der Briefkopf konnte für diese Rechnung wegen eines Fehlers auf unserer Seite nicht verwendet werden; die Rechnung ist im Standardlayout erstellt |
Trifft mehr als ein Grund zu, nennt der Header den ersten in dieser Reihenfolge. Mit einem Idempotency-Key nennt eine Wiederholung den Briefkopf, auf den der erste Aufruf gedruckt wurde; wurde der Briefkopf seither geändert, ist die Wiederholung eine andere Anfrage (422 idempotency-key-reused).
Platzhalter. Steht einer dieser Platzhalter als Text im Briefkopf, setzt NormAPI auf jeder Seite den Wert dafür ein, in Liberation Sans, in der Größe des Platzhalters und an dessen Stelle. Groß- und Kleinschreibung spielen keine Rolle.
| Platzhalter | Wird ersetzt durch |
|---|---|
[RECHNUNGSNUMMER] | Die Rechnungsnummer (BT-1). Auch [INVOICE NUMBER] |
[SEITE] | Die Seitenzahl. Auch [PAGE] und [X] |
[SEITEN] | Die Zahl der Seiten. Auch [PAGES] und [Y] |
Ohne Platzhalter steht auf Folgeseiten oben „Rechnung 2026-0042 · Seite 2 von 3“, bei einer Gutschrift „Gutschrift …“. Erkennt NormAPI die Platzhalter des Briefkopfs nicht, etwa weil die Schrift keine Unicode-Zuordnung hat, sagt es die Vorschau im Konto.
Das Adressfeld nach DIN 5008. NormAPI druckt den Empfänger in das Adressfeld und die Rechnungsangaben in den Informationsblock, jeweils an den Stellen der DIN 5008:2020, in Millimetern von der Oberkante der Seite. Der Text der Rechnung steht zwischen 25 und 190 mm vom linken Rand.
| Bereich | Form B | Form A |
|---|---|---|
| Adressfeld, 85 × 45 mm, 20 mm vom linken Rand | 45 bis 90 mm | 27 bis 72 mm |
| Zusatz- und Vermerkzone: oben im Adressfeld, hier steht die Rücksendezeile Ihres Briefkopfs | 45 bis 62,7 mm | 27 bis 44,7 mm |
| Anschriftzone, ab 25 mm vom linken Rand: hier druckt NormAPI die Anschrift des Empfängers | 62,7 bis 90 mm | 44,7 bis 72 mm |
| Informationsblock, 125 bis 190 mm vom linken Rand: die Rechnungsangaben | ab 50 mm | ab 32 mm |
| Titel: die gedruckte Überschrift, bei einer Rechnung „RECHNUNG“ (ihre Grundlinie) | 98,46 mm | 80,46 mm |
| Faltmarken | 105 und 210 mm | 87 und 192 mm |
NormAPI erkennt die Form an den Faltmarken des Briefkopfs oder, ohne Marken, an der Rücksendezeile in der Zusatz- und Vermerkzone. Findet es weder noch, gilt der Briefkopf als ohne Fenster: Anschrift und Rechnungsangaben stehen dann direkt unter dem Briefkopf. Im Konto lassen sich Fensterform und Ränder ändern, und die Vorschau zeigt eine Beispielrechnung auf Ihrem Briefkopf mit dem Adressfeld als gestricheltem Kasten.
Wiederholungen: Idempotency-Key
Nach einem Timeout wissen Sie nicht, ob die Anfrage nie ankam oder ob die Rechnung erzeugt wurde und nur die Antwort verloren ging. Schicken Sie einen Idempotency-Key-Header mit — 1 bis 255 druckbare ASCII-Zeichen, eine UUID oder Ihre Rechnungsnummer — und wiederholen Sie mit demselben Schlüssel. War der erste Aufruf erfolgreich, bekommen Sie die Rechnung noch einmal, markiert mit Idempotent-Replayed: true, und sie wird nicht ein zweites Mal gezählt. Ein Schlüssel gilt 24 Stunden und nur als SHA-256-Wert: Weder er noch die Rechnung werden gespeichert. Er wirkt bei Aufrufen mit API-Schlüssel, denn nur die werden gezählt.
curl -X POST https://api.normapi.de/v1/invoices \
-H 'Authorization: Bearer nk_live_…' \
-H 'Idempotency-Key: RE-2026-0815' \
-H 'Content-Type: application/json' \
--data-binary @rechnung.json -o rechnung.xml| Situation | Antwort |
|---|---|
| Erster Aufruf mit dem Schlüssel | Läuft wie gewohnt |
| Gleicher Schlüssel, gleiche Anfrage, und der erste Aufruf war erfolgreich | 200 mit derselben Rechnung, Idempotent-Replayed: true, nicht gezählt — auch wenn das Kontingent inzwischen ausgeschöpft ist |
| Der erste Aufruf ist gescheitert (400, 422, 503 …) | Der Schlüssel ist wieder frei: Daten korrigieren und mit demselben Schlüssel wiederholen |
| Gleicher Schlüssel, während der erste Aufruf noch läuft | 409 mit Retry-After: 1 |
| Gleicher Schlüssel für eine andere Anfrage: andere Daten, andere Syntax oder ein anderes PDF | 422 idempotency-key-reused — eine neue Rechnung braucht einen neuen Schlüssel |
Eine Wiederholung wird neu erzeugt, nicht aus einem Speicher gelesen, denn Rechnungen werden nicht gespeichert. Die Rechnung ist dieselbe; die internen Zeitstempel eines PDFs können abweichen.
Kunden Ihrer Software
Erzeugt Ihre Software Rechnungen für ihre eigenen Kunden — eine Buchhaltung, ein Shop-System, ein Portal —, dann nennen Sie bei jedem Aufruf den Kunden, für den er läuft: im Header X-Normapi-Customer, mit Ihrer eigenen Kennung für ihn. Es bleibt ein Konto mit einem Kontingent, aber NormAPI zählt jeden Kunden für sich. Im Konto unter Kunden sehen Sie je Monat, was jeder erzeugt hat, und können für jeden eine Obergrenze setzen.
curl -X POST https://api.normapi.de/v1/invoices \
-H 'Authorization: Bearer nk_live_…' \
-H 'X-Normapi-Customer: tenant-42' \
-H 'Content-Type: application/json' \
--data-binary @rechnung.json -o rechnung.xml| Frage | Antwort |
|---|---|
| Was darf drinstehen? | 1 bis 64 Zeichen: Buchstaben, Ziffern und . _ : -, am Anfang ein Buchstabe oder eine Ziffer; Groß- und Kleinschreibung zählt. Sonst 400 invalid-request, rejectedFields nennt den Header |
| Wofür gilt er? | Für jeden Aufruf unter /v1 mit API-Schlüssel, Prüfen, Auslesen und Erzeugen. Die Antwort schickt ihn zurück. Ohne Schlüssel wird er ignoriert, denn dann zählt nichts |
| Und das Kontingent? | Bleibt eines für das Konto: Alle Kunden zusammen zählen gegen den Tarif |
| Mit Idempotency-Key? | Der Kunde gehört zur Anfrage. Derselbe Idempotency-Key für einen anderen Kunden ist eine andere Anfrage: 422 idempotency-key-reused |
Ein Schlüssel je Kunde. Beim Anlegen lässt sich ein Schlüssel an einen Kunden binden. Jeder Aufruf damit zählt für diesen Kunden, auch ohne Header; nennt der Header einen anderen, antwortet die API mit 400 customer-mismatch. So kann ein Kunde, der seinen Schlüssel selbst in der Hand hat, nicht auf einen anderen buchen. Ein gebundener Schlüssel liest unter /v1/customers nur seinen eigenen Kunden und ändert keine Obergrenze (403 customer-bound-key).
Obergrenze je Kunde. Für einen Kunden lässt sich festlegen, wie viele Rechnungen er im Kalendermonat (UTC) höchstens erzeugt. Gezählt wird, was erzeugt wurde; Ablehnungen zählen nicht, und die Wiederholung eines gelungenen Aufrufs unter demselben Idempotency-Key wird auch an der Grenze ausgeliefert. Ist die Grenze erreicht, antwortet /v1/invoices für diesen Kunden mit 429 customer-quota-exceeded und einem Retry-After bis zum Monatsanfang. Prüfen und Auslesen bleiben frei, Ihre anderen Kunden arbeiten weiter. Darunter tragen gelungene Antworten X-Normapi-Customer-Used (in diesem Monat schon erzeugt) und X-Normapi-Customer-Limit.
{
"type": "https://normapi.de/problems/customer-quota-exceeded",
"title": "Monthly ceiling for this customer reached",
"status": 429,
"customer": "tenant-42",
"used": 500,
"limit": 500,
"resetsAt": "2026-11-01T00:00:00Z"
}Die Zahlen für Ihre Abrechnung gibt es auch über die API, mit demselben Schlüssel, ungezählt:
| Aufruf | Wirkung |
|---|---|
| GET /v1/customers?month=2026-10 | Alle Kunden des Monats, die meisten erzeugten Rechnungen zuerst: erzeugte Rechnungen (generated), abgelehnte Erzeugungen mit 4xx (refused), Aufrufe je Endpunkt (calls) und die Obergrenze (monthlyInvoices). Ohne month der laufende Monat; limit (1–1000, Standard 100) und cursor blättern weiter |
| GET /v1/customers/{kunde} | Ein Kunde im Monat (month wie oben) — Nullen für einen, der noch nichts aufgerufen hat |
| PUT /v1/customers/{kunde}/allowance | Body {"monthlyInvoices": 500}: setzt oder ersetzt die Obergrenze (1 bis 10.000.000), sofort für den ganzen Monat |
| DELETE /v1/customers/{kunde}/allowance | Entfernt die Obergrenze; 204, ob es eine gab oder nicht |
{
"month": "2026-10",
"customers": [
{ "customer": "tenant-42", "generated": 418, "refused": 3,
"calls": { "generate": 421, "validate": 96 }, "monthlyInvoices": 500 },
{ "customer": "tenant-7", "generated": 97, "refused": 0,
"calls": { "generate": 97 }, "monthlyInvoices": null }
],
"withoutCustomer": { "generated": 12, "refused": 1,
"calls": { "generate": 13, "validate": 40 } },
"nextCursor": null
}Ein Schlüssel ohne Konto bekommt hier 403 account-required, ein Aufruf ohne Schlüssel 401. Ist nextCursor nicht null, holt derselbe Aufruf mit cursor= diesem Wert die nächste Seite.
Was geprüft wird
Was acceptable: true bedeutet, hängt vom Dokument ab. Alles Folgende läuft bei jedem Aufruf, sofern nicht anders vermerkt, gegen die offizielle KoSIT-Konfiguration v2026-08-31 (XRechnung 3.0.2); „nicht geprüft“ heißt genau das.
| Dokument (Prüfen) | XML-Schema | EN-16931-Regeln | XRechnung-Regeln |
|---|---|---|---|
| XRechnung UBL, Rechnung oder Gutschrift, einschließlich der Varianten Extension und CVD | UBL 2.1 | geprüft | geprüft |
| XRechnung CII sowie ZUGFeRD/Factur-X im Profil XRECHNUNG | CII D16B | geprüft | geprüft |
| EN 16931 ohne nationale CIUS: UBL, CII sowie ZUGFeRD/Factur-X EN 16931 (früher COMFORT) | UBL 2.1 / CII D16B | geprüft | entfällt |
| ZUGFeRD/Factur-X BASIC — ein von NormAPI ergänztes Szenario mit den offiziellen EN-16931-Regeln | CII D16B | geprüft | entfällt |
| ZUGFeRD/Factur-X EXTENDED — ein von NormAPI ergänztes Szenario mit den offiziellen EN-16931-Regeln | CII D16B | der EN-16931-Kern; Elemente darüber hinaus nicht geprüft | entfällt |
| ZUGFeRD/Factur-X MINIMUM und BASIC WL, Peppol BIS Billing, alles andere (scenario null) | nicht geprüft | nicht geprüft | nicht geprüft |
| Das PDF einer Hybrid-Rechnung (Prüfen) | Geprüft? |
|---|---|
| Das eingebettete XML (factur-x.xml, xrechnung.xml oder zugferd-invoice.xml) | geprüft, wie in der Tabelle oben |
| PDF/A-3-Konformität der Datei | mit ?pdf=check geprüft (veraPDF), Verstöße als Warnung; der Validator dieser Website fragt immer danach; mit ?rules=france stets geprüft, als Fehler |
| Profil in den XMP-Metadaten (ConformanceLevel) passend zum XML | mit ?pdf=check geprüft, Abweichung als Warnung; mit ?rules=france stets geprüft, als Fehler |
| Ob die Seite zeigt, was im XML steht | nicht geprüft |
| Erzeugen | XML | |
|---|---|---|
| syntax=ubl, syntax=cii | XRechnung 3.0.2 mit allen drei Regelebenen, mit profile=en16931 Schema und EN-16931-Regeln; bei jedem Aufruf | keins |
| syntax=zugferd auf unserer Seite | Profil XRECHNUNG mit allen drei Regelebenen, mit profile=en16931 Profil EN 16931 mit Schema und EN-16931-Regeln; bei jedem Aufruf | PDF/A-3: unsere Seite weist die Testsuite bei jedem Release mit veraPDF nach, nicht je Aufruf |
| syntax=zugferd auf Ihrem PDF | Profil XRECHNUNG oder EN 16931, wie oben; bei jedem Aufruf | PDF/A-3B mit veraPDF, bei jedem Aufruf; ob Ihre Seite dieselben Beträge zeigt, wird nicht verglichen |
Feldreferenz: Rechnung erzeugen
Pflichtfelder sind markiert. Alles Weitere entscheidet das Regelwerk selbst — fehlt etwa beim Verkäufer sowohl USt-IdNr. als auch Steuernummer, kommt eine 422 mit BR-DE-16, nicht ein erfundener eigener Fehlercode.
| Feld | Typ | Bedeutung |
|---|---|---|
| invoiceNumber * | string | Rechnungsnummer (BT-1) |
| issueDate * | YYYY-MM-DD | Rechnungsdatum (BT-2) |
| typeCode | UNTDID 1001 | Belegart (BT-3), Standard 380 Rechnung · 326 Teilrechnung · 381 Gutschrift · 384 Rechnungskorrektur · 389 Gutschriftsverfahren · 875/876/877 Abschlags-, Teilschluss-, Schlussrechnung (Bau). Andere Codes lösen die Warnung BR-DE-17 aus |
| dueDate | YYYY-MM-DD | Fälligkeitsdatum (BT-9). Ohne dueDate oder paymentTerms weiß der Empfänger nicht, wann zu zahlen ist |
| deliveryDate | YYYY-MM-DD | Liefer-/Leistungsdatum (BT-72); ohne dieses und ohne invoicePeriod gibt das Regelwerk einen Hinweis aus |
| deliverTo | DeliverTo | Lieferort (BG-13), wenn nicht einfach an den Käufer geliefert wird: name (BT-70), locationId (BT-71, etwa die GLN) mit locationIdScheme (BT-71-1, 0088 für GLN), address (BG-15, aufgebaut wie die Adresse unten). Bei einer innergemeinschaftlichen Lieferung (Kategorie K) nötig: Ohne Lieferland lehnt das Regelwerk ab (BR-IC-12) |
| invoicePeriod | Period | Leistungszeitraum (BG-14): startDate (BT-73) und endDate (BT-74), je YYYY-MM-DD; ein Ende genügt. Statt deliveryDate oder daneben, etwa bei einer Monatsrechnung |
| currency * | ISO 4217 | Währung (BT-5), z. B. EUR |
| buyerReference | string | Käuferreferenz (BT-10) — bei Behörden die Leitweg-ID. Pflicht in der XRechnung (BR-DE-15), mit profile=en16931 freiwillig |
| orderReference | string | Bestellnummer des Käufers (BT-13) |
| contractReference | string | Vertragsnummer (BT-12) |
| projectReference | string | Projektnummer (BT-11) |
| despatchAdviceReference | string | Lieferscheinnummer (BT-16) |
| attachments | Attachment[] | Belege zur Rechnung (BG-24), höchstens 20: id (BT-122, sonst der Dateiname), description (BT-123), die Datei als content (Base64, BT-125) mit filename und mimeCode, oder url (BT-124). Erlaubt sind PDF, PNG, JPEG, CSV, XLSX und ODS (BR-CL-24), Dateinamen nur einmal (BR-DE-22), zusammen bis 3 MB. In CII warnen CII-SR-475/476 ab zwei Dateien oder Beschreibungen, ein bekannter Fehler dieser CEN-Regeln |
| precedingInvoices | Reference[] | Bezug auf frühere Rechnungen (BG-3): number (BT-25, Pflicht), issueDate (BT-26). Bei 384 erwartet (BR-DE-26). In CII und ZUGFeRD höchstens einer — das Schema hat nur Platz für einen |
| note | string | Freitext-Bemerkung (BT-22) |
| seller * | Party | Verkäufer (BG-4), siehe unten |
| buyer * | Party | Käufer (BG-7), siehe unten |
| payment * | Payment | Zahlungsangaben (BG-16) |
| paymentTerms | string | Zahlungsbedingungen (BT-20). Skonto maschinenlesbar als Zeile #SKONTO#TAGE=14#PROZENT=2.00# (BR-DE-18) |
| allowances | AllowanceCharge[] | Nachlässe auf Rechnungsebene (BG-20), siehe unten |
| charges | AllowanceCharge[] | Zuschläge auf Rechnungsebene (BG-21), siehe unten |
| prepaidAmount | Zahl | Bereits gezahlt (BT-113), etwa Abschläge vor der Schlussrechnung; der Zahlbetrag (BT-115) sinkt entsprechend |
| lines * | Line[] | Mindestens eine Position (BG-25) |
Party — für Verkäufer und Käufer gleich aufgebaut; was auf welcher Seite Pflicht ist, entscheidet das Regelwerk (der Verkäufer braucht eine steuerliche Identifikation und in der XRechnung einen Kontakt, der Käufer nicht):
| Feld | Typ | Bedeutung |
|---|---|---|
| name * | string | Firmierung (BT-27 / BT-44) |
| identifier | string | Kennung (BT-29). Wichtig für Verkäufer ohne USt-IdNr.: Die Steuernummer erfüllt BR-DE-16, aber erst eine Kennung erfüllt BR-CO-26 — die Steuernummer darf hier doppelt eingetragen werden |
| vatId | string | USt-IdNr. (BT-31), z. B. DE123456789 |
| taxNumber | string | Steuernummer (BT-32) |
| electronicAddress | string | Elektronische Adresse (BT-34 / BT-49), üblicherweise E-Mail. Die XRechnung verlangt sie auf beiden Seiten (PEPPOL-EN16931-R020 und -R010), EN 16931 nicht |
| electronicAddressScheme | EAS-Code | Schema der Adresse; Standard EM (E-Mail) |
| address * | Address | street (optional), city, postcode, country (ISO 3166-1 alpha-2) — Stadt und PLZ sind Pflicht |
| contact | Contact | name, phone, email — beim Verkäufer in der XRechnung Pflicht (BR-DE-2 bis 7), mit Plausibilitätsprüfung (BR-DE-27/28); mit profile=en16931 freiwillig, auch einzeln |
| tradingName | string | Handelsname neben dem rechtlichen Namen (BT-28 / BT-45) |
| registrationId | string | Registernummer, etwa HRB 123456 (BT-30 / BT-47); identifiziert den Verkäufer wie die USt-IdNr. (BR-CO-26). registrationIdScheme (BT-30-1): ISO-6523-Code, für deutsche Register keiner, für eine französische SIREN 0002 |
| legalInformation | string | Nur beim Verkäufer (BT-33): was das Gesellschaftsrecht auf Geschäftsbriefen verlangt — Sitz, Registergericht, Geschäftsführer (§ 35a GmbHG, § 80 AktG, § 37a HGB). Das ZUGFeRD-PDF druckt es mit der Registernummer |
Payment und Line:
| Feld | Typ | Bedeutung |
|---|---|---|
| payment.meansCode * | UNTDID 4461 | 58 SEPA-Überweisung, 30 Überweisung, 59 SEPA-Lastschrift, 48/54/55 Karte; andere Codes (10, 20, ZZZ …) ohne weitere Gruppe |
| payment.meansText | string | Zahlungsweg im Klartext (BT-82), z. B. „SEPA-Überweisung“ |
| payment.iban | string | Empfängerkonto (BT-84); bei Code 58/30 von BR-DE-23-a verlangt. Bei einer Gutschrift das Konto, auf das ausgezahlt wird |
| payment.accountName | string | Kontoinhaber (BT-85) |
| payment.bic | BIC | BIC der Empfängerbank (BT-86), 8 oder 11 Zeichen |
| payment.reference | string | Verwendungszweck (BT-83) |
| payment.card | Card | Bei 48/54/55 (BG-18): number (BT-87) — nur die letzten 4 bis 6 Ziffern, ohne Sternchen oder Leerzeichen; mehr als 10 Ziffern lehnt die API mit 400 ab —, holder (BT-88), network (z. B. VISA, nur UBL) |
| payment.directDebit | DirectDebit | Bei 59 (BG-19) alle drei: mandateReference (BT-89), creditorId (BT-90, Ihre Gläubiger-ID), debitedAccount (BT-91, die IBAN des Kunden). Die eigene IBAN dann weglassen |
| line.name * | string | Positionsbezeichnung (BT-153) |
| line.description | string | Beschreibung (BT-154) |
| line.quantity * | Zahl | Menge (BT-129), bis 6 Nachkommastellen |
| line.unit * | UN/ECE Rec 20 | Einheit (BT-130): C62 Stück, HUR Stunde, DAY Tag … |
| line.unitPrice * | Zahl | Einzelpreis netto (BT-146), bis 4 Nachkommastellen — mit baseQuantity der Preis für so viele Einheiten |
| line.vatCategory * | UNTDID 5305 | S Normalsatz · Z Nullsatz · E steuerbefreit · AE Reverse Charge · K innergemeinschaftlich · G Export · O nicht steuerbar · L/M. O steht allein: keine andere Kategorie daneben, keine USt-IdNr. bei Verkäufer und Käufer (der Verkäufer dann mit taxNumber und identifier), ein Befreiungsgrund |
| line.vatRate | Prozent | Steuersatz (BT-152); bei S Pflicht |
| line.vatExemptionReason | string | Befreiungsgrund (BT-120) — bei E/AE/K/G vom Regelwerk verlangt, z. B. „Kleinunternehmer gemäß § 19 UStG“ |
| line.allowances | Adjustment[] | Nachlässe auf die Position (BG-27): amount oder percent, baseAmount, reason, reasonCode — ohne Steuerkategorie. Das Positionsnetto (BT-131) ist Menge × Preis ÷ baseQuantity abzüglich Nachlässe zuzüglich Zuschläge |
| line.charges | Adjustment[] | Zuschläge auf die Position (BG-28), gleich aufgebaut |
| line.period | Period | Leistungszeitraum der Position (BG-26), aufgebaut wie invoicePeriod |
| line.orderLineReference | string | Position der Bestellung, die diese Position abrechnet (BT-132) |
| line.note | string | Hinweis zu dieser Position (BT-127) |
| line.sellerItemId | string | Artikelnummer des Verkäufers (BT-155) |
| line.buyerItemId | string | Artikelnummer des Käufers für denselben Artikel (BT-156) |
| line.standardItemId | string | Kennung nach einem registrierten Schema (BT-157), meist die GTIN (EAN) von der Verpackung |
| line.standardItemIdScheme | ISO 6523 ICD | Schema der Kennung (BT-157-1); ohne Angabe 0160, die GTIN |
| line.listPrice | Zahl | Listenpreis vor dem Rabatt (BT-148), mindestens unitPrice. Den Rabatt (BT-147) rechnen wir: listPrice − unitPrice |
| line.baseQuantity | Zahl | Preisbasismenge (BT-149): für wie viele Einheiten unitPrice und listPrice gelten, z. B. 100 bei Schrauben je 100 Stück; ohne Angabe 1 |
AllowanceCharge — ein Nachlass oder Zuschlag auf Rechnungsebene; auf Positionsebene dieselben Felder ohne die drei Steuerangaben:
| Feld | Typ | Bedeutung |
|---|---|---|
| amount | Zahl | Betrag (BT-92 / BT-99), zwei Nachkommastellen. Genau eines von amount und percent |
| percent | Prozent | Prozentsatz (BT-94 / BT-101); den Betrag rechnet der Server, kaufmännisch auf den Cent gerundet |
| baseAmount | Zahl | Grundbetrag (BT-93 / BT-100), nur zusammen mit percent — ein Grundbetrag ohne Prozentsatz ist ein Fehler (PEPPOL-EN16931-R042). Ohne Angabe: die Positionsnettos derselben Steuerkategorie und desselben Satzes, auf Positionsebene Menge × Preis |
| reason | string | Grund im Klartext (BT-97 / BT-104). reason oder reasonCode muss da sein |
| reasonCode | Code | Nachlass: UNTDID 5189, z. B. 95 Rabatt · Zuschlag: UNTDID 7161, z. B. FC Fracht |
| vatCategory * | UNTDID 5305 | Steuerkategorie (BT-95 / BT-102) — bestimmt, welche Steuergruppe der Betrag mindert oder erhöht |
| vatRate | Prozent | Steuersatz (BT-96 / BT-103) |
| vatExemptionReason | string | Befreiungsgrund für die Steuergruppe (BT-120), wenn sie nur durch diesen Betrag entsteht |
Fehlerantworten
Fehler sind RFC-9457-Problem-JSON mit type, title, detail und status. Die Statuscodes tragen Bedeutung:
| Status | Bedeutung | Reaktion |
|---|---|---|
| 400 | Anfrage strukturell unbrauchbar — detail nennt jedes betroffene Feld: fehlend, falsch geformt (etwa eine BIC oder ein X-Normapi-Customer mit Leerzeichen) oder nicht zu einem Dokument zu machen (amount und percent zugleich, ein Prozentsatz ohne Grundbetrag, eine Kartennummer mit mehr als 10 Ziffern). Außerdem: ein an einen Kunden gebundener Schlüssel mit einem Header, der einen anderen nennt (customer-mismatch) | Anfrage korrigieren |
| 401 | Der mitgeschickte Schlüssel ist unbekannt oder widerrufen. Ohne Schlüssel gelten die anonymen Limits — ein falscher ist schlechter als keiner. Außerdem: Ihr eigenes PDF ohne Schlüssel geschickt (api-key-required) | Schlüssel prüfen |
| 402 | Das Monatskontingent an erzeugten Rechnungen ist aufgebraucht. Bewusst nicht 429: das hier heißt nicht „langsamer“, sondern „der Tarif ist voll“ — Prüfen und Auslesen bleiben frei | Nächster Tarif, oder Monatswechsel abwarten |
| 403 | Unter /v1/customers: ein Schlüssel ohne Konto (account-required), oder der Schlüssel eines Kunden, der andere Kunden lesen oder eine Obergrenze ändern will (customer-bound-key) | Den Schlüssel des Kontos nehmen |
| 409 | Ein Aufruf mit demselben Idempotency-Key läuft noch. Unter /v1/customers: das Konto hat schon 100.000 Obergrenzen (too-many-customer-allowances) | Nach Retry-After mit demselben Schlüssel wiederholen — oder nicht mehr gebrauchte Obergrenzen entfernen |
| 413 | Dokument über 5 MB — auch Ihr eigenes PDF —, oder Rechnungsdaten über 6 MB | Nicht wiederholen |
| 415 | Body ist weder XML noch PDF, oder Teil pdf ist kein PDF | Nicht wiederholen |
| 422 | Verarbeitbar, aber unzulässig: PDF ohne eingebettete Rechnung, beim Auslesen auch XML, das keine Rechnung ist (nicht wohlgeformt, mit DOCTYPE, falsches Wurzelelement) — oder beim Erzeugen Daten, die das Regelwerk ablehnt (findings[] nennt jede Regel und in fields die betroffenen Felder Ihres JSON), Ihr eigenes PDF, das kein PDF/A-3 werden kann (violations[]), oder ein Idempotency-Key, der für eine andere Anfrage benutzt wurde | Daten, PDF oder Schlüssel korrigieren |
| 429 | Ihr Anfragebudget ist erschöpft; Retry-After nennt die Wartezeit in Sekunden. Ohne Schlüssel auch: die Rechnungen des Tages sind erzeugt (daily-limit-reached), Retry-After reicht bis Mitternacht. Mit Kunde auch: er hat seine Obergrenze im Monat erreicht (customer-quota-exceeded), Retry-After reicht bis zum Monatsanfang | Warten, dann wiederholen — oder mit Schlüssel, oder die Obergrenze anheben |
| 503 | Alle Prüf-Slots belegt, bei ZUGFeRD auch alle PDF-Slots — Auslastung unsererseits, nicht Ihr Fehler | Nach Retry-After erneut versuchen |
{
"type": "https://normapi.de/problems/invoice-not-permitted",
"title": "Invoice not permitted by the rule set",
"status": 422,
"detail": "... Fields to correct: seller.vatId (BR-DE-16), seller.taxNumber (BR-DE-16).",
"rulesetVersion": "v2026-08-31",
"findings": [
{ "code": "BR-DE-16", "severity": "ERROR",
"text": "[BR-DE-16] ... Umsatzsteueridentifikationsnummer ...",
"fields": ["seller.vatId", "seller.taxNumber"],
"explanation": { "meaning": "...", "cause": "...",
"fix": "Tragen Sie mindestens eines der drei Elemente ein. ...",
"url": "https://normapi.com/de/fehler#BR-DE-16" } }
]
}Limits
- 30 Anfragen pro Minute je Client, zugleich das Burst-Budget. Darüber: 429 mit Retry-After.
- Ohne Schlüssel 10 erzeugte Rechnungen am Tag je Client (je IPv6-/64-Netz), bis Mitternacht deutscher Zeit. Darüber: 429 vom Typ daily-limit-reached mit Retry-After bis Mitternacht. Abgelehnte Daten (400, 422) zählen nicht, Prüfen und Auslesen haben kein Tageslimit, mit Schlüssel gilt das Kontingent des Tarifs.
- 5 MB je Dokument — echte E-Rechnungen sind Kilobytes.
- Begrenzte Parallelität beim Prüfen, Auslesen und Erzeugen von PDFs: Unter Last antwortet die API schnell mit 503 statt langsam mit Timeouts. Kurz warten und wiederholen.
- Höhere Kontingente für Integrationen: kontakt@normapi.de.
Client-Bibliotheken
@normapi/client — TypeScript, null Abhängigkeiten, läuft in Node 18+, Browsern, Deno und Bun. Deckt alle Endpunkte ab: validate(), parse() (ab 0.7.0) und generateInvoice(). Für den Briefkopf nimmt generateInvoicePdf() ab 0.9.0 die Option letterhead (auto oder none) und gibt letterhead und letterheadSkipped zurück.
npm install @normapi/clientFür KI-Agenten gibt es dieselbe API als MCP-Server — unter https://normapi.com/mcp und als npx @normapi/mcp: MCP-Server einrichten.
import { validate, RateLimitError } from '@normapi/client'
import { readFile } from 'node:fs/promises'
const result = await validate(await readFile('rechnung.xml'))
if (result.acceptable) {
console.log(`gültig — geprüft als ${result.scenario}`)
} else {
for (const f of result.findings) {
console.log(`${f.severity} ${f.code}: ${f.text}`)
}
}Für alle anderen Sprachen ist die API bewusst kurz: ein POST mit rohem Body. Die curl-Beispiele oben übersetzen sich in jede HTTP-Bibliothek in wenige Zeilen — hier PHP, weil in diesem Markt viel WooCommerce und JTL läuft.
<?php
$xml = file_get_contents('rechnung.xml');
$ch = curl_init('https://api.normapi.de/v1/validate');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => $xml,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Content-Type: application/xml',
'Accept-Language: de',
'Authorization: Bearer ' . getenv('NORMAPI_KEY'),
],
]);
$result = json_decode(curl_exec($ch), true);
curl_close($ch);
if ($result['acceptable']) {
echo "gültig — geprüft als {$result['scenario']}\n";
} else {
foreach ($result['findings'] as $f) {
echo "{$f['severity']} {$f['code']}: {$f['text']}\n";
if ($f['explanation'] !== null) {
echo " Lösung: {$f['explanation']['fix']}\n";
}
}
}Sie erzeugen mit horstoeko, Mustang oder einer anderen freien Bibliothek und betreiben den KoSIT-Validator bisher selbst? Der KoSIT-Validator als API zeigt dieselbe Prüfung in PHP, Java und der CI — ohne Java-Installation und ohne Docker.
Spezifikation & Postman
Die vollständige OpenAPI-Spezifikation (3.1) beschreibt alle Endpunkte, alle Statuscodes und die Feldstruktur — geeignet für Codegeneratoren, Mock-Server und den Import in jedes API-Werkzeug.
Regelwerk & Versionierung
Jede Antwort nennt in rulesetVersion den Regelwerksstand, der das Urteil gefällt hat — aktuell v2026-08-31. Das XRechnung-Regelwerk wird etwa zweimal jährlich aktualisiert; eine heute gültige Rechnung kann gegen ein neues Regelwerk Hinweise oder Fehler produzieren.
Wenn sich der Regelstand ändert, schreiben wir Ihnen: Benachrichtigung abonnieren. Was einzelne Regeln bedeuten und wie man sie behebt: das Fehlercode-Nachschlagewerk. Zum Ausprobieren ohne Code: der Validator, und mit Konto der Playground: echte Aufrufe von Erzeugen, Prüfen und Auslesen, ohne Schlüssel und mit dem Aufruf als curl, TypeScript oder Python daneben.