API documentation

Two endpoints: validate invoices against the official KoSIT rule set, and generate valid XRechnung from JSON. Everything described here is the same API the public validator runs on.

Last updated: 14 August 2026 · rule set v2026-01-31

Overview

Base URL: https://api.normapi.de. Requests and responses are UTF-8; errors are RFC 9457 problem JSON. No authentication during the beta — the fair-use limits apply. Design partners get API keys with their own quota (Authorization: Bearer nk_live_…): kontakt@normapi.de.

EndpointPurpose
POST /v1/validateValidate one invoice: XRechnung XML (UBL or CII) or a ZUGFeRD/Factur-X PDF.
POST /v1/invoicesGenerate a validated XRechnung (UBL) from invoice data as JSON.

POST /v1/validate

The document is the raw request body — no multipart, no base64. The file type is detected from content, never from the Content-Type header. For ZUGFeRD/Factur-X PDFs the embedded invoice is extracted and validated.

Request
curl -X POST https://api.normapi.de/v1/validate \
  -H 'Content-Type: application/xml' \
  -H 'X-Document-Name: invoice.xml' \
  --data-binary @invoice.xml
Response 200 (abridged)
{
  "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 * ..."
    }
  ]
}

What the fields mean — three of them are routinely misread:

FieldMeaning
acceptableThe verdict. The one answer an accept/reject decision should rest on.
schematronValidNot a verdict. “The business rules reported nothing at all.” The rule set emits advisories on perfectly valid invoices — any advisory sets this false while acceptable stays true.
businessRulesEvaluatedFalse when a schema failure stopped the run early. An empty findings list then means "not checked", never "nothing wrong".
scenarioWhich rule scenario judged the document — “EN16931 XRechnung (UBL Invoice)”, “EN16931 (CII)” for plain EN 16931 profiles (typical B2B ZUGFeRD). null: no scenario matched, nothing was checked, and acceptable false is a refusal to judge, not a judgement.
findings[]Every message with its rule code, severity (ERROR / WARNING / INFORMATION), rule text, XPath location and the failed test. Every BR-DE code has an explained error page.

POST /v1/invoices

Invoice data as JSON in, a validated XRechnung 3.0.2 back — UBL by default, UN/CEFACT CII with ?syntax=cii, or a complete ZUGFeRD hybrid PDF (PDF/A-3 with the invoice embedded and a human-readable page) with ?syntax=zugferd. The server computes every total — line nets, the VAT breakdown, document totals — in decimal arithmetic, so the BR-CO sum rules hold by construction. The classic e-invoicing failure is caller-side float arithmetic; an API that accepted precomputed totals would reproduce exactly that bug.

No invoice leaves unvalidated: every generated document runs through the official rule set before the response. A 200 therefore carries a document that passed — if your JSON describes an invoice the rules do not permit, you get a 422 with the findings.

Request
curl -X POST https://api.normapi.de/v1/invoices \
  -H 'Content-Type: application/json' \
  --data-binary @invoice.json -o invoice.xml

# UN/CEFACT CII instead of 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 @invoice.json -o invoice.pdf
invoice.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": "Payable within 30 days net.",
  "lines": [
    { "name": "Software licence, annual", "quantity": 3, "unit": "C62",
      "unitPrice": 199.00, "vatCategory": "S", "vatRate": 19 }
  ]
}

The response body is the XML itself (Content-Type application/xml) — redirect it to a file and you are done. Two headers carry provenance:

HeaderContent
X-Normapi-RulesetThe rule set that judged the document, e.g. v2026-01-31
X-Normapi-Scenario"EN16931 XRechnung (UBL Invoice)" — the rules the document passed

Field reference: generating

Required fields are marked. Everything else is decided by the rule set itself — a seller with neither VAT id nor tax number gets a 422 naming BR-DE-16, not a made-up error code of ours.

FieldTypeMeaning
invoiceNumber *stringInvoice number (BT-1)
issueDate *YYYY-MM-DDIssue date (BT-2)
dueDateYYYY-MM-DDDue date (BT-9). Without dueDate and paymentTerms, BR-CO-25 fails
deliveryDateYYYY-MM-DDDelivery date (BT-72); omitting it draws an advisory from the rule set
currency *ISO 4217Currency (BT-5), e.g. EUR
buyerReference *stringBuyer reference (BT-10) — the Leitweg-ID for public-sector buyers
notestringFree-text note (BT-22)
seller *PartySeller (BG-4), see below
buyer *PartyBuyer (BG-7), see below
payment *PaymentPayment instructions (BG-16)
paymentTermsstringPayment terms (BT-20)
lines *Line[]At least one line (BG-25)

Party — the same shape for seller and buyer; which side needs what is the rule set's call (the seller needs a contact and tax identity, the buyer does not):

FieldTypeMeaning
name *stringLegal name (BT-27 / BT-44)
identifierstringAny party identifier (BT-29). Matters for sellers without a VAT id: the tax number satisfies BR-DE-16 but only an identifier satisfies BR-CO-26 — repeating the tax number here is fine
vatIdstringVAT id (BT-31), e.g. DE123456789
taxNumberstringGerman tax number (BT-32)
electronicAddressstringElectronic address (BT-34 / BT-49), usually an email address
electronicAddressSchemeEAS codeScheme of the address; default EM (email)
address *Addressstreet (optional), city, postcode, country (ISO 3166-1 alpha-2) — city and postcode are mandatory
contactContactname, phone, email — mandatory on the seller (BR-DE-2 through 7), with plausibility checks (BR-DE-27/28)

Payment and Line:

FieldTypeMeaning
payment.meansCode *UNTDID 446158 SEPA credit transfer, 30 credit transfer — other means coming
payment.ibanstringPayee account (BT-84); required by BR-DE-23-a for codes 58/30
payment.referencestringRemittance information (BT-83)
line.name *stringItem name (BT-153)
line.descriptionstringItem description (BT-154)
line.quantity *numberQuantity (BT-129), up to 6 decimals
line.unit *UN/ECE Rec 20Unit (BT-130): C62 piece, HUR hour, DAY day …
line.unitPrice *numberNet unit price (BT-146), up to 4 decimals
line.vatCategory *UNTDID 5305S standard · Z zero-rated · E exempt · AE reverse charge · K intra-community · G export · O/L/M
line.vatRatepercentVAT rate (BT-152); required for S
line.vatExemptionReasonstringExemption reason (BT-120) — demanded by the rule set for E/AE/K/G, e.g. "Kleinunternehmer gemäß § 19 UStG"

Error responses

Errors are RFC 9457 problem JSON with type, title, detail and status. The status codes carry meaning:

StatusMeaningReaction
400Request structurally unusable — detail names every missing fieldFix the request
413Document over 5 MBDo not retry
415Body is neither XML nor PDFDo not retry
422Readable but not permitted: a PDF without an embedded invoice — or, when generating, data the rule set rejects (findings[] names each rule)Fix the data
429Your request budget is exhausted; Retry-After says how many seconds to waitWait, then retry
503All validation slots busy — load on our side, not your faultRetry after Retry-After
Example 422 (generating)
{
  "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 requests per minute per client, doubling as the burst budget. Beyond it: 429 with Retry-After.
  • 5 MB per document — real e-invoices are kilobytes.
  • Bounded concurrent validations: under load the API answers fast with 503 instead of slowly with timeouts. Wait briefly and retry.
  • Higher quotas for integrations: kontakt@normapi.de.

Client libraries

@normapi/client — TypeScript, zero dependencies, runs in Node 18+, browsers, Deno and Bun. Covers both endpoints: validate() and generateInvoice().

Install
npm install @normapi/client
TypeScript
import { validate, RateLimitError } from '@normapi/client'
import { readFile } from 'node:fs/promises'

const result = await validate(await readFile('invoice.xml'))

if (result.acceptable) {
  console.log(`valid — checked as ${result.scenario}`)
} else {
  for (const f of result.findings) {
    console.log(`${f.severity} ${f.code}: ${f.text}`)
  }
}

For every other language the API is deliberately small: one POST with a raw body. The curl examples above translate into any HTTP library in a few lines.

Rule set & versioning

Every response names the rule set that produced its verdict in rulesetVersion — currently v2026-01-31. The XRechnung rule set updates roughly twice a year; an invoice that is valid today can draw advisories or errors from a newer release.

We email when the rule set changes: subscribe to notifications. What individual rules mean and how to fix them: the error-code reference. To try it without writing code: the validator.