UBL Validation API Reference
Technical reference for validating UBL 2.1 invoices and credit notes. The endpoint runs the OASIS UBL 2.1 XSD, the EN 16931 Schematron, and the CIUS overlay the document declares in BT-24: Peppol BIS Billing 3.0, NLCIUS, XRechnung, or PINT. Every finding is returned as structured JSON with rule id, layer, friendly message, and field paths.
https://api.invoicexml.com/v1/validate/ubl
Code Example
curl -X POST https://api.invoicexml.com/v1/validate/ubl \ -H "Authorization: Bearer YOUR_API_KEY" \ -F "[email protected]"
The invoice profile and version are detected automatically from the uploaded file, so you never need to pass them. The detected values are returned in the response.
Try it out online, no coding required
Upload an invoice and get a full compliance report instantly, right in your browser.
Request
| Parameter | Type | Description |
|---|---|---|
| file * | binary | The invoice file to validate. Accepted format: standalone UBL 2.1 XML (Invoice or CreditNote). PDFs are not accepted on this endpoint. |
Content-Type: multipart/form-data
That is the whole request. The syntax and the conformance profile (Peppol BIS, XRechnung, NLCIUS, PINT,
or the Factur-X/ZUGFeRD profile ladder) are detected automatically from the document's specification
identifier (BT-24), the matching rule set is applied, and the response reports what was validated against
in data.profile and data.customizationId.
Headers
| Header | Value |
|---|---|
| Authorization * | Bearer YOUR_API_KEY |
| Content-Type | multipart/form-data |
Response
200 Valid invoice
The invoice passed every check. valid is true and errors is empty.
{
"valid": true,
"detail": "Your invoice is ubl compliant and meets the EN 16931 specifications.",
"data": {
"schemaValid": true,
"schematronValid": true,
"conformanceLevel": "EN16931",
"profile": "en16931",
"customizationId": "urn:cen.eu:en16931:2017"
},
"errors": [],
"warnings": [],
"report": [
{ "code": "BT-1", "name": "Invoice number", "section": "header", "path": "invoiceNumber", "value": "INV-2026-001", "isValid": true, "errors": [], "warnings": [] },
{ "code": "BT-2", "name": "Invoice issue date", "section": "header", "path": "issueDate", "value": "2026-05-19", "isValid": true, "errors": [], "warnings": [] }
// ... one row per EN 16931 Business Term. See the Validation Report section below.
]
}
200 Invalid invoice
Validation still returns HTTP 200. valid is false and the findings are in the errors array.
{
"valid": false,
"detail": "Validation failed with 3 error(s)",
"data": {
"schemaValid": true,
"schematronValid": false,
"conformanceLevel": "EN16931",
"profile": "en16931",
"customizationId": "urn:cen.eu:en16931:2017"
},
"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. (at /*:CrossIndustryInvoice)"
},
{
"rule": "BR-06",
"layer": "en16931",
"line": null,
"message": "The seller must have a name (BT-27). Add the seller's name.",
"btCodes": ["BT-27"],
"fields": ["seller.name"],
"raw": "[BR-06] EN16931: An invoice shall contain the seller name. (at /*:CrossIndustryInvoice/*:SupplyChainTradeTransaction/*:ApplicableHeaderTradeAgreement/*:SellerTradeParty)"
},
{
"rule": "BR-23",
"layer": "en16931",
"line": 2,
"message": "Line item 2: Each invoice line must have a net amount.",
"btCodes": ["BT-131"],
"fields": ["lines[1].lineNetAmount"],
"raw": "[BR-23] EN16931: An Invoice line shall have an Invoice line net amount. (at /*:CrossIndustryInvoice/*:SupplyChainTradeTransaction/*:IncludedSupplyChainTradeLineItem[2])"
}
],
"warnings": [
{
"rule": "BR-CL-01",
"layer": "en16931",
"line": null,
"message": "The document type code should use a recognised UNTDID 1001 value.",
"btCodes": ["BT-3"],
"fields": ["typeCode"],
"raw": "[BR-CL-01] EN16931-codelist: The document type code MUST be coded according to UNTDID 1001. (at /*:CrossIndustryInvoice/*:ExchangedDocument/*:TypeCode)"
}
],
"report": [
// ... one row per EN 16931 Business Term. See the Validation Report section below.
{ "code": "BT-27", "name": "Seller name", "section": "seller", "path": "seller.name", "value": null, "isValid": false, "errors": ["The seller must have a name (BT-27). Add the seller's name."], "warnings": [] }
]
}
Error Reference
When an invoice fails validation, the top-level errors array contains one "finding" object per violated business rule. The warnings array uses the same finding shape for non-blocking advisory issues that do not cause valid to become false. On a valid invoice errors is an empty array, while warnings may still contain entries.
Each finding object carries both a friendly, user-facing message and the verbatim technical raw validator output. Each object contains:
| Field | Type | Description |
|---|---|---|
| rule | string | The business rule identifier (e.g. BR-01, BR-DE-15). |
| layer | string | The validation layer that produced the finding: xsd (XML schema), en16931 (EN 16931 core business rules), or cius (profile overlay rules such as Peppol BIS or BR-DE). The PDF embedding findings (PDF-EMBED, PDF-EMBED-SYNTAX) report upload problems rather than rule violations and omit this field. |
| line | int | null | The 1-based invoice line item number the error relates to, or null when the error applies to the document level. |
| message | string | A plain-language explanation of the violation, suitable for displaying directly to end users. |
| btCodes | string[] | The EN 16931 Business Term / Business Group codes the rule references (e.g. ["BT-27"]). Empty when the rule maps to no specific term. |
| fields | string[] | Dotted JSON paths into the invoice document that the error applies to (e.g. ["seller.name"], ["lines[1].lineNetAmount"]). Derived from btCodes; line-scoped paths carry a zero-based lines[N] index. Empty when no field could be resolved. |
| raw | string | The verbatim technical message from the validator, including the rule id, layer and XPath location. |
The fields array is intended for client UIs: each path matches the request body structure, so a front-end form can map a validation error straight onto the input that produced it. Highlight that input, attach message beside it, and the user can correct the exact field without reading the raw rule text. Line-scoped paths use a zero-based index (lines[1] is the second line item) to match JavaScript array indexing.
| Status | Meaning | Action |
|---|---|---|
| 200 | Validation completed. Check valid to determine compliance. |
If valid is false, inspect the errors array. |
| 401 | Missing or invalid API key. | Check the Authorization header. |
| 422 | File is not a valid PDF or XML document. | Ensure you are uploading a supported file format. |
Validation Report
Every response, whether the invoice is valid or not, includes a top-level report array. It contains one row per EN 16931 Business Term present on the parsed invoice, pairing each term with its current value and a pass/fail flag. This gives a complete, field-by-field picture of the document, not just the rules that failed, which is useful for rendering a compliance table or a side-by-side review UI. The snippets above show report truncated to a couple of rows to avoid repeating the full structure.
Each row in the array is an object with the following fields:
| Field | Type | Description |
|---|---|---|
| code | string | The EN 16931 Business Term code (e.g. BT-1). |
| name | string | The human-readable name of the Business Term. |
| section | string | Logical grouping for display: header, seller, buyer, lines, payment, totals, or tax. |
| path | string | Dotted JSON path into the invoice document (e.g. lines[0].priceDetails.netPrice). Same path scheme as errors[].fields. |
| value | string | null | The current value of the term as a string, or null when the field is absent. |
| isValid | boolean | true when this row has no errors, otherwise false. |
| errors | string[] | Every error message referencing this Business Term; empty when none. |
| warnings | string[] | Every warning message referencing this Business Term; empty when none. |
A single report row looks like this:
{
"code": "BT-1",
"name": "Invoice number",
"section": "header",
"path": "invoiceNumber",
"value": "INV-2026-001",
"isValid": true,
"errors": [],
"warnings": []
}
Frequently Asked Questions
Which rule sets does POST /v1/validate/ubl run?
The OASIS UBL 2.1 XSD for the detected document type (Invoice or CreditNote), then the rules matching the declared CustomizationID: the EN 16931 Schematron for plain core documents, the Peppol BIS Billing 3.0 rules for Peppol and EHF documents, SI-UBL for NLCIUS, the KoSIT rules for XRechnung UBL, and the PINT rules for Peppol International documents. The layer field on each finding shows whether it came from xsd, en16931, or the cius overlay.
What do data.profile and data.customizationId contain?
data.customizationId is the BT-24 value exactly as the document declared it. data.profile is the slug of the rule set that was applied: en16931, peppol-bis-3, nlcius, xrechnung, or pint. EHF Billing 3.0 declares the Peppol BIS identifier, so EHF documents report peppol-bis-3, and the Norwegian national rules included in the Peppol artifact apply automatically.
What happens when the CustomizationID is unknown?
The document is validated against the EN 16931 base rules, data.profile is null, and a PROFILE-DETECTION warning explains that no dedicated rule set exists for the declared identifier. An exotic CIUS therefore still gets a base-rules verdict instead of a hard failure.
Can I upload a PDF with embedded UBL?
No. This endpoint accepts XML only and rejects other uploads with HTTP 422. Note that Factur-X and ZUGFeRD hybrids embed CII rather than UBL; validate those PDFs via POST /v1/validate/facturx or /v1/validate/zugferd.
Why do invalid invoices still return HTTP 200?
HTTP 200 means the validation ran to completion; the verdict is in the valid flag and the findings are in the errors and warnings arrays. Non-2xx statuses are reserved for transport problems: 401 for a missing or invalid API key, and 422 when the upload is not a readable XML file.