You can have a document checked before you send it. The endpoint runs the same five-level validation chain that runs after submission — so whatever passes here will pass there. It creates no invoice, stores nothing and consumes no credit.
Validating a document
POST /api/v1/documents/validate
X-Api-Key: …
Content-Type: application/json
Accept-Language: en
{
"xml_content": "<Invoice xmlns=…>…</Invoice>",
"receiver_peppol_id": "0245:2120984085"
}
Only xml_content is required. Including receiver_peppol_id also turns on the
receiver check via the Peppol SMP; without it only the document itself is validated.
The response is always 200, even for an invalid document. An invalid invoice is not a call failure — the verdict is part of the data.
{
"valid": false,
"network_ready": false,
"overall_severity": "FATAL",
"validation_levels": {
"L1_xml": "OK", "L2_xsd": "OK", "L3_en16931": "OK",
"L4_peppol": "FATAL", "L5_sk": "WARNING"
},
"errors": [{
"rule_id": "PEPPOL-EN16931-R001",
"rule_set": "PEPPOL",
"level": 4,
"severity": "FATAL",
"raw_message": "Business process MUST be provided.",
"raw_location": "/Invoice[1]/cbc:ProfileID",
"title": "Business process identifier is missing",
"explanation": "Peppol requires the invoice to state…",
"fix": "Add the cbc:ProfileID element with the value…",
"location": "Invoice header → Profile",
"localized": true
}],
"warnings": [],
"truncated": false,
"total_findings": 1,
"document": { "document_type": "INVOICE", "invoice_number": "FA-2026-001", "…": "…" },
"receiver": {
"peppol_id": "0245:2120984085",
"registered": true,
"supports_document_type": false,
"reason": "NOT_SERVICED",
"checked_at": "2026-08-05T10:00:00Z"
},
"duration_ms": 1840
}
Findings are localized, the original stays
title, explanation, fix and location are translated according to the
Accept-Language header (sk and en are supported; without the header we use en).
The language actually applied comes back in Content-Language.
The same finding shape is returned by GET /api/v1/invoices/{id}/validation — see Validation.
raw_message and raw_location are never translated — they are the original
Schematron text and XPath. Those are exactly what you need when working the problem with
your ERP vendor.
If a rule is not in our catalogue yet, localized is false and title carries the
original English message. A finding is never lost.
The five validation levels
| Level | What it checks |
|---|---|
L1_xml | XML well-formedness |
L2_xsd | UBL 2.1 schema |
L3_en16931 | EN 16931 rules |
L4_peppol | Peppol BIS 3.0 rules |
L5_sk | Slovak national rules |
Values are OK, WARNING, FATAL or SKIPPED. SKIPPED means the level did not run —
typically because validation stopped earlier: a failure at L1 halts everything after it.
The EN 16931 and Peppol BIS rules run in a single chain, so they are never executed twice; each level still reports its own result.
Level L5_sk can additionally return ERROR — the Slovak rule set failed to execute. It does
not fail the document, but you will find a finding with rule_id SK_ENGINE among the
warnings, so it is clear the Slovak rules were not verified.
The receiver check
network_ready is true only when the document is valid and the receiver is in the
network and supports the document type. Both network_ready and receiver appear in
the response only when you asked for the check.
The reason field distinguishes three situations that must not be conflated:
reason | Meaning | What to do |
|---|---|---|
NOT_REGISTERED | the receiver is not in the Peppol network | verify their Peppol ID |
NOT_SERVICED | the receiver is in the network but does not support this document type | verify the document type |
LOOKUP_UNAVAILABLE | no verdict was reached — the SMP did not answer | retry shortly |
With LOOKUP_UNAVAILABLE both registered and supports_document_type are null, not
false — we did not establish that the receiver is missing, we simply did not find out.
Checking a receiver without a document
If you only need to know whether a customer is reachable, you do not have to send a document:
GET /api/v1/participants/0245:2120984085/reachability?document_type=INVOICE
The response has the same shape as the receiver block above.
Limits
- documents up to 10 MB, the same as for sending; larger ones return 413
- at most 500 findings per response; when truncated,
truncatedistrueandtotal_findingscarries the real count - if the validation service is temporarily unavailable we return 503 with a
Retry-Afterheader; sending invoices is unaffected - validations and reachability lookups per minute depend on your plan — from 10
validations and 30 lookups on MINI to 300 validations and 120 lookups on ENTERPRISE
(Limits & quotas). Both paths have their own counter —
validating in a loop will not exhaust your invoice-sending quota. Your limit and remaining
allowance are carried by the
RateLimit-PolicyandRateLimitheaders; exceeding it returns 429 withRetry-After