Developers · API v3 · OpenAPI 3.1.0

The engine the tool runs, as an API.

v3 repairs with the patch engine: targeted edits on your own document, never a rebuild, and every result is checked by the official validators. Same engine, validator set and loss guard as the web tool. No AI model is called, and the invoices you send are not stored.

Endpoints
EndpointDoesKey
Validate
POST /v3/validateOfficial verdict with structured findingsany key
Fix
POST /v3/fixRepair with the patch engine; answer open questions in the same callany key
Convert
POST /v3/convertConvert to another syntax / CIUS, with the verdict of the outputany key
Render
POST /v3/renderComplete human-readable rendering (every BT of the XML)any key
Meta
GET /v3/healthEngine and validator status (no key)no key
GET /v3/openapi.jsonThis document (no key)no key

Base URL https://www.invoicenavigator.eu/api/v3. Same keys, limits and quota as v1/v2 (one request each); sk_test_ keys are not metered. Spec: openapi.json (OpenAPI 3.1).

Try it

Key, then fix

curl · test key
# a one-hour, 10-request test key, no account needed
curl -s -X POST https://www.invoicenavigator.eu/api/developers/instant-key
curl · fix
curl -s -X POST https://www.invoicenavigator.eu/api/v3/fix \
  -H "Authorization: Bearer sk_test_…" \
  -H "Content-Type: application/json" \
  -d "$(jq -n --arg b "$(base64 < invoice.xml | tr -d '\n')" '{xmlBase64: $b}')" \
  | jq '.data | {outcome, valid, remaining: [.remaining[].ruleId], questions: [.questions[].id]}'
Fix

Repair, answer, repeat

POST /v3/fix returns outcome — already_compliant, compliant, partial, needs_input or lossy_refused — the repaired xml, the patches applied to your document, the verdict on the result, the remaining findings and the questions only you can answer. Send the answers back with the same xml; they are written into your document before the repair runs.

Body fieldTypeMeaning
xmlstringUBL 2.1 or CII D16B as text. At most 10 MB.
xmlBase64stringInstead of xml: the file's bytes, base64 — on every endpoint. Read the way the receiver's XML parser reads them (byte order mark, declared encoding, ISO-8859-1); bytes that contradict their encoding get 422 XML_ENCODING_CONFLICT with the byte offset. Use it when you read the invoice from a file.
mode"strict" | "permissive"Default strict: a value the engine is not sure of becomes a question. permissive fills it with the best inference.
answers[]object{ id, value, scheme? } for a field question, { id, option } for a choice. Unknown ids come back in data.answers.rejected.
TypeScript
const res = await fetch('https://www.invoicenavigator.eu/api/v3/fix', {
  method: 'POST',
  headers: { Authorization: `Bearer ${process.env.INVOICE_NAVIGATOR_KEY}`, 'Content-Type': 'application/json' },
  body: JSON.stringify({ xml }),
})
const body = await res.json()
if (!body.success) throw new Error(body.error.code)          // 503 ORACLE_UNAVAILABLE: retry
const fix = body.data

if (fix.outcome === 'needs_input' || fix.questions.length > 0) {
  // ask your user (labels come in EN and DE), then send the answers back with the same xml
  const answers = fix.questions.map((q) =>
    q.kind === 'choice' ? { id: q.id, option: q.options[0].value } : { id: q.id, value: lookUp(q.id) },
  )
  // POST /fix again with { xml, answers }
}
if (fix.valid) await save(fix.xml)                           // fix.xml is null if it would lose content

xml is null when the result would lose a value of your document (fidelity.critical > 0); that has not happened on the 37-invoice corpus gate.

Validate

Findings you can act on

POST /v3/validate runs the official validator set for the detected format (XSD, CEN EN 16931 schematron, KoSIT for XRechnung, Peppol and EN 16931) and returns each finding with the business terms it names, its location, the validator's message, German text where we have it, and the action that resolves it.

data.findings[]
{
  "ruleId": "BR-DE-15",
  "severity": "error",
  "oracle": "xrechnung-cius-3.0-ubl",
  "bt": ["BT-10"],
  "xpath": "…",
  "message": { "en": "[BR-DE-15] … (BT-10) …", "de": "…" },
  "plain": { "en": "German invoices must include a buyer reference (Leitweg-ID for public buyers).", "de": "…" },
  "action": { "kind": "ask", "fields": [{ "id": "buyer_reference", "label": { "en": "Buyer reference", "de": "…" }, "xpath": "Invoice/cbc:BuyerReference", … }] }
}

data.lineArithmetic[]: line amounts that don't add up — quantity × net price ≠ line amount, or net price ≠ gross price − discount — with the line, expected and actual. The official CEN check does not test line arithmetic, so an invoice can pass it with these. They also appear in findings as warnings; they never change valid and never count as errors.

Convert

Another syntax or CIUS

POST /v3/convert with a target: xrechnung-ubl, xrechnung-cii, peppol-bis, en16931-ubl, en16931-cii. Same syntax (e.g. Peppol UBL → XRechnung UBL) only changes the document identity, so nothing can be lost. UBL ↔ CII is rebuilt from the canonical model and checked term by term; if a value would be lost the call answers 422 CONVERSION_LOSSY with the list, unless you send allowLoss: true. Planned: nl-si-ubl, cius-ro, fatturapa, ksef-fa3 — today a 422 UNSUPPORTED_TARGET listing what is supported.

Python
import requests

r = requests.post(
    "https://www.invoicenavigator.eu/api/v3/convert",
    headers={"Authorization": f"Bearer {key}"},
    json={"xml": open("invoice.xml").read(), "target": "xrechnung-ubl"},
    timeout=120,
)
body = r.json()
if r.status_code == 422 and body["error"]["code"] == "CONVERSION_LOSSY":
    print("would lose:", [(t["bt"], t["value"]) for t in body["error"]["details"]["loss"]["terms"]])
else:
    data = body["data"]
    print(data["method"], data["verdict"]["state"], [f["ruleId"] for f in data["findings"]])
Render

Every field, readable

POST /v3/render returns a standalone HTML page (format: "html", default) or the structured reading ("json"), in English or German: every business term of the XML under its own label, plus anything outside EN 16931. PDF is not available yet.

curl
curl -s -X POST https://www.invoicenavigator.eu/api/v3/render \
  -H "Authorization: Bearer sk_test_…" -H "Content-Type: application/json" \
  -d "$(jq -n --rawfile xml invoice.xml '{xml: $xml, locale: "de"}')" \
  | jq -r .data.html > invoice.html
Versioning

v2 is deprecated for new integrations

v1 and v2 keep working. Their repair endpoints (/v2/validate-and-fix, /v1/fixer/fix) still run the earlier engine, which rebuilds the document; v3 patches it. New integrations should use v3. We will not switch v2 off without at least six months notice by e-mail to key owners and on the changelog. Within v3, changes are additive; a breaking change gets a new path (/api/v4).

Status: GET /v3/health reports the engine and whether the official validator is reachable. When it is not, validate, fix and convert answer 503 ORACLE_UNAVAILABLE with Retry-After — never a pass. A document a validator cannot evaluate at all (it breaks the rule engine) gets 422 NOT_EVALUABLE naming the validator and its error; a file that is not well-formed XML gets 422 MALFORMED_XML.