How to add e-invoice compliance to your pipeline.
Between the ERP export and the delivery channel, one call decides whether the file goes out, goes back, or waits for a person. This guide wires that call in, with the code, in five steps.
| Stage | System | Carries |
|---|---|---|
| Source | Your ERP | UBL or CII XML, exported per invoice or per batch. |
| Check | Invoice Navigator API | Validate → patch structure → re-validate → sign. One POST, one protocol. |
| Delivery | Peppol access point, portal, archive | The file that passed, or the reason it did not. |
Your pipeline logic stays yours; the API only answers about the file it was given.
- 01
Get a key
For development, an instant test key needs no account (10 requests, one hour). For production, sign up: a
sk_live_and ask_test_key are created together.curl -X POST https://www.invoicenavigator.eu/api/developers/instant-key
- 02
Validate
POST /v1/validate runs EN 16931, Peppol BIS and the country rules that apply to the file and reports without changing it. Use it where you only need a verdict.
JavaScript const res = await fetch('https://www.invoicenavigator.eu/api/v1/validate', { method: 'POST', headers: { Authorization: 'Bearer sk_test_…', 'Content-Type': 'application/json' }, body: JSON.stringify({ xml: invoiceXml, fileName: 'invoice-001.xml' }), }) const { data } = await res.json() console.log(data.isValid, data.errors.length, data.warnings.length) for (const e of data.errors) console.log(e.code, e.location, e.message)Python r = requests.post( "https://www.invoicenavigator.eu/api/v1/validate", headers={"Authorization": "Bearer sk_test_…"}, json={"xml": invoice_xml, "fileName": "invoice-001.xml"}, ) data = r.json()["data"] print(data["isValid"], len(data["errors"])) for e in data["errors"]: print(e["code"], e.get("location"), e["message"]) - 03
Validate, patch, re-validate
POST /v2/validate-and-fix does the whole loop. The response sorts every finding into one of three outcomes, and your code branches on exactly those:
fixesApplied— structural edits were made and the patched file passed re-validation. Deliverdata.fixedXml.fixSummary.needsInput— business data is missing. Collect it and POST todata._links.fixWithInput; the engine never guesses.remainingIssuesafter patching — the source system must re-export. Amounts, VAT totals and payment details are never altered.
JavaScript const res = await fetch('https://www.invoicenavigator.eu/api/v2/validate-and-fix', { method: 'POST', headers: { Authorization: 'Bearer sk_live_…', 'Content-Type': 'application/json' }, body: JSON.stringify({ xml: invoiceXml, fileName: 'invoice-001.xml', autoFix: true }), }) const { data } = await res.json() if (data.originalValid) deliver(invoiceXml) else if (data.fixedValid) deliver(data.fixedXml) // patched, re-validated else if (data.fixSummary?.needsInput) askOperations(data) // POST values to data._links.fixWithInput else reexport(data.remainingIssues) // structural failure at the sourcePython r = requests.post( "https://www.invoicenavigator.eu/api/v2/validate-and-fix", headers={"Authorization": "Bearer sk_live_…"}, json={"xml": invoice_xml, "fileName": "invoice-001.xml", "autoFix": True}, ) data = r.json()["data"] if data["originalValid"]: deliver(invoice_xml) elif data.get("fixedValid"): deliver(data["fixedXml"]) elif (data.get("fixSummary") or {}).get("needsInput"): ask_operations(data) # POST values to data["_links"]["fixWithInput"] else: reexport(data["remainingIssues"]) - 04
Keep the evidence
Store
validationRefwith the invoice record. On Team, POST /v1/evidence-pack turns it into a signed pack (PDF or JSON) that anyone can verify later at GET /v1/verify/{'{id}'} or on /verify, without a key. The composite call can inline the pack withgenerate_evidence_pack: true.JavaScript // Team keys. The pack is bound to a validationRef from /v1/validate or /v2/validate-and-fix. const pack = await fetch('https://www.invoicenavigator.eu/api/v1/evidence-pack', { method: 'POST', headers: { Authorization: 'Bearer sk_live_…', 'Content-Type': 'application/json' }, body: JSON.stringify({ validationRef: data.validationRef, format: 'json' }), }).then((r) => r.json()) // Anyone can check it later, without a key: const check = await fetch(`https://www.invoicenavigator.eu/api/v1/verify/${pack.data.evidencePackId}`).then((r) => r.json()) console.log(check.verified)Python pack = requests.post( "https://www.invoicenavigator.eu/api/v1/evidence-pack", headers={"Authorization": "Bearer sk_live_…"}, json={"validationRef": data["validationRef"], "format": "json"}, ).json() check = requests.get(f"https://www.invoicenavigator.eu/api/v1/verify/{pack['data']['evidencePackId']}").json() print(check["verified"]) - 05
Go live
- Swap
sk_test_forsk_live_in the environment; nothing else changes. - Handle
429withRetry-Afterand5xxwith backoff; never retry other 4xx. - Route
needsInputto a person or a form, andremainingIssuesback to the source system. - Log
X-Request-Idon every call; watch the month counter on GET /v1/usage. - Test with your own invoices for your main trading partners before the switch, and confirm your access point accepts the patched output.
- Swap
- Can I test without an account or without touching my quota?
- Yes. POST https://www.invoicenavigator.eu/api/developers/instant-key returns a sk_test_ key without an account (10 requests, one hour). Every account also gets a sk_test_ key next to its live key; test keys run the same engine, are not metered, and are limited to 100 requests an hour.
- Which invoice formats does the API accept?
- UBL 2.1 and UN/CEFACT CII XML, including the national profiles built on them such as XRechnung and Peppol BIS Billing 3.0. Format detection is automatic; send the XML as a JSON string.
- What does the engine change, and what does it never change?
- It patches structure: syntax, code lists, ordering, identifiers, missing containers. It never changes amounts, VAT totals or payment details; when business data is missing it reports needsInput and waits for your values.
- What happens if the API is unavailable?
- Calls fail with a 5xx and an X-Request-Id. Retry with exponential backoff, keep the invoice in your own queue until it validates, and store the validationRef and evidence pack id with your invoice record.
- Do I need separate keys per environment?
- Use the sk_test_ key in development and CI and the sk_live_ key in production. Only live calls are metered and stored as flows.
Free: 100 requests a month, no card. Team: €49 a month for 5,000 requests, batch, evidence packs and conversion. Details on /developers.