API-Dokumentation

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

Stand: 12. November 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 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.

EndpunktZweck
POST /v1/validateEine Rechnung prüfen: XRechnung-XML (UBL oder CII) oder ZUGFeRD/Factur-X-PDF.
POST /v1/parseEine eingegangene Rechnung als JSON auslesen — in genau der Form, die POST /v1/invoices annimmt.
POST /v1/invoicesAus 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.

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.com/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, 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.

Anfrage
curl -X POST https://api.normapi.de/v1/parse \
  -H 'Content-Type: application/pdf' \
  --data-binary @eingang.pdf
Antwort 200 (gekürzt)
{
  "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
}
FeldBedeutung
invoiceDie 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.
statedWas 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.
profileDer 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, documentKindubl 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.
validationMit ?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.
TypeScript (@normapi/client ab 0.7.0)
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.

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

# 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' ...
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-Normapi-Daily-RemainingOhne Schlüssel: wie viele Rechnungen heute noch möglich sind, diese schon abgezogen. Dazu X-Normapi-Daily-Limit, das Tageslimit
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)
X-Normapi-Ignored-FieldsFelder 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-CustomerDer 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-LetterheadBei 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.

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

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

Anfrage
# 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:

WertBedeutung
autoDer Standard: der Briefkopf Ihres Kontos, wenn keiner der Gründe unten dagegen spricht
noneDas 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.

Antwort-Header
# 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-SkippedGrund
requestedDer Aufruf schickte letterhead=none
customer-callDer 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-mismatchDer Briefkopf gilt für einen Verkäufer, und der Verkäufer der Rechnung ist ein anderer
needs-confirmationDie Prüfung des Briefkopfs hat Hinweise ergeben, die im Konto noch bestätigt werden müssen („Trotzdem verwenden“)
recheck-failedDie erneute Prüfung nach einem Update von NormAPI ist fehlgeschlagen; das Konto zeigt den Grund
render-failedDer 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.

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

BereichForm BForm A
Adressfeld, 85 × 45 mm, 20 mm vom linken Rand45 bis 90 mm27 bis 72 mm
Zusatz- und Vermerkzone: oben im Adressfeld, hier steht die Rücksendezeile Ihres Briefkopfs45 bis 62,7 mm27 bis 44,7 mm
Anschriftzone, ab 25 mm vom linken Rand: hier druckt NormAPI die Anschrift des Empfängers62,7 bis 90 mm44,7 bis 72 mm
Informationsblock, 125 bis 190 mm vom linken Rand: die Rechnungsangabenab 50 mmab 32 mm
Titel: die gedruckte Überschrift, bei einer Rechnung „RECHNUNG“ (ihre Grundlinie)98,46 mm80,46 mm
Faltmarken105 und 210 mm87 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.

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

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

Antwort 429
{
  "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:

AufrufWirkung
GET /v1/customers?month=2026-10Alle 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}/allowanceBody {"monthlyInvoices": 500}: setzt oder ersetzt die Obergrenze (1 bis 10.000.000), sofort für den ganzen Monat
DELETE /v1/customers/{kunde}/allowanceEntfernt die Obergrenze; 204, ob es eine gab oder nicht
Antwort GET /v1/customers
{
  "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-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 Dateimit ?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 XMLmit ?pdf=check geprüft, Abweichung als Warnung; mit ?rules=france stets geprüft, als Fehler
Ob die Seite zeigt, was im XML stehtnicht geprüft
ErzeugenXMLPDF
syntax=ubl, syntax=ciiXRechnung 3.0.2 mit allen drei Regelebenen, mit profile=en16931 Schema und EN-16931-Regeln; bei jedem Aufrufkeins
syntax=zugferd auf unserer SeiteProfil XRECHNUNG mit allen drei Regelebenen, mit profile=en16931 Profil EN 16931 mit Schema und EN-16931-Regeln; 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 oder EN 16931, wie oben; 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 dieses und ohne invoicePeriod gibt das Regelwerk einen Hinweis aus
deliverToDeliverToLieferort (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)
invoicePeriodPeriodLeistungszeitraum (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 4217Währung (BT-5), z. B. EUR
buyerReferencestringKäuferreferenz (BT-10) — bei Behörden die Leitweg-ID. Pflicht in der XRechnung (BR-DE-15), mit profile=en16931 freiwillig
orderReferencestringBestellnummer des Käufers (BT-13)
contractReferencestringVertragsnummer (BT-12)
projectReferencestringProjektnummer (BT-11)
despatchAdviceReferencestringLieferscheinnummer (BT-16)
attachmentsAttachment[]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
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 eine steuerliche Identifikation und in der XRechnung einen Kontakt, 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. Die XRechnung verlangt sie auf beiden Seiten (PEPPOL-EN16931-R020 und -R010), EN 16931 nicht
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 in der XRechnung Pflicht (BR-DE-2 bis 7), mit Plausibilitätsprüfung (BR-DE-27/28); mit profile=en16931 freiwillig, auch einzeln
tradingNamestringHandelsname neben dem rechtlichen Namen (BT-28 / BT-45)
registrationIdstringRegisternummer, 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
legalInformationstringNur 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:

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 — mit baseQuantity der Preis für so viele Einheiten
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 ÷ baseQuantity abzüglich Nachlässe zuzüglich Zuschläge
line.chargesAdjustment[]Zuschläge auf die Position (BG-28), gleich aufgebaut
line.periodPeriodLeistungszeitraum der Position (BG-26), aufgebaut wie invoicePeriod
line.orderLineReferencestringPosition der Bestellung, die diese Position abrechnet (BT-132)
line.notestringHinweis zu dieser Position (BT-127)
line.sellerItemIdstringArtikelnummer des Verkäufers (BT-155)
line.buyerItemIdstringArtikelnummer des Käufers für denselben Artikel (BT-156)
line.standardItemIdstringKennung nach einem registrierten Schema (BT-157), meist die GTIN (EAN) von der Verpackung
line.standardItemIdSchemeISO 6523 ICDSchema der Kennung (BT-157-1); ohne Angabe 0160, die GTIN
line.listPriceZahlListenpreis vor dem Rabatt (BT-148), mindestens unitPrice. Den Rabatt (BT-147) rechnen wir: listPrice − unitPrice
line.baseQuantityZahlPreisbasismenge (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:

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 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
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 und Auslesen bleiben freiNächster Tarif, oder Monatswechsel abwarten
403Unter /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
409Ein 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
413Dokument über 5 MB — auch Ihr eigenes PDF —, oder Rechnungsdaten über 6 MBNicht wiederholen
415Body ist weder XML noch PDF, oder Teil pdf ist kein PDFNicht wiederholen
422Verarbeitbar, 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 wurdeDaten, PDF oder Schlüssel korrigieren
429Ihr 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 MonatsanfangWarten, dann wiederholen — oder mit Schlüssel, oder die Obergrenze anheben
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,
  "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.

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 alle Endpunkte, alle Statuscodes und die Feldstruktur — geeignet für Codegeneratoren, Mock-Server und den Import in jedes API-Werkzeug.

openapi.yamlPostman-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, 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.