Überblick
Basis-URL: https://api.normapi.de. Requests und Antworten sind UTF-8; Fehler kommen als RFC-9457-Problem-JSON. Zum Prüfen 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 bleibt kostenlos und zählt nie aufs Kontingent.
| Endpunkt | Zweck |
|---|---|
POST /v1/validate | Eine Rechnung prüfen: XRechnung-XML (UBL oder CII) oder ZUGFeRD/Factur-X-PDF. |
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. |
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.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, sonst Englisch; Content-Language nennt die gewählte. Jede Fachregel ist erklärt; null steht nur bei Schema-Meldungen. Der Regeltext in text bleibt unübersetzt. |
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.
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{
"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-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) |
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. Eine Datei, die kein PDF/A-3 werden kann, kommt als 422 zurück; ihre violations nennen die verletzten Abschnitte der ISO 19005-3. Meist sind die Schriften 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.
{
"type": "https://normapi.de/problems/pdf-not-pdfa",
"title": "PDF cannot become a PDF/A-3",
"status": 422,
"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 ..." }
]
}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 gespeichert wird nichts. Die Rechnung ist dieselbe; die internen Zeitstempel eines PDFs können abweichen.
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 | nicht geprüft |
| Profil und Version in den XMP-Metadaten | nicht geprüft |
| Ob die Seite zeigt, was im XML steht | nicht geprüft |
| Erzeugen | XML | |
|---|---|---|
| syntax=ubl, syntax=cii | XRechnung 3.0.2, alle drei Regelebenen, bei jedem Aufruf | keins |
| syntax=zugferd auf unserer Seite | Profil XRECHNUNG, alle drei Regelebenen, 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, alle drei Regelebenen, 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 Angabe gibt das Regelwerk einen Hinweis aus |
| currency * | ISO 4217 | Währung (BT-5), z. B. EUR |
| buyerReference * | string | Käuferreferenz (BT-10) — bei Behörden die Leitweg-ID |
| 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 Kontakt und steuerliche Identifikation, 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 |
| 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 Pflicht (BR-DE-2 bis 7), mit Plausibilitätsprüfung (BR-DE-27/28) |
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 |
| 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 abzüglich Nachlässe zuzüglich Zuschläge |
| line.charges | Adjustment[] | Zuschläge auf die Position (BG-28), gleich aufgebaut |
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 nicht zu einem Dokument zu machen (amount und percent zugleich, ein Prozentsatz ohne Grundbetrag, eine Kartennummer mit mehr als 10 Ziffern) | 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 bleibt frei | Nächster Tarif, oder Monatswechsel abwarten |
| 409 | Ein Aufruf mit demselben Idempotency-Key läuft noch | Nach Retry-After mit demselben Schlüssel wiederholen |
| 413 | Dokument über 5 MB — auch Ihr eigenes PDF | 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 — oder beim Erzeugen Daten, die das Regelwerk ablehnt (findings[] nennt jede Regel), 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 | Warten, dann wiederholen |
| 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,
"rulesetVersion": "v2026-08-31",
"findings": [
{ "code": "BR-DE-16", "severity": "ERROR",
"text": "[BR-DE-16] ... Umsatzsteueridentifikationsnummer ...",
"explanation": { "meaning": "...", "cause": "...",
"fix": "Tragen Sie mindestens eines der drei Elemente ein. ...",
"url": "https://normapi.de/fehler/br-de-16" } }
]
}Limits
- 30 Anfragen pro Minute je Client, zugleich das Burst-Budget. Darüber: 429 mit Retry-After.
- 5 MB je Dokument — echte E-Rechnungen sind Kilobytes.
- Begrenzte parallele Prüfungen und PDF-Erzeugungen: 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 beide Endpunkte ab: validate() und generateInvoice().
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 beide 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.