Error Handling
Every failed request returns a structured JSON error you can act on programmatically. This page covers the response envelope, the error categories, and how to handle them in code.
status for the HTTP outcome, branch on errorCode for the category, and you have handled every error the API can return.
The error envelope
Every error response, on every endpoint, contains the same top-level fields:
| Field | Type | Description |
|---|---|---|
| type | string | RFC 7807 type URI for the error class. |
| title | string | Short, human-readable summary of the error class. |
| status | int | The HTTP status code, repeated in the body for convenience. |
| errorCode | int | The InvoiceXML category code. This is the one value to branch on. |
| detail | string | Longer human-readable explanation, safe to show to end users. |
| errors | array | An array of finding objects, one per validation error. Present on validation failures, and identical in shape whether the failure is XML validation (4001) or model validation (4002). |
| warnings | array | Non-blocking findings, same object shape as a 4001 finding. Advisory only, they never cause a request to fail. |
Error codes
The errorCode identifies the category of error. Branch on it to decide how to handle the response. The errors column shows what, if anything, the errors field carries for that code.
| errorCode | Name | Description | errors |
|---|---|---|---|
| 4000 | General Error | An unclassified processing error. Check title and detail for information. |
none |
| 4001 | XML Validation | The invoice XML violates EN 16931 Schematron business rules (e.g. BR-01, BR-CO-15). Each violation is a finding object. | errors[] |
| 4002 | Model Validation | One or more request fields are missing or malformed before processing could start. | errors[] |
| 4003 | Missing File | A multipart endpoint expected a file upload but none was provided. Include the file in the request. | none |
| 4004 | Unsupported Content Type | The uploaded file's content type or extension is not accepted by that endpoint (for example, a non-PDF where a PDF is required). | none |
| 4005 | File Too Large | The uploaded file exceeds the 20 MB size limit. | none |
| 4006 | No Embedded XML | The PDF does not contain an embedded Factur-X / ZUGFeRD XML attachment. Returned by /v1/extract/*, /v1/validate/*, and /v1/convert/* when no XML is present. To read a PDF without embedded XML, use /v1/parse/json. |
none |
| 4007 | PDF Error | The PDF could not be processed. It may be corrupted, password-protected, or not a valid invoice PDF. | none |
| 4008 | Not an Invoice | The uploaded document does not appear to be an invoice. Returned by AI-powered endpoints (e.g. /v1/parse/json, /v1/transform/to/*) when classification rejects the input. |
none |
| 4009 | Multiple Invoices | The uploaded PDF contains more than one invoice. Split it and resubmit one invoice per file. | none |
| 4010 | Unauthorized | The Authorization header is missing, malformed, or carries an unknown / revoked API key. Re-check the bearer token and retry. |
none |
| 4011 | Remote Fetch Error | A pdfUrl supplied to /v1/create/facturx or /v1/create/zugferd could not be fetched: it was not a valid http/https URL, resolved to a blocked address, or the host was unreachable. Used only by the create endpoints' optional pdfUrl input. |
none |
Example responses
The envelope is identical in all of them. Both validation categories (4001 and 4002) use the very same errors array of finding objects, so you handle them with one code path. Every other error code carries no errors field at all.
XML validation, errorCode 4001
The generated or uploaded XML violates EN 16931 Schematron rules. errors is an array of finding objects, one per violated rule.
{
"type": "https://tools.ietf.org/html/rfc9110#section-15.5.1",
"title": "Validation Failed",
"status": 400,
"errorCode": 4001,
"detail": "The invoice contains 2 validation error(s).",
"errors": [
{
"rule": "BR-01",
"layer": "en16931",
"line": null,
"message": "The invoice is missing a specification identifier (BT-24).",
"btCodes": ["BT-24"],
"fields": ["specificationId"],
"raw": "[BR-01] EN16931: An invoice shall have a specification identifier."
},
{
"rule": "PEPPOL-EN16931-R010",
"layer": "cius",
"line": null,
"message": "Peppol BIS requires a buyer electronic address. Add the buyer's electronic address (a buyer VAT identifier is used as a fallback), or use the plain en16931 profile if the invoice is not meant for Peppol.",
"btCodes": ["BT-49"],
"fields": ["buyer.electronicAddress"],
"raw": "[PEPPOL-EN16931-R010] CIUS: Buyer electronic address MUST be provided"
}
],
"warnings": []
}
Each finding is self-describing. Use message for end-user display and raw for debugging or logs. The btCodes and fields arrays let you map a finding back to a form input.
| Field | Type | Description |
|---|---|---|
| rule | string | The validated rule identifier, e.g. BR-01. |
| layer | string | null | Validation layer that produced the finding: xsd (structure), en16931 (European core rules), or cius (the profile overlay: Peppol BIS, XRechnung, NLCIUS, PINT, or the Factur-X/ZUGFeRD profile rules). null on 4002 findings, which have no validation layer. |
| line | int | null | 1-based invoice line number, or null for a document-level finding. |
| message | string | Plain-language explanation, safe to show end users. |
| btCodes | string[] | EN 16931 Business Term codes the rule references. |
| fields | string[] | Dotted JSON paths into the invoice the finding maps to. Line-scoped paths use a zero-based index (line 3 maps to lines[2]). |
| raw | string | Verbatim validator output, including the rule id, layer and XPath location. |
Map findings straight to your form.
Key your invoice-form inputs by the same dotted paths (or keep a lookup from BT code to input). On an error response, match each finding'sfields or btCodes to the matching input, mark it invalid, and show the friendly message beside it. The user is taken straight to the field that needs fixing, with no XPath decoding.
Model validation, errorCode 4002
A request field is missing or malformed, so processing never started. errors is the exact same flat array of finding objects as 4001. rule, layer and raw are null (model validation has no Schematron rule or validation layer), while btCodes and fields are resolved from the field path, so the finding maps to the offending input by both BT code and JSON path.
{
"type": "https://tools.ietf.org/html/rfc9110#section-15.5.1",
"title": "One or more validation errors occurred.",
"status": 400,
"errorCode": 4002,
"valid": false,
"detail": "The request failed validation with 2 error(s).",
"errors": [
{
"rule": null,
"layer": null,
"line": null,
"message": "The seller name is required.",
"btCodes": ["BT-27"],
"fields": ["seller.name"],
"raw": null
},
{
"rule": null,
"layer": null,
"line": 1,
"message": "Quantity must be greater than zero.",
"btCodes": ["BT-129"],
"fields": ["lines[0].quantity"],
"raw": null
}
],
"warnings": []
}
Other errors, errorCode 4000 and 4003 to 4011
Missing-file, unsupported content type, file-size, no-embedded-XML, PDF-processing and remote-fetch errors carry no errors field. The title and detail tell you what happened.
{
"type": "https://tools.ietf.org/html/rfc9110#section-15.5.1",
"title": "Missing File",
"status": 400,
"errorCode": 4003,
"detail": "No file was provided in the request."
}
warnings array uses the exact same finding shape as 4001 errors, but it is advisory. A response can be successful and still carry warnings. Only entries in errors make a request fail.
Handling errors in code
Parse the JSON body and branch on errorCode. Because both validation categories share the same errors array, they need just one branch between them; every other code falls through to a generic handler.
const response = await fetch(apiUrl, { method: 'POST', body: formData });
if (!response.ok) {
const problem = await response.json();
switch (problem.errorCode) {
case 4001: // XML validation, Schematron rule violations
case 4002: // Model validation, malformed request fields
// Both return the same flat array of finding objects.
problem.errors.forEach(f => {
const where = f.line != null ? `line ${f.line}` : 'document';
console.error(`${where}: ${f.message}`);
// f.fields and f.btCodes map the finding to a form input
});
break;
default: // 4000, 4003 to 4011: no errors array, use title + detail
console.error(`${problem.title}: ${problem.detail}`);
}
}