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: 14. August 2026 · Regelwerk v2026-01-31

Überblick

Basis-URL: https://api.normapi.de. Requests und Antworten sind UTF-8; Fehler kommen als RFC-9457-Problem-JSON. Während der Beta ist keine Authentifizierung nötig — es gelten die Fair-Use-Limits. Design-Partner erhalten API-Schlüssel mit eigenem Kontingent (Authorization: Bearer nk_live_…): kontakt@normapi.de.

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

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 '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-01-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": "/Invoice[1]/cac:LegalMonetaryTotal[1]/...",
      "line": null, "column": null,
      "test": "(- 1 * ..."
    }
  ]
}

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). 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. Zu jedem BR-DE-Code gibt es eine erklärte Fehlerseite.

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-01-31
X-Normapi-Scenario„EN16931 XRechnung (UBL Invoice)" — die Regeln, die das Dokument bestanden hat

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)
dueDateYYYY-MM-DDFälligkeitsdatum (BT-9). Ohne dueDate und paymentTerms scheitert BR-CO-25
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
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)
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 — andere Zahlwege folgen
payment.ibanstringEmpfängerkonto (BT-84); bei Code 58/30 von BR-DE-23-a verlangt
payment.referencestringVerwendungszweck (BT-83)
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/L/M
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"

Fehlerantworten

Fehler sind RFC-9457-Problem-JSON mit type, title, detail und status. Die Statuscodes tragen Bedeutung:

StatusBedeutungReaktion
400Anfrage strukturell unbrauchbar — detail nennt jedes fehlende FeldAnfrage korrigieren
413Dokument über 5 MBNicht wiederholen
415Body ist weder XML noch PDFNicht wiederholen
422Verarbeitbar, aber unzulässig: PDF ohne eingebettete Rechnung — oder beim Erzeugen Daten, die das Regelwerk ablehnt (findings[] nennt jede Regel)Daten korrigieren
429Ihr Anfragebudget ist erschöpft; Retry-After nennt die Wartezeit in SekundenWarten, dann wiederholen
503Alle Prüf-Slots belegt — 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-01-31",
  "findings": [
    { "code": "BR-DE-16", "severity": "ERROR",
      "text": "[BR-DE-16] ... Umsatzsteueridentifikationsnummer ..." }
  ]
}

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

Regelwerk & Versionierung

Jede Antwort nennt in rulesetVersion den Regelwerksstand, der das Urteil gefällt hat — aktuell v2026-01-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.