Documentation

API documentation

normbill is a headless e-invoicing API: you POST invoice JSON, you get back a compliant XRechnung or ZUGFeRD / Factur-X document — validated before it reaches you. When validation fails, every finding maps back to a field in your request with a plain-language message and a concrete fix.

Product pages: XRechnung API · ZUGFeRD API · XRechnung vs ZUGFeRD · XRechnung PHP. Same JSON payload, different format.

Authentication

Every request carries an API key as a bearer token. You get keys in the dashboard right after signing up — no sales call.

Authorization: Bearer sk_test_…   # or sk_live_… in production

Requests without a valid key are rejected with 401 unauthorized.

The API is plain HTTPS — no client library is required. Typed clients exist for Node.js / TypeScript and PHP, and an MCP server lets AI agents call the API directly — see SDKs & MCP server. Live status: /status. OpenAPI: /openapi.json.

Your first invoice

POST /v1/invoices/generate takes a JSON invoice and returns the generated XML plus a validation report. This payload is complete and passes validation as-is:

curl https://api.normbill.com/v1/invoices/generate \
  -H "Authorization: Bearer sk_test_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "format": "xrechnung-3.0",
    "invoice": {
      "number": "INV-2026-0001",
      "issue_date": "2026-06-15",
      "due_date": "2026-07-15",
      "currency": "EUR",
      "buyer_reference": "04011000-1234512345-06",
      "seller": {
        "name": "Muster Lieferant GmbH",
        "vat_id": "DE123456789",
        "address": {
          "country": "DE",
          "city": "Berlin",
          "postal_code": "10115"
        },
        "contact": {
          "name": "Billing",
          "phone": "+49 30 1234567",
          "email": "billing@lieferant.example"
        }
      },
      "buyer": {
        "name": "Beispiel Kunde AG",
        "address": {
          "country": "DE",
          "city": "Hamburg",
          "postal_code": "20095"
        },
        "contact": {
          "email": "ap@kunde.example"
        }
      },
      "payment": {
        "iban": "DE89370400440532013000"
      },
      "lines": [
        {
          "name": "Beratungsleistung",
          "qty": 10,
          "unit_price": 100,
          "vat": 19
        }
      ]
    }
  }'

A successful response (HTTP 200):

{
  "format": "xrechnung-3.0",
  "profile": "XRechnung 3.0.2 (UBL 2.1)",
  "xml": "<?xml version=\"1.0\" encoding=\"UTF-8\"?><Invoice …",
  "validation": {
    "valid": true,
    "tier": "schematron",
    "counts": { "errors": 0, "warnings": 0 },
    "issues": []
  }
}

Amounts you omit are computed for you (line totals, VAT breakdown, grand total), and sensible defaults are applied — for example currency defaults to EUR and the payment means code is derived from the IBAN. Field names are snake_case.

When validation fails: the 422 report

If the document is not valid, the API responds with HTTP 422 and does not return an invalid document. The body is an RFC 9457 problem-details object carrying a full report. Each issue tells you the rule that fired, the path in your request JSON, what is wrong, and how to fix it:

{
  "type": "https://normbill.com/docs/errors/validation-failed",
  "title": "The invoice is not valid",
  "status": 422,
  "detail": "1 error(s), 0 warning(s). Each issue includes the JSON path and a suggested fix.",
  "report": {
    "valid": false,
    "format": "xrechnung-3.0",
    "profile": "XRechnung 3.0.2 (UBL 2.1)",
    "tier": "schematron",
    "counts": {
      "errors": 1,
      "warnings": 0
    },
    "issues": [
      {
        "severity": "error",
        "rule": "BR-DE-15",
        "path": "invoice.buyer_reference",
        "message": "Buyer reference (BT-10) is missing. XRechnung requires it; public-sector buyers in Germany use their Leitweg-ID here.",
        "fix": "Set invoice.buyer_reference. Leitweg-ID format: 04011000-1234512345-06.",
        "docs_url": "https://normbill.com/docs/rules/br-de-15",
        "location": "/ubl:Invoice/cbc:BuyerReference"
      }
    ]
  }
}
  • report.tier — how far validation got: structural (normbill's own pre-flight rules), schema (XSD), or schematron (the authoritative KoSIT rule set).
  • issues[].path — the field in your request JSON, e.g. invoice.buyer_reference or invoice.lines[0].vat. It is null when a finding cannot be mapped back to an input field.
  • issues[].docs_url — a per-rule page under /docs/rules/<rule-id>.

Quotas and rate-limit headers

Plans include a monthly document allowance (see pricing). Only successful generate and parse calls consume it — failed attempts never count, and /v1/invoices/validate has its own free fair-use allowance of five times your document quota. Every response reports where you stand:

HeaderMeaning
X-RateLimit-LimitYour monthly allowance in the current period.
X-RateLimit-RemainingDocuments left this period.
X-Quota-WarningPresent from 80% usage, so you are warned well before the limit.

On paid plans with metered overage active on the subscription, extra documents beyond the allowance are billed at the per-document rate shown for your tariff — capped at 2× the plan fee by default. Where metered overage is not active, calls stop at the quota with a 429 rather than being billed. The Free plan returns 429 quota-exceeded with an upgrade link.

Retries are safe: POST /v1/invoices/generate accepts an optional Idempotency-Key header (valid for 24 hours). A retry with the same key and body returns the same document and never consumes quota twice.

Test vs. live keys

  • sk_test_ keys have their own allowance of 1000 requests per month and are never billed — they do not count against your plan.
  • Documents generated with a test key carry a visible note marking them as test invoices, so they can't be mistaken for legal documents.
  • sk_live_ keys count against the plan quota and produce production documents.

SDKs & MCP server

Typed clients for both ecosystems, with the same surface as the raw REST API:

npm i @normbill/xrechnung            # TypeScript / Node.js
composer require normbill/xrechnung    # PHP

Both cover generate, validate and parse, and type the 422 validation report so every issue maps to a field in your request. Source and docs: npm · Packagist.

MCP server: AI agents and assistants (Claude, Cursor, …) can call the API directly through @normbill/mcp — tools for generate / validate / parse plus explain_rule, a no-key lookup that returns the documentation for any KoSIT rule id. One-line setup in Claude Code:

claude mcp add normbill --env NORMBILL_API_KEY=sk_test_… -- npx -y @normbill/mcp

API reference

The whole surface is three endpoints — small on purpose. Full request/response schemas live in the OpenAPI spec; every non-2xx response is an RFC 9457 problem-details body (see the error reference).

POST/v1/invoices/generateGenerate a compliant e-invoice

Invoice JSON in, validated document out. Formats: xrechnung-3.0 (UBL, authoritative KoSIT validation — XRechnung API), xrechnung-cii-3.0 (CII / UN/CEFACT D16B, same XRechnung CIUS, authoritative KoSIT validation), peppol-bis-3.0 (UBL, BIS Billing 3.0 CIUS), zugferd-2.x / facturx-1.0 (CII embedded in a hybrid PDF/A-3, returned base64 in the pdf field — paid plans, ZUGFeRD API). ZUGFeRD / Factur-X / Peppol are validated at the structural EN 16931 tier today; the report's tier field states which tier ran.

curl https://api.normbill.com/v1/invoices/generate \
  -H "Authorization: Bearer sk_test_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "format": "xrechnung-3.0",
    "invoice": {
      "number": "INV-2026-0001",
      "issue_date": "2026-06-15",
      "due_date": "2026-07-15",
      "currency": "EUR",
      "buyer_reference": "04011000-1234512345-06",
      "seller": {
        "name": "Muster Lieferant GmbH",
        "vat_id": "DE123456789",
        "address": {
          "country": "DE",
          "city": "Berlin",
          "postal_code": "10115"
        },
        "contact": {
          "name": "Billing",
          "phone": "+49 30 1234567",
          "email": "billing@lieferant.example"
        }
      },
      "buyer": {
        "name": "Beispiel Kunde AG",
        "address": {
          "country": "DE",
          "city": "Hamburg",
          "postal_code": "20095"
        },
        "contact": {
          "email": "ap@kunde.example"
        }
      },
      "payment": {
        "iban": "DE89370400440532013000"
      },
      "lines": [
        {
          "name": "Beratungsleistung",
          "qty": 10,
          "unit_price": 100,
          "vat": 19
        }
      ]
    }
  }'

Returns 200 with xml (+ pdf for ZUGFeRD / Factur-X) and the validation report, or 422 with the full report when the document is invalid.

POST/v1/invoices/validateValidate an existing e-invoice

Validates XML you already have — from your own system or a supplier. XRechnung documents (UBL xrechnung-3.0 and CII xrechnung-cii-3.0) run through the authoritative KoSIT validator. ZUGFeRD / Factur-X CII is checked at the structural EN 16931 tier (tier 1) today. The report's tier field states which one ran.

curl https://api.normbill.com/v1/invoices/validate \
  -H "Authorization: Bearer sk_test_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "content": "<?xml version=\"1.0\"?><Invoice …>…</Invoice>" }'

A valid document returns the report with valid: true; an invalid one returns the same 422 shape as above — every failed rule with its ID, severity, message and a suggested fix. Try it without a key on the free validator.

POST/v1/invoices/parseParse XML to invoice JSON

The receiving side of the mandate: maps an inbound e-invoice back to the normbill invoice model. Both syntaxes are accepted and auto-detected — UBL 2.1 (XRechnung) and CII / UN-CEFACT D16B (ZUGFeRD, Factur-X); the response's format field states which was detected.

curl https://api.normbill.com/v1/invoices/parse \
  -H "Authorization: Bearer sk_test_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "content": "<?xml version=\"1.0\"?><ubl:Invoice …>…</ubl:Invoice>" }'

A successful response (HTTP 200):

{
  "format": "xrechnung-3.0",
  "profile": "XRechnung 3.0.2 (UBL 2.1)",
  "invoice": {
    "number": "INV-2026-0001",
    "issue_date": "2026-06-15",
    "seller": { "name": "Muster Lieferant GmbH", "vat_id": "DE123456789", … },
    "lines": [ … ]
  }
}

Error reference

Every non-2xx response is an RFC 9457 problem-details body whose type URL links to a documented error:

ErrorStatusMeaning
invalid-request400Invalid request body
bad-request400Bad request
payload-too-large413Payload too large
unauthorized401Unauthorized
validation-failed422The invoice is not valid
unparseable-document400The document could not be parsed
plan-limit403Feature not included in your plan
quota-exceeded429Quota exceeded
rate-limited429Too many requests (burst rate limit)
overage-cap-reached429Overage spending cap reached
validate-fair-use429Validation fair-use allowance exhausted
generate-failure-fair-use429Failed-generate fair-use allowance exhausted
idempotency-key-invalid400Invalid Idempotency-Key header
idempotency-key-reuse422Idempotency key reused with a different body
idempotency-conflict409Concurrent request with the same idempotency key
pdf-unavailable503PDF rendering temporarily unavailable
validator-unavailable503Validation service temporarily unavailable
not-ready503Service not ready

Rule reference

Validation findings link to per-rule pages with the mapped JSON field and a concrete fix. The curated rules (grown from real traffic — the KoSIT validator checks many more):

RuleWhat it checks
BR-DE-1Payment instructions (BG-16) are missing. XRechnung requires them.
BR-DE-2Seller contact details (BG-6) are missing. XRechnung requires a contact person or department.
BR-DE-3Seller city (BT-37) is missing.
BR-DE-4Seller postal code (BT-38) is missing.
BR-DE-5Seller contact name (BT-41) is missing.
BR-DE-6Seller contact phone number (BT-42) is missing.
BR-DE-7Seller contact email (BT-43) is missing.
BR-DE-8Buyer city (BT-52) is missing.
BR-DE-9Buyer postal code (BT-53) is missing.
BR-DE-10A deliver-to address (BG-15) is present but its city (BT-77) is missing — XRechnung requires it.
BR-DE-11A deliver-to address (BG-15) is present but its post code (BT-78) is missing — XRechnung requires it.
BR-DE-15Buyer reference (BT-10) is missing. XRechnung requires it; public-sector buyers in Germany use their Leitweg-ID here.
BR-DE-16Seller VAT identifier (BT-31) or tax registration number (BT-32) is required.
BR-DE-17This invoice type code is not recommended in XRechnung.
BR-DE-18A payment-terms line starting with # is not a valid Skonto segment — XRechnung requires the exact format #SKONTO#TAGE=n#PROZENT=n.nn# (optionally #BASISBETRAG=n.nn#), one per line.
BR-DE-23-aPayment means is a credit transfer (code 30/58) but no payment account (IBAN, BT-84) is given.
BR-DE-23-bPayment means is a credit transfer (code 30/58), so direct-debit information (BG-19) must not be present.
BR-DE-25-aPayment means is a SEPA direct debit (code 49/59) but direct-debit information (BG-19) is missing.
BR-DE-25-bPayment means is a SEPA direct debit (code 49/59), so a payee account (BG-17) must not be present.
BR-DE-26A corrected invoice (type_code 384) should reference the invoice it corrects (BT-25).
BR-DE-27The seller contact phone number (BT-42) should contain at least three digits.
BR-DE-30Direct-debit information (BG-19) is present but the creditor identifier (BT-90) is missing.
BR-DE-31Direct-debit information (BG-19) is present but the debited account identifier (BT-91) is missing.
BR-DE-22Two embedded attachments share the same filename — every attached document's filename must be unique within the invoice.
BR-TMP-2An external document location (BT-124) must be an absolute URL with a valid scheme (announced to become an error in a future XRechnung release).
PEPPOL-EN16931-R061A direct-debit payment (means code 49/59) must carry a mandate reference (BT-89).
PEPPOL-EN16931-R046The item net price must equal the gross price minus the price allowance when a gross price is provided.
BR-CO-10The sum of line net amounts (BT-106) does not match the invoice lines.
BR-CO-13Supplied totals.net does not equal the sum of line nets minus document allowances plus document charges.
BR-CO-14Supplied totals.tax does not match the computed VAT breakdown.
BR-CO-15Supplied totals.gross does not equal net + tax.
BR-CO-16The amount due (BT-115) must equal the invoice total with VAT (BT-112) minus the paid amount (BT-113).
BR-CO-25An amount is due but neither a due date (BT-9) nor payment terms (BT-20) are present.
BR-DEC-01A document-level allowance amount (BT-92) has more than 2 decimal places.
BR-DEC-02A document-level allowance base amount (BT-93) has more than 2 decimal places.
BR-DEC-05A document-level charge amount (BT-99) has more than 2 decimal places.
BR-DEC-06A document-level charge base amount (BT-100) has more than 2 decimal places.
BR-DEC-12totals.net (BT-109) has more than 2 decimal places — EN 16931 allows at most 2.
BR-DEC-13totals.tax (BT-110) has more than 2 decimal places — EN 16931 allows at most 2.
BR-DEC-14totals.gross (BT-112) has more than 2 decimal places — EN 16931 allows at most 2.
BR-DEC-24A line-level allowance amount (BT-136) has more than 2 decimal places.
BR-DEC-25A line-level allowance base amount (BT-137) has more than 2 decimal places.
BR-DEC-27A line-level charge amount (BT-141) has more than 2 decimal places.
BR-DEC-28A line-level charge base amount (BT-142) has more than 2 decimal places.
BR-S-02The invoice has standard-rated (S) lines, which require the seller VAT identifier (BT-31) or tax registration number (BT-32).
BR-S-05A standard-rated (S) line must have a VAT rate greater than zero.
BR-Z-05A zero-rated (Z) line must have a VAT rate of exactly 0.
BR-E-05A VAT-exempt (E) line must have a VAT rate of 0.
BR-AE-05A reverse-charge (AE) line must have a VAT rate of 0.
BR-AE-02Reverse charge (AE) requires a seller VAT identifier (BT-31) or tax registration number (BT-32), and the buyer VAT identifier (BT-48).
BR-27The item net price (BT-146) must not be negative.
BR-E-10Lines use VAT category "E" (exempt) but no exemption reason (BT-120) or reason code (BT-121) is given.
BR-AE-10Lines use VAT category "AE" (reverse charge) but no exemption reason (BT-120) or reason code (BT-121) is given.
BR-IC-10Lines use VAT category "K" (intra-community supply) but no exemption reason (BT-120) or reason code (BT-121) is given.
BR-G-10Lines use VAT category "G" (export outside the EU) but no exemption reason (BT-120) or reason code (BT-121) is given.
BR-O-10Lines use VAT category "O" (not subject to VAT) but no exemption reason (BT-120) or reason code (BT-121) is given.
BR-O-05A line "not subject to VAT" (O) must not carry a VAT rate.
BR-O-11VAT category "O" (not subject to VAT) cannot be mixed with other VAT categories on the same invoice (BR-O-11..14).
BR-32A document-level allowance needs a VAT category (BT-95); it cannot be inherited because the lines use more than one VAT category/rate.
BR-37A document-level charge needs a VAT category (BT-102); it cannot be inherited because the lines use more than one VAT category/rate.
BR-33A document-level allowance must state why it is granted (BT-97/BT-98).
BR-38A document-level charge must state what it is for (BT-104/BT-105).
BR-42A line-level allowance must state why it is granted (BT-139/BT-140).
BR-44A line-level charge must state what it is for (BT-144/BT-145).
NB-AC-MISMATCHAn allowance/charge amount does not equal base_amount × percent / 100 — the KoSIT validator rejects this (PEPPOL-EN16931-R040).
NB-LEITWEG-FORMATbuyer_reference does not look like a Leitweg-ID. German B2G platforms (ZRE/OZG-RE) reject malformed Leitweg-IDs. For B2B invoices any buyer reference is fine — ignore this warning then.
NB-EADDR-SELLERSeller electronic address (BT-34) is missing — XRechnung requires one for both parties.
NB-EADDR-BUYERBuyer electronic address (BT-49) is missing — XRechnung requires one for both parties.
NB-IBAN-CHECKSUMThe IBAN fails its checksum — this is almost certainly a typo.
NB-VAT-RATEA line has neither a VAT rate nor a VAT category — the category cannot be determined.
NB-PAYMENT-MEANS-UNSUPPORTEDCard-payment means codes (48/54/55) need card information (BG-18) that normbill cannot express yet — KoSIT would reject the invoice (BR-DE-24-a).
NB-VAT-ID-FORMATA German VAT identifier (BT-31/BT-48) must be "DE" followed by exactly 9 digits. EN 16931 and the KoSIT validator only check the ISO country prefix, not the national length, so a German id with the wrong number of digits still passes tier 3 — normbill catches it here.
NB-VALIDATOR-REJECTThe official KoSIT validator rejected this document without a mappable rule finding — typically an XML schema (XSD) violation.
BR-CO-26The seller carries none of: VAT identifier (BT-31), legal registration identifier (BT-30) or seller identifier (BT-29) — at least one is required so the buyer can identify the supplier. The German tax number (BT-32) alone does NOT satisfy this rule.
BR-IC-11An intra-community supply (VAT category K) must state the actual delivery date (BT-72) or an invoicing period (BG-14).
BR-IC-12An intra-community supply (VAT category K) must state the deliver-to country (BT-80).
BR-29The invoicing period end date (BT-74) must not be earlier than its start date (BT-73).
BR-30An invoice line period end date (BT-135) must not be earlier than its start date (BT-134).
PEPPOL-EN16931-R110An invoice line period start date must lie within the document invoicing period.
PEPPOL-EN16931-R111An invoice line period end date must lie within the document invoicing period.
BR-CL-01Type code 381 (credit note) is not allowed in the UBL Invoice syntax — UNTDID 1001 lists 381 only for UBL CreditNote documents, so the KoSIT validator rejects it.
BR-IC-05An intra-community supply (K) line must have a VAT rate of exactly 0.
BR-G-05An export (G) line must have a VAT rate of exactly 0.
BR-CL-04The invoice currency code (BT-5) is not part of ISO 4217.
BR-CL-14A country code in the invoice is not an ISO 3166-1 alpha-2 code.
BR-CL-15The item country of origin (BT-159) is not an ISO 3166-1 alpha-2 code.
BR-CL-16The payment means code (BT-81) is not a UNTDID 4461 code.
BR-CL-19An allowance reason code (BT-98 document level, BT-140 line level) is not part of the UNCL 5189 subset EN 16931 permits.
BR-CL-20A charge reason code (BT-105 document level, BT-145 line level) is not a UNTDID 7161 code.
BR-CL-22A VAT exemption reason code (BT-121) is not part of the VATEX code list.
BR-CL-25An electronic address scheme (BT-34 seller, BT-49 buyer — the schemeID on cbc:EndpointID) is not part of the EAS code list.

Interactive API reference

The full request/response schemas are published as an OpenAPI 3.1 document generated directly from the runtime contracts — the reference cannot drift from what the API actually validates:

  • Interactive reference — every schema, browsable, with request builders
  • /openapi.json — the raw OpenAPI spec, ready for codegen (also served by the API at https://api.normbill.com/openapi.json)
  • Postman collection — generate / validate / parse (import into Postman or Insomnia)
  • Bruno OpenAPI import — point Bruno at /openapi.json (or the YAML stub) for the same contract

Questions, missing docs, or a rule that needs a better explanation? Email support@normbill.com.

Spotted something?

Bug, missing feature, wrong validation result — tell us. The page you're on is attached automatically; email optional.