Developers · Reference · v1
Validate without changing anything.
POST /v1/validate runs the rules and reports. It never patches. When you want the file fixed and re-validated in the same call, use POST /v2/validate-and-fix.
Request
curl -X POST https://www.invoicenavigator.eu/api/v1/validate \
-H "Authorization: Bearer sk_live_…" \
-H "Content-Type: application/json" \
-d "$(jq -n --rawfile xml invoice.xml '{xml: $xml, fileName: "invoice.xml"}')"| Body field | Type | Meaning |
|---|---|---|
| xml | string | The invoice XML as a JSON string — not raw XML, not a file upload. Required. At most 10 MB. |
| fileName | string | Stored with the validation record. |
| webhookUrl | string | Called with the result when set (validation.completed / validation.failed). |
| options.ruleset_versions | object | Pin a ruleset, e.g. { "de-xrechnung": "3.0.2" }. The response says whether the pin took. |
Sending Content-Type: application/xml with a raw document returns 400 INVALID_JSON. The XML goes inside the JSON body.
Response
{
"success": true,
"data": {
"validationRef": "VAL-…",
"isValid": false,
"format": "ubl",
"formatVersion": "2.1",
"rulesetsApplied": [
{ "id": "en16931", "version": "…", "pinned": false, "source": "validator_execution" }
],
"errors": [
{ "code": "BR-DE-15", "message": "…", "location": "/Invoice/cbc:BuyerReference", "suggestion": "…" }
],
"warnings": [],
"metadata": { "invoiceNumber": "…", "issueDate": "…", "currency": "EUR", "sellerCountry": "DE" }
},
"meta": { "validationRef": "VAL-…", "processingTimeMs": 987 }
}| data.* | Type | Meaning |
|---|---|---|
| validationRef | string | Reference of this run; use it for an evidence pack. |
| isValid | boolean | No errors from the rules and the XSD/Schematron gate agrees. |
| format / formatVersion | string | null | Detected syntax, e.g. ubl 2.1 or cii. |
| rulesetsApplied[] | object | Which rulesets actually ran, with version, pinned, latestAvailable and a deprecationWarning when relevant. |
| errors[] / warnings[] | object | code, message, location (XPath), and for errors a suggestion. |
| metadata | object | Invoice number, dates, currency, seller and buyer names, VAT ids, countries, totals — as read from the file. |
A failing invoice is still 200 with success: true. Look each code up under /errors.
Batch
POST /v1/validate/batch
Up to 50 invoices per request, each with its own id; the response lists one result per id plus validCount and invalidCount. Team keys only.
{
"invoices": [
{ "id": "inv-1", "xml": "<Invoice>…</Invoice>", "fileName": "inv-1.xml" },
{ "id": "inv-2", "xml": "<Invoice>…</Invoice>" }
]
}Errors
| Status | Code | When |
|---|---|---|
| 400 | INVALID_JSON | The body is not valid JSON. |
| 400 | MISSING_XML | The "xml" field is missing. |
| 400 | INVALID_XML | The "xml" field is not a string. |
| 400 | XML_TOO_LARGE | The XML is larger than 10 MB. |
| 401 | UNAUTHORIZED | No Bearer header, or the key is unknown. |
| 401 | KEY_EXPIRED | An instant test key past its hour. |
| 402 | QUOTA_EXCEEDED | Monthly quota used up, or the 10 instant-key requests. details.upgradeUrl points to the Team checkout. |
| 429 | RATE_LIMITED | Too many requests in the sliding hour. Retry-After is set. |
Full list, headers and retry guidance in the API reference. Free keys: 60 requests an hour, 100 a month.
Examples
const res = await fetch('https://www.invoicenavigator.eu/api/v1/validate', {
method: 'POST',
headers: { Authorization: 'Bearer sk_live_…', 'Content-Type': 'application/json' },
body: JSON.stringify({ xml: invoiceXml }),
})
const { success, data, error } = await res.json()
if (!success) throw new Error(error.code)
console.log(data.isValid, data.errors.map((e) => e.code))r = requests.post(
"https://www.invoicenavigator.eu/api/v1/validate",
headers={"Authorization": "Bearer sk_live_…"},
json={"xml": invoice_xml},
)
body = r.json()
data = body["data"]
print(data["isValid"], [e["code"] for e in data["errors"]])