Ü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.
| 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) 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.
curl -X POST https://api.normapi.de/v1/validate \
-H 'Content-Type: application/xml' \
-H 'X-Document-Name: rechnung.xml' \
--data-binary @rechnung.xml{
"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:
| 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). 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.
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-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.
| Feld | Typ | Bedeutung |
|---|---|---|
| invoiceNumber * | string | Rechnungsnummer (BT-1) |
| issueDate * | YYYY-MM-DD | Rechnungsdatum (BT-2) |
| dueDate | YYYY-MM-DD | Fälligkeitsdatum (BT-9). Ohne dueDate und paymentTerms scheitert BR-CO-25 |
| 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 |
| 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) |
| 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 — andere Zahlwege folgen |
| payment.iban | string | Empfängerkonto (BT-84); bei Code 58/30 von BR-DE-23-a verlangt |
| payment.reference | string | Verwendungszweck (BT-83) |
| 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/L/M |
| 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" |
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 fehlende Feld | Anfrage korrigieren |
| 413 | Dokument über 5 MB | Nicht wiederholen |
| 415 | Body ist weder XML noch PDF | Nicht wiederholen |
| 422 | Verarbeitbar, aber unzulässig: PDF ohne eingebettete Rechnung — oder beim Erzeugen Daten, die das Regelwerk ablehnt (findings[] nennt jede Regel) | Daten korrigieren |
| 429 | Ihr Anfragebudget ist erschöpft; Retry-After nennt die Wartezeit in Sekunden | Warten, dann wiederholen |
| 503 | Alle Prüf-Slots belegt — 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-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().
npm install @normapi/clientimport { 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.