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.
| Endpoint | Does | Key |
|---|---|---|
| Validate | ||
| POST /v3/validate | Official verdict with structured findings | any key |
| Fix | ||
| POST /v3/fix | Repair with the patch engine; answer open questions in the same call | any key |
| Convert | ||
| POST /v3/convert | Convert to another syntax / CIUS, with the verdict of the output | any key |
| Render | ||
| POST /v3/render | Complete human-readable rendering (every BT of the XML) | any key |
| Meta | ||
| GET /v3/health | Engine and validator status (no key) | no key |
| GET /v3/openapi.json | This 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).
Key, then fix
# a one-hour, 10-request test key, no account needed curl -s -X POST https://www.invoicenavigator.eu/api/developers/instant-key
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]}'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 field | Type | Meaning |
|---|---|---|
| xml | string | UBL 2.1 or CII D16B as text. At most 10 MB. |
| xmlBase64 | string | Instead 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. |
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 contentxml 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.
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.
{
"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.
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.
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"]])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 -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.htmlv2 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.