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
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 fieldTypeMeaning
xmlstringThe invoice XML as a JSON string — not raw XML, not a file upload. Required. At most 10 MB.
fileNamestringStored with the validation record.
webhookUrlstringCalled with the result when set (validation.completed / validation.failed).
options.ruleset_versionsobjectPin 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
200 · rules applied, findings listed
{
  "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.*TypeMeaning
validationRefstringReference of this run; use it for an evidence pack.
isValidbooleanNo errors from the rules and the XSD/Schematron gate agrees.
format / formatVersionstring | nullDetected syntax, e.g. ubl 2.1 or cii.
rulesetsApplied[]objectWhich rulesets actually ran, with version, pinned, latestAvailable and a deprecationWarning when relevant.
errors[] / warnings[]objectcode, message, location (XPath), and for errors a suggestion.
metadataobjectInvoice 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.

Request
{
  "invoices": [
    { "id": "inv-1", "xml": "<Invoice>…</Invoice>", "fileName": "inv-1.xml" },
    { "id": "inv-2", "xml": "<Invoice>…</Invoice>" }
  ]
}
Errors
StatusCodeWhen
400INVALID_JSONThe body is not valid JSON.
400MISSING_XMLThe "xml" field is missing.
400INVALID_XMLThe "xml" field is not a string.
400XML_TOO_LARGEThe XML is larger than 10 MB.
401UNAUTHORIZEDNo Bearer header, or the key is unknown.
401KEY_EXPIREDAn instant test key past its hour.
402QUOTA_EXCEEDEDMonthly quota used up, or the 10 instant-key requests. details.upgradeUrl points to the Team checkout.
429RATE_LIMITEDToo 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
Node 18+
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))
Python · requests
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"]])