ePostman docs
Console

Pre-flight validation

Check an invoice and the receiver's reachability before you send it.

3 min read
  • #validation
  • #pre-flight
  • #documents/validate
  • #reachability
  • #smp
  • #schematron

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

LevelWhat it checks
L1_xmlXML well-formedness
L2_xsdUBL 2.1 schema
L3_en16931EN 16931 rules
L4_peppolPeppol BIS 3.0 rules
L5_skSlovak 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:

reasonMeaningWhat to do
NOT_REGISTEREDthe receiver is not in the Peppol networkverify their Peppol ID
NOT_SERVICEDthe receiver is in the network but does not support this document typeverify the document type
LOOKUP_UNAVAILABLEno verdict was reached — the SMP did not answerretry 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, truncated is true and total_findings carries the real count
  • if the validation service is temporarily unavailable we return 503 with a Retry-After header; 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-Policy and RateLimit headers; exceeding it returns 429 with Retry-After