API-Dokumentation

Zwei Endpunkte: Rechnungen gegen das offizielle KoSIT-Regelwerk prüfen und valide XRechnung aus JSON erzeugen. Alles hier Beschriebene ist dieselbe API, die der öffentliche Validator nutzt.

Stand: 15. Oktober 2026 · Regelwerk v2026-08-31

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

EndpunktZweck
POST /v1/validateEine Rechnung prüfen: XRechnung-XML (UBL oder CII) oder ZUGFeRD/Factur-X-PDF.
POST /v1/invoicesAus 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.

Anfrage
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
Antwort 200 (gekürzt)
{
  "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:

FeldBedeutung
acceptableDas Urteil. Die eine Antwort, auf die eine Annehmen/Ablehnen-Entscheidung gehört.
schematronValidKein 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.
businessRulesEvaluatedFalse, wenn ein Schema-Fehler die Prüfung vorzeitig beendet hat. Eine leere Fehlerliste heißt dann „nicht geprüft“, nicht „nichts gefunden“.
scenarioWelches 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[].explanationDie 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.

Anfrage
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
rechnung.json
{
  "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:

HeaderInhalt
X-Normapi-RulesetRegelwerksstand 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-UsedErzeugte Rechnungen in diesem Monat — nur bei Schlüsseln mit Kontingent
X-Normapi-Quota-LimitDas Kontingent des Tarifs. Ist es erreicht, antwortet /v1/invoices mit 402
X-Request-IdDie Kennung dieses Aufrufs, an jeder Antwort — nennen Sie sie bei Rückfragen
Idempotent-Replayedtrue 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.

Gutschrift zu einer Rechnung
"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.

Treuerabatt, Versandkosten, Mengenrabatt auf eine Position
"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).

SEPA-Lastschrift
"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.

Anfrage
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.pdf

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

Beispiel 422 (Ihr PDF)
{
  "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.

Anfrage
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
SituationAntwort
Erster Aufruf mit dem SchlüsselLäuft wie gewohnt
Gleicher Schlüssel, gleiche Anfrage, und der erste Aufruf war erfolgreich200 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äuft409 mit Retry-After: 1
Gleicher Schlüssel für eine andere Anfrage: andere Daten, andere Syntax oder ein anderes PDF422 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-SchemaEN-16931-RegelnXRechnung-Regeln
XRechnung UBL, Rechnung oder Gutschrift, einschließlich der Varianten Extension und CVDUBL 2.1geprüftgeprüft
XRechnung CII sowie ZUGFeRD/Factur-X im Profil XRECHNUNGCII D16Bgeprüftgeprüft
EN 16931 ohne nationale CIUS: UBL, CII sowie ZUGFeRD/Factur-X EN 16931 (früher COMFORT)UBL 2.1 / CII D16Bgeprüftentfällt
ZUGFeRD/Factur-X BASIC — ein von NormAPI ergänztes Szenario mit den offiziellen EN-16931-RegelnCII D16Bgeprüftentfällt
ZUGFeRD/Factur-X EXTENDED — ein von NormAPI ergänztes Szenario mit den offiziellen EN-16931-RegelnCII D16Bder EN-16931-Kern; Elemente darüber hinaus nicht geprüftentfällt
ZUGFeRD/Factur-X MINIMUM und BASIC WL, Peppol BIS Billing, alles andere (scenario null)nicht geprüftnicht geprüftnicht 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 Dateinicht geprüft
Profil und Version in den XMP-Metadatennicht geprüft
Ob die Seite zeigt, was im XML stehtnicht geprüft
ErzeugenXMLPDF
syntax=ubl, syntax=ciiXRechnung 3.0.2, alle drei Regelebenen, bei jedem Aufrufkeins
syntax=zugferd auf unserer SeiteProfil XRECHNUNG, alle drei Regelebenen, bei jedem AufrufPDF/A-3: unsere Seite weist die Testsuite bei jedem Release mit veraPDF nach, nicht je Aufruf
syntax=zugferd auf Ihrem PDFProfil XRECHNUNG, alle drei Regelebenen, bei jedem AufrufPDF/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.

FeldTypBedeutung
invoiceNumber *stringRechnungsnummer (BT-1)
issueDate *YYYY-MM-DDRechnungsdatum (BT-2)
typeCodeUNTDID 1001Belegart (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
dueDateYYYY-MM-DDFälligkeitsdatum (BT-9). Ohne dueDate oder paymentTerms weiß der Empfänger nicht, wann zu zahlen ist
deliveryDateYYYY-MM-DDLiefer-/Leistungsdatum (BT-72); ohne Angabe gibt das Regelwerk einen Hinweis aus
currency *ISO 4217Währung (BT-5), z. B. EUR
buyerReference *stringKäuferreferenz (BT-10) — bei Behörden die Leitweg-ID
precedingInvoicesReference[]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
notestringFreitext-Bemerkung (BT-22)
seller *PartyVerkäufer (BG-4), siehe unten
buyer *PartyKäufer (BG-7), siehe unten
payment *PaymentZahlungsangaben (BG-16)
paymentTermsstringZahlungsbedingungen (BT-20). Skonto maschinenlesbar als Zeile #SKONTO#TAGE=14#PROZENT=2.00# (BR-DE-18)
allowancesAllowanceCharge[]Nachlässe auf Rechnungsebene (BG-20), siehe unten
chargesAllowanceCharge[]Zuschläge auf Rechnungsebene (BG-21), siehe unten
prepaidAmountZahlBereits 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):

FeldTypBedeutung
name *stringFirmierung (BT-27 / BT-44)
identifierstringKennung (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
vatIdstringUSt-IdNr. (BT-31), z. B. DE123456789
taxNumberstringSteuernummer (BT-32)
electronicAddressstringElektronische Adresse (BT-34 / BT-49), üblicherweise E-Mail
electronicAddressSchemeEAS-CodeSchema der Adresse; Standard EM (E-Mail)
address *Addressstreet (optional), city, postcode, country (ISO 3166-1 alpha-2) — Stadt und PLZ sind Pflicht
contactContactname, phone, email — beim Verkäufer Pflicht (BR-DE-2 bis 7), mit Plausibilitätsprüfung (BR-DE-27/28)

Payment und Line:

FeldTypBedeutung
payment.meansCode *UNTDID 446158 SEPA-Überweisung, 30 Überweisung, 59 SEPA-Lastschrift, 48/54/55 Karte; andere Codes (10, 20, ZZZ …) ohne weitere Gruppe
payment.meansTextstringZahlungsweg im Klartext (BT-82), z. B. „SEPA-Überweisung“
payment.ibanstringEmpfängerkonto (BT-84); bei Code 58/30 von BR-DE-23-a verlangt. Bei einer Gutschrift das Konto, auf das ausgezahlt wird
payment.accountNamestringKontoinhaber (BT-85)
payment.bicBICBIC der Empfängerbank (BT-86), 8 oder 11 Zeichen
payment.referencestringVerwendungszweck (BT-83)
payment.cardCardBei 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.directDebitDirectDebitBei 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 *stringPositionsbezeichnung (BT-153)
line.descriptionstringBeschreibung (BT-154)
line.quantity *ZahlMenge (BT-129), bis 6 Nachkommastellen
line.unit *UN/ECE Rec 20Einheit (BT-130): C62 Stück, HUR Stunde, DAY Tag …
line.unitPrice *ZahlEinzelpreis netto (BT-146), bis 4 Nachkommastellen
line.vatCategory *UNTDID 5305S 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.vatRateProzentSteuersatz (BT-152); bei S Pflicht
line.vatExemptionReasonstringBefreiungsgrund (BT-120) — bei E/AE/K/G vom Regelwerk verlangt, z. B. „Kleinunternehmer gemäß § 19 UStG“
line.allowancesAdjustment[]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.chargesAdjustment[]Zuschläge auf die Position (BG-28), gleich aufgebaut

AllowanceCharge — ein Nachlass oder Zuschlag auf Rechnungsebene; auf Positionsebene dieselben Felder ohne die drei Steuerangaben:

FeldTypBedeutung
amountZahlBetrag (BT-92 / BT-99), zwei Nachkommastellen. Genau eines von amount und percent
percentProzentProzentsatz (BT-94 / BT-101); den Betrag rechnet der Server, kaufmännisch auf den Cent gerundet
baseAmountZahlGrundbetrag (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
reasonstringGrund im Klartext (BT-97 / BT-104). reason oder reasonCode muss da sein
reasonCodeCodeNachlass: UNTDID 5189, z. B. 95 Rabatt · Zuschlag: UNTDID 7161, z. B. FC Fracht
vatCategory *UNTDID 5305Steuerkategorie (BT-95 / BT-102) — bestimmt, welche Steuergruppe der Betrag mindert oder erhöht
vatRateProzentSteuersatz (BT-96 / BT-103)
vatExemptionReasonstringBefreiungsgrund 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:

StatusBedeutungReaktion
400Anfrage 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
401Der 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
402Das Monatskontingent an erzeugten Rechnungen ist aufgebraucht. Bewusst nicht 429: das hier heißt nicht „langsamer“, sondern „der Tarif ist voll“ — Prüfen bleibt freiNächster Tarif, oder Monatswechsel abwarten
409Ein Aufruf mit demselben Idempotency-Key läuft nochNach Retry-After mit demselben Schlüssel wiederholen
413Dokument über 5 MB — auch Ihr eigenes PDFNicht wiederholen
415Body ist weder XML noch PDF, oder Teil pdf ist kein PDFNicht wiederholen
422Verarbeitbar, 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 wurdeDaten, PDF oder Schlüssel korrigieren
429Ihr Anfragebudget ist erschöpft; Retry-After nennt die Wartezeit in SekundenWarten, dann wiederholen
503Alle Prüf-Slots belegt, bei ZUGFeRD auch alle PDF-Slots — Auslastung unsererseits, nicht Ihr FehlerNach Retry-After erneut versuchen
Beispiel 422 (Erzeugen)
{
  "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().

Installation
npm install @normapi/client

Für KI-Agenten gibt es dieselbe API als MCP-Server — unter https://normapi.com/mcp und als npx @normapi/mcp: MCP-Server einrichten.

TypeScript
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
<?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.

openapi.yaml · Postman-Collection

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.