POST /invoice/validate
The normative verdict on a European electronic invoice: is this document conformant to EN 16931? The answer is deterministic and sourced - every violated rule is reported with its official CEN rule id (BR-xx, BR-CO-xx, BR-DEC-xx, the VAT-category BR-S/Z/E/AE/IC/G/O/AF/AG/B-xx families, BR-CL-xx), its normative message, its semantic path in BT/BG terms, and the observed vs expected values whenever they can be computed (recomposed totals, VAT breakdown, decimal precision). No LLM anywhere in the pipeline: the same document always gets the same verdict, and every response cites the exact version of the normative artefacts it was judged against.
It validates the three formats of the structured e-invoicing core - Factur-X/ZUGFeRD (PDF/A-3 with embedded XML), CII D16B and UBL 2.1 (Invoice and CreditNote) - exactly the formats mandated by the French e-invoicing reform (all businesses must be able to receive them from 2026-09-01; small and medium businesses must issue them from 2027-09-01). Before your agent books an incoming invoice, this is the “should you accept this invoice?” check.
The verdict is built from four layers, each reported separately in the response:
- XSD - schema validity against the official XSDs (CII D16B, UBL 2.1 Invoice/CreditNote, or the XSD of the declared Factur-X profile).
- EN 16931 business rules - every violated rule with its official id, severity, normative message, BT/BG path and observed/expected values.
- Factur-X/ZUGFeRD profile (CII documents only) - the profile declared in BT-24, the content’s conformance to that profile’s XSD, and any declared-vs-content mismatch.
- Identifier plausibility - syntax only - IBAN (mod-97), BIC (ISO 9362),
SIREN/SIRET (Luhn), VAT number syntax by country prefix, and the shape of
the electronic addresses BT-34 (
seller_electronic_address) and BT-49 (buyer_electronic_address), plus the monetary use of the currency code BT-5 (currency_is_monetary). Under the ISO 6523 scheme0002(the SIRENE register) the value is accepted as 9 digits (SIREN) or 14 digits (SIRET) - the way the official French Factur-X samples declare it. Every check reportspass,failornot_present- never silent.
On top of that EN 16931 core, an optional ?ruleset= parameter layers a
national or network CIUS rule pack - XRechnung (the German BR-DE
customization, faithful to the KoSIT reference validator) or Peppol BIS
Billing 3.0 (faithful to the OpenPeppol reference validator). The pack is
additive: the four core layers always run, and the pack’s own sourced
verdict (BR-DE-xx, PEPPOL-EN16931-Rxxx) is reported alongside them. That
makes one paid surface answer for three markets on a single call - the
French EN 16931 core, the German XRechnung mandate, and the Peppol network -
with the same doctrine of honesty throughout (faithful to the reference
validators, sourced rule ids, no LLM, contractual coverage per pack). See
?ruleset= below.
The runtime is stateless pure computation - no network call, no secret, no
store - and real-world invoices respond in under 200 ms. See the live
/catalog for the authoritative endpoint listing and
price.
x402 golden rule: the agent pays for the answer to its question. The
question is “is this invoice conformant?” - so a readable document gets a
200, billed, whether the verdict is valid or invalid. The documented
negative verdict - rule ids, observed/expected, layer by layer - is the
product: it is exactly what an agent needs to reject an invoice or send it
back with actionable reasons. Only requests the service cannot answer - a
malformed body, an unreadable or scanned document, a body over the size cap,
an unknown query parameter or value - leave the 200 range and are not
billed.
Request
POST with the invoice document as the body, in any of three equivalent
input forms - identical to /invoice/read:
| Content-Type | Body |
|---|---|
application/pdf | The raw bytes of a Factur-X/ZUGFeRD PDF |
application/xml or text/xml | The raw bytes of a CII D16B or UBL 2.1 (Invoice/CreditNote) XML |
application/json | { "document_base64": "..." } - standard base64 of the whole document, whitespace tolerated (wrapped base64 accepted) |
The content type only distinguishes the JSON form; the format of the document
itself is detected from its magic bytes (%PDF or the XML root element).
POST /invoice/validate?profile=strict
Content-Type: application/xml
<rsm:CrossIndustryInvoice ...> (raw CII bytes)
Body cap: 10 MB. The cap is enforced on the announced Content-Length
and on the actual bytes read (chunked bodies included), and the same bound
applies to the decompression of the embedded XML attachment of a Factur-X
PDF. A body over the cap returns a contractual JSON 413; it is not billed.
?profile=declared|strict (optional, default declared)
Factur-X profiles (MINIMUM, BASIC WL, BASIC, EN 16931, EXTENDED) are nested
subsets: the smaller profiles deliberately carry fewer fields than full
EN 16931 requires. The profile query parameter picks the yardstick:
declared(default) - the business rules are restricted to the subset applicable to the declared Factur-X profile: a MINIMUM invoice is not failed on fields it is not allowed to carry. “Is this document conformant to what it claims to be?”strict- additionally evaluates the full EN 16931 rule set, even when the declared profile is poorer. “Is this invoice usable as a full EN 16931 invoice - e.g. for the French reform?”
The mode used is echoed in summary.profile_mode. For UBL documents the
two modes are equivalent: Factur-X profiles are a CII notion, so the profile
layer reports not_applicable and the full rule set applies either way - a
documented no-op, not an error.
Any other value returns 400 INVALID_INPUT naming the accepted variants;
it is not billed. An unknown query key (?profil=, a typo) is rejected the
same way - a 400 INVALID_INPUT naming the offending key and the only ones
admitted (profile, ruleset), never a silent fallback to declared: a
mistyped parameter is never quietly ignored.
?ruleset=en16931|xrechnung|peppol (optional, default en16931)
Selects a national or network CIUS rule pack applied on top of the EN 16931 core. The four core layers always run; the pack adds to the verdict, it never replaces it. This is what turns one endpoint into a three-market offering on a single surface:
en16931(default) - the core alone. The response is bit-for-bit backward compatible: no ruleset field is serialised, exactly as before the packs existed. Existing agents change nothing.xrechnung- the German BR-DE customization on top of EN 16931 (idsBR-DE-xx), faithful to the KoSIT reference validator. Applies to UBL and CII alike. This is the rule set behind the German mandate (B2G already in force; B2B reception from 2025, phased issuance to 2027/2028).peppol- Peppol BIS Billing 3.0 (idsPEPPOL-EN16931-Rxxx), faithful to the OpenPeppol reference validator - the network layer behind the Belgian mandate (mandatory B2B issuance, penalties since April 2026). UBL 2.1 only (the Peppol network does not accept CII - see the matrix).
It composes with ?profile= (e.g. ?profile=strict&ruleset=xrechnung). A pack
call carries a data.ruleset report (its own sourced violations,
checked_rulesets, contractual coverage and pack artefacts_version -
xrechnung-3.0.2 / peppol-bis-3.0.20). The pack’s findings are partitioned
by severity: fatal ones make the verdict invalid; warning/information
ones set warnings_present without blocking.
Syntax × pack matrix:
| Pack | UBL 2.1 | CII D16B |
|---|---|---|
en16931 | ✅ core | ✅ core |
xrechnung | ✅ BR-DE | ✅ BR-DE |
peppol | ✅ PEPPOL-EN16931-R | ⚠️ sourced invalid verdict (peppol_requires_ubl) |
A CII document sent with ?ruleset=peppol is a valid question, not a
malformed request: Peppol BIS 3.0 mandates UBL 2.1, so a CII document cannot
be Peppol-compliant, and the normative answer to “is this CII Peppol-valid?”
is no. Per the x402 golden rule that is a 200, billed, verdict
invalid, carrying a single dedicated fatal violation peppol_requires_ubl
(observed: "cii", expected: "ubl") - never a 4xx or 5xx. It is the
teaching example of the golden rule: a sourced negative verdict is the product,
not an error. (Contrast /invoice/generate,
which builds documents: there an incoherent ruleset + syntax request is a
400, because you cannot fabricate a Peppol invoice as CII - here we judge an
existing document, so the answer is a billed verdict.)
An unknown value of ?ruleset= (?ruleset=zugferd) returns
400 INVALID_INPUT naming the accepted variants (en16931, xrechnung,
peppol); it is not billed.
200 response - UnifiedResponse
{
"data": {
"verdict": "valid | invalid",
"warnings_present": false,
"document": { ... },
"layers": { "xsd": { ... }, "business_rules": { ... }, "profile": { ... }, "plausibility": { ... } },
"summary": { ... },
"vat_consistency": { ... },
"ruleset": { ... }
},
"provenance": {
"source": "en16931-artefacts",
"fetched_at": "2026-07-12T10:29:22Z",
"freshness": { "kind": "snapshot", "as_of": "2026-07-11T00:00:00Z" }
}
}
data.verdict:validorinvalid- any fatal finding in any layer makes the verdictinvalid;warnings_presentflags non-fatal findings. A pack’s fatal violation makes the verdictinvalidjust like a core one.data.document: a minimal identification echo (format, invoice number, seller name, grand total, currency) so the verdict is attributable to a document without a second call. The full semantic model is/invoice/read’s job.data.vat_consistency: Invoket consistency checks on the VAT logic the document declares - informational, never part of the EN 16931 verdict. See the block below.data.ruleset: present only when?ruleset=xrechnungorpeppolwas requested (omitted entirely under the defaulten16931, so the default response is bit-for-bit unchanged). The pack’s sourced report - see the ruleset report below.provenance.freshness:kindissnapshotandas_ofdates the normative artefacts the verdict was computed against - there is no fetch and no clock behind it. The precise artefact pin issummary.artefacts_version, which names the core and the pack artefacts (en16931-1.3.16+facturx-1.08+xrechnung-3.0.2+peppol-bis-3.0.20).
The four layers
layers.xsd
Schema validity against the official XSDs. ok plus an errors array; any
XSD error is fatal.
layers.business_rules
The EN 16931 business rules, each violation reported as:
| Field | Description |
|---|---|
rule_id | The official CEN rule id - BR-CO-14, BR-DEC-19, BR-S-08, … |
severity | fatal or warning, as the artefacts define it |
message | The normative rule text |
path | The semantic location in BT/BG terms (e.g. BG-22/BT-110) |
observed / expected | Present whenever the rule’s arithmetic can be recomposed - totals, VAT breakdown, decimal precision - so the agent sees the exact numeric disagreement |
layers.profile (CII only)
For CII documents: the Factur-X/ZUGFeRD profile declared in the BT-24
URN, the content’s conformance to that profile’s XSD (xsd_violations), and
content_profile - the smallest nested profile whose XSD accepts the
document. A declared/content gap is reported as a profile_mismatch
finding. For UBL documents the layer reports status: "not_applicable" with
an explicit reason (actual service response):
{ "ok": true, "status": "not_applicable", "reason": "Factur-X profiles only exist for CII documents; UBL invoices carry no Factur-X profile", "xsd_violations": [], "content_profile": null }
layers.plausibility - syntax only, never a registry lookup
Syntactic plausibility of the identifiers the document carries: IBAN mod-97
checksum, BIC format (ISO 9362), SIREN/SIRET Luhn, VAT number syntax by
country prefix, the calendar validity of the dates BT-2
(issue_date_calendar) and BT-9 (due_date_calendar) - a well-shaped
20261399 is not a day that exists - the shape of the electronic addresses
BT-34/BT-49, and the monetary use of the currency code BT-5
(currency_is_monetary). Every check reports pass, fail or not_present - never silent, so “the IBAN
checked out” and “there was no IBAN to check” are never confused.
This layer lives outside the XSLT oracle: no EN 16931 business rule has an
opinion on the content of an IBAN, a SIREN, an electronic address or the
billing use of a currency code, so these
checks add to the verdict without touching the oracle-equality guarantee
against the reference validator. A failing check is a fatal finding like any
other - the verdict becomes invalid and the response is a 200, billed -
never a 4xx.
SIREN/SIRET under ISO 6523. A French legal id can be declared under two ISO 6523 schemes, and plausibility follows each faithfully:
0002(SIRENE) - the value is accepted as 9 digits (SIREN) or 14 digits (SIRET), because the official FR Factur-X samples (FNFE) declare the 14-digit SIRET under this scheme. For a 14-digit value the Luhn key is checked on its SIREN (the first 9 digits): the establishment NIC is not itself a Luhn-protected number, and a valid FNFE SIRET has a Luhn-valid SIREN while its full 14 digits are not Luhn-valid. So a real French SIRET is apass, not a spuriousfail.0009stays the strict SIRET scheme: 14 digits, Luhn on the full value.
This is a syntax relaxation only - it decides what shape is plausible, not whether the identifier is registered, and it does not touch the verdict’s oracle-equality guarantee against the reference validator (which has no such layer).
Electronic addresses (BT-34 / BT-49) - the verdict no longer depends on
?ruleset=. The seller’s and buyer’s electronic addresses are judged by
seller_electronic_address and buyer_electronic_address, on two points:
- The
valuemust be the bare identifier. The EAS code belongs to theschemeIDattribute, so a value that repeats it -0208:0642943543- or carries any other scheme-looking prefix -FR:0642943543- is afail. A Peppol access point would never route it. - The declared scheme’s format is checked whenever Peppol imposes one of
fatalseverity -0088(GLN),0192(Norwegian org. number),0184(Danish CVR),0208(Belgian enterprise number, mod-97),0007(Swedish org. number),0151(Australian ABN). These are the same validators thepeppolpack runs, and they now apply whatever?ruleset=you ask for: a document that?ruleset=peppolcallsinvalidonPEPPOL-COMMON-R043is no longer quietlyvalidunder the defaulten16931. Schemes whose upstream Peppol severity is onlywarning(R044 codice IPA, R047 P.IVA) are deliberately left out of plausibility - hardening beyond the upstream rule would be our opinion, not Peppol’s.
Like the rest of the layer, this is plausibility outside the XSLT oracle:
no BR rule judges the content of BT-34/BT-49, so oracle equality is intact, and
a fail is a 200 billed with verdict invalid, never a 4xx. Whether the
address is actually registered on the Peppol network is a different
question, answered by /company/peppol.
Currency (BT-5) - an ISO 4217 code that bills nothing. currency_is_monetary
fails on a short, explicit list of two codes that have no legitimate billing
use, never on a “starts with X” heuristic:
XXX- “no currency involved”. On a document carrying non-zero amounts that is an internal contradiction: an invoice for2052 XXXstates its own total is denominated in nothing.XTS- reserved for test purposes, never for production. It is the shape a test fixture takes when it escapes into a real run.
Everything else passes, deliberately: the metals XAU, XAG, XPD, XPT
and the units of account XDR, XUA have real invoicing and accounting
uses, and failing them would be a false positive against a legitimate issuer.
BR-CL-03/BR-CL-04 are right to accept every ISO 4217 code and are
untouched - this is plausibility layered on top of an oracle that has no
opinion here, so oracle equality is intact and a fail is again a 200
billed with verdict invalid, never a 4xx.
This layer answers “could this identifier exist?” - it never checks that
it does. Whether the SIREN is actually registered, the VAT number actually
active, the company actually solvent, is a different job against different
sources: validate the invoice, then check the counterparty - company
identity (/company/resolve) and live EU VAT status (/company/vat) are
separate paid calls; see the live /catalog for the
endpoints actually served today.
data.vat_consistency - Invoket checks (INV-VAT-*), never BR-
The EN 16931 verdict says whether the document is conformant; it says
nothing about the fiscal soundness of what the document declares. An
invoice can satisfy every BR- rule and still be refused: an intra-Community
supply carrying no reverse-charge mention, a rate that does not exist in the
country of taxation, a reverse charge claimed between two parties of the same
State. data.vat_consistency carries those observations - delimited without
ambiguity:
INV-VAT-xx, neverBR-: these are Invoket checks, not standardised rules. AnINV-VAT-*id never appears among the EN 16931 violations, nor among a CIUS pack’s;- informational: it changes neither the normative violations nor
data.verdict- the response says so itself, invat_consistency.noteand ininformational: true; - contractual coverage:
checked[]lists the four checks and what each judges,findings[]what was observed,not_applicable[]what could not conclude with its reason.
check_id | What it observes |
|---|---|
INV-VAT-01 | An intra-Community B2B supply (two different Member States, buyer carrying a BT-48 VAT identifier) whose every BG-23 group charges VAT: no reverse charge (category AE, Art. 196), no intra-Community supply (K, Art. 138), no exemption reason (BT-120/BT-121) - if one of them applies, the mention is compulsory (Art. 226) |
INV-VAT-02 | A rate applied (BT-119, category S) that is not among the rates published for the country of taxation on the invoice date (TEDB snapshot; the published rates are listed in the message) |
INV-VAT-03 | Category AE/K claimed while both parties are established in the same country - a signal, not a verdict: domestic reverse charges exist as national options (Art. 194, 199) |
INV-VAT-04 | The buyer’s VAT identifier (BT-48) absent or syntactically invalid on an invoice claiming AE/K: a valid number is a substantive condition (Art. 138(1)(b), 196). Syntax only - existence is /company/vat |
Each finding cites its articles in the form
/legal/eu-act can serve
(/legal/eu-act?celex=32006L0112&article=138) and names the BT terms in
play.
What the block refuses to guess: the place of taxation cannot be read off a
document - it depends on the nature of the operation (goods dispatched,
services, immovable property), which an invoice does not carry. On a
cross-border invoice INV-VAT-02 therefore comes back not_applicable,
pointing at POST /invoice/vat-treatment,
which receives that nature and renders the full verdict.
Real response on an FR -> IE service invoice charging French VAT - the document
is valid under EN 16931, and the block still flags what a tax authority
would:
{
"informational": true,
"note": "Invoket consistency checks (INV-VAT-*), not EN 16931 rules: they are informational, they never appear among the EN 16931 violations and they do not change the verdict of this response.",
"checked": [
{ "check_id": "INV-VAT-01", "summary": "Intra-Community B2B supply carrying neither a reverse-charge mention nor an exemption reason" },
{ "check_id": "INV-VAT-02", "summary": "VAT rate applied that is not published for the country of taxation on the invoice date" },
{ "check_id": "INV-VAT-03", "summary": "Reverse charge claimed while both parties are established in the same country" },
{ "check_id": "INV-VAT-04", "summary": "Buyer VAT identifier missing or syntactically invalid on a supply claiming an exemption" }
],
"findings": [
{
"check_id": "INV-VAT-01",
"summary": "Intra-Community B2B supply carrying neither a reverse-charge mention nor an exemption reason",
"message": "the supplier is established in FR and the customer in IE, and the customer carries a VAT identifier (BT-48), yet every VAT breakdown group (BG-23) charges VAT and none claims a relief: no reverse charge (category AE, Art. 196), no intra-Community supply (category K, Art. 138) and no exemption reason (BT-120/BT-121). If either applies, the mention is compulsory on the invoice (Art. 226) and its absence gets the invoice rejected. If the supplier is registered in IE and charges local VAT there, this is expected. POST /invoice/vat-treatment returns which one applies, with the wording to carry",
"terms": ["BT-40 (seller country)", "BT-55 (buyer country)", "BT-48 (buyer VAT identifier)", "BT-118 (VAT category code)", "BT-120 (VAT exemption reason)"],
"legal_basis": [
{ "celex": "32006L0112", "article": "138", "title": "Exemption of intra-Community supplies of goods", "query": "/legal/eu-act?celex=32006L0112&article=138" },
{ "celex": "32006L0112", "article": "196", "title": "Reverse charge by the customer for services covered by Article 44", "query": "/legal/eu-act?celex=32006L0112&article=196" },
{ "celex": "32006L0112", "article": "226", "title": "Compulsory particulars on invoices", "query": "/legal/eu-act?celex=32006L0112&article=226" }
]
}
],
"not_applicable": [
{
"check_id": "INV-VAT-02",
"reason": "the country of taxation cannot be read off the document: for a cross-border supply it depends on the nature of the operation (goods dispatched, services, immovable property...), which an invoice does not carry. POST /invoice/vat-treatment determines it, then returns the rate of that place"
}
]
}
Two more real findings, on other documents:
INV-VAT-02on a French invoice applying 17 %: “a rate the official TEDB snapshot does not publish for that country on that date: standard 20 / reduced 5.5, 10 / super-reduced 2.1. A rate that does not exist is refused by the tax administration whatever the arithmetic of the invoice.”INV-VAT-03on a domestic French invoice carrying categoryAE: “the reverse charge of Article 196 and the intra-Community supply of Article 138 both suppose two different Member States. Domestic reverse charges do exist as national options (Art. 194, 199: construction, waste, electronics…), so this is a signal, not a verdict.”
In all three cases data.verdict stayed valid: the checks inform, they
never vote.
data.ruleset - the CIUS pack report
Present only on a ?ruleset=xrechnung|peppol call (never under the default
en16931). It is the pack’s own sourced verdict, kept separate from the
core layers.business_rules - a BR-DE or Peppol violation lives here, and the
core business_rules layer can be ok: true while the pack fails:
| Field | Description |
|---|---|
ruleset | the pack requested - xrechnung or peppol |
checked_rulesets | the packs actually evaluated, core included - ["en16931", "xrechnung"]; coverage is never implicit |
ok | true iff the pack carries no fatal violation |
violations | the pack’s fatal violations (ids BR-DE-xx, PEPPOL-EN16931-Rxxx, or peppol_requires_ubl) - same { rule_id, severity, message, path, observed, expected } shape as the core; each one makes the verdict invalid |
warnings | the pack’s non-blocking findings (warning/information severity), distinct from violations; they set warnings_present but never change the verdict |
coverage | the pack’s contractual coverage - families evaluated and not_implemented gaps named (e.g. Peppol’s country-gated national CIUS layers are out of scope, stated explicitly) |
artefacts_version | the pack artefact that dates this verdict - xrechnung-3.0.2 or peppol-bis-3.0.20 |
Example - ?ruleset=xrechnung on a document missing the buyer reference
Actual service response for a CII invoice missing BT-10 (buyer reference),
posted as application/xml to /invoice/validate?ruleset=xrechnung. The core
EN 16931 layers pass; the XRechnung pack fails on BR-DE-15, so the overall
verdict is invalid - 200, billed:
{
"data": {
"verdict": "invalid",
"warnings_present": false,
"document": { "format": "cii", "number": "...", "seller": "...", "grand_total": "...", "currency": "EUR" },
"layers": {
"xsd": { "ok": true, "errors": [] },
"business_rules": { "ok": true, "violations": [] },
"profile": { "ok": true, "status": "declared", "profile": "en16931", "content_profile": "en16931" },
"plausibility": { "ok": true }
},
"summary": {
"violations": { "xsd": 0, "business_rules_fatal": 0, "business_rules_warning": 0, "profile": 0, "plausibility_failed": 0 },
"profile_mode": "declared",
"coverage": { "families": [ "..." ], "not_implemented": [] },
"artefacts_version": "en16931-1.3.16+facturx-1.08+xrechnung-3.0.2+peppol-bis-3.0.20"
},
"ruleset": {
"ruleset": "xrechnung",
"checked_rulesets": ["en16931", "xrechnung"],
"ok": false,
"violations": [
{
"rule_id": "BR-DE-15",
"severity": "fatal",
"message": "[BR-DE-15] Buyer reference (BT-10) must be present.",
"path": "/CrossIndustryInvoice"
}
],
"warnings": [],
"coverage": {
"families": [ "BR-DE (XRechnung 3.0.2 CIUS: mandatory content, payment-means routing, seller & buyer address/contact, IBAN/Skonto)" ],
"not_implemented": [ "..." ]
},
"artefacts_version": "xrechnung-3.0.2"
}
},
"provenance": {
"source": "en16931-artefacts",
"fetched_at": "2026-07-13T00:00:00Z",
"freshness": { "kind": "snapshot", "as_of": "2026-07-11T00:00:00Z" }
}
}
The BR-DE violation sits in the pack report, with the core
business_rules layer still ok - additive, never silent, official CIUS ids.
A ?ruleset=peppol call on a valid BIS UBL invoice returns the mirror image:
data.ruleset.ok: true, violations: [], artefacts_version: "peppol-bis-3.0.20" - the pack passes and the core stays decisive.
summary - counts, contractual coverage, artefact version
summary.violations counts findings per layer and severity (see the example
below). Two fields make the verdict’s scope explicit in every response:
summary.coverage-familieslists the rule families actually evaluated;not_implementednames what the verdict does not cover (empty today). The verdict is never silently partial: what it checked is part of the answer.summary.artefacts_version- the exact pin of the normative artefacts the verdict was computed against, e.g.en16931-1.3.16+facturx-1.08+xrechnung-3.0.2+peppol-bis-3.0.20(EN 16931 validation artefacts v1.3.16 + Factur-X 1.08 + XRechnung 3.0.2 + Peppol BIS 3.0.20). Same version, same document, same verdict - reproducibly. On a?ruleset=call the applicable pack version is repeated indata.ruleset.artefacts_version.
Example - real multi-violation response
Actual service response for a CII invoice from the validation corpus whose
totals were mutated (posted as application/xml): the VAT total disagrees
with the VAT breakdown, which cascades into the grand total. Both rules fire
with the recomposed numbers:
{
"data": {
"verdict": "invalid",
"warnings_present": false,
"document": {
"format": "cii",
"number": "12115118",
"seller": "De Koksmaat",
"grand_total": "250.33",
"currency": "EUR"
},
"layers": {
"xsd": { "ok": true, "errors": [] },
"business_rules": {
"ok": false,
"violations": [
{
"rule_id": "BR-CO-14",
"severity": "fatal",
"message": "Invoice total VAT amount (BT-110) = Σ VAT category tax amount (BT-117).",
"path": "BG-22/BT-110",
"observed": "21.73",
"expected": "20.73"
},
{
"rule_id": "BR-CO-15",
"severity": "fatal",
"message": "Invoice total amount with VAT (BT-112) = Invoice total amount without VAT (BT-109) + Invoice total VAT amount (BT-110).",
"path": "BG-22/BT-112",
"observed": "250.33",
"expected": "251.33"
}
]
},
"profile": {
"ok": true,
"status": "declared",
"profile": "en16931",
"urn": "urn:cen.eu:en16931:2017",
"xsd_violations": [],
"content_profile": "en16931"
},
"plausibility": {
"ok": true,
"iban_checksum": { "status": "pass" },
"bic_format": { "status": "not_present" },
"seller_legal_id_luhn": { "status": "not_present" },
"buyer_legal_id_luhn": { "status": "not_present" },
"seller_vat_syntax": { "status": "pass" },
"buyer_vat_syntax": { "status": "not_present" },
"issue_date_calendar": { "status": "pass" },
"due_date_calendar": { "status": "pass" },
"seller_electronic_address": { "status": "not_present" },
"buyer_electronic_address": { "status": "not_present" },
"currency_is_monetary": { "status": "pass" }
}
},
"summary": {
"violations": {
"xsd": 0,
"business_rules_fatal": 2,
"business_rules_warning": 0,
"profile": 0,
"plausibility_failed": 0
},
"profile_mode": "declared",
"coverage": {
"families": [
"BR (mandatory information)",
"BR-CO (coherence and computed totals)",
"BR-DEC (decimal precision)",
"BR-S / BR-Z / BR-E / BR-AE / BR-IC / BR-G / BR-O / BR-AF / BR-AG / BR-B (VAT categories)",
"BR-CL (code lists)"
],
"not_implemented": []
},
"artefacts_version": "en16931-1.3.16+facturx-1.08+xrechnung-3.0.2+peppol-bis-3.0.20"
}
},
"provenance": {
"source": "en16931-artefacts",
"fetched_at": "2026-07-12T10:29:22Z",
"freshness": { "kind": "snapshot", "as_of": "2026-07-11T00:00:00Z" }
}
}
This is a default (ruleset=en16931) call, so no data.ruleset field is
present - the response is bit-for-bit what it was before the packs existed.
This response is a 200, billed: the agent asked for a verdict and got
one - invalid, with the two official rule ids, the exact semantic locations
and the exact numeric disagreements needed to act on it.
Faithful to the reference validator - quirks included
The verdicts are those of the official CEN validation artefacts, pinned at the version cited in every response. The native engine is locked against the official tooling by a golden corpus: any divergence is a bug on our side.
That fidelity cuts both ways: the official artefacts have known misalignments between their UBL and CII bindings - country code lists and the UNCL 4451 subject-code list differ between the two syntaxes, and some rules branch differently - so on those edge cases the same semantic invoice can get a different verdict depending on the syntax it is written in. Exactly as it would with the official tooling. We reproduce the reference validator, quirks included, and report the misalignments upstream rather than patching verdicts locally (#509, #510).
The CIUS packs follow the same doctrine: XRechnung is locked against the
KoSIT reference validator and Peppol against the OpenPeppol reference
validator by golden oracles (ids for XRechnung, ids and severities for
Peppol). Each pack cites its own pinned artefact version - xrechnung-3.0.2,
peppol-bis-3.0.20 - and the divergences we track (e.g. KoSIT compiling
against a different EN 16931 artefact release than our core) are reported, not
silently patched.
Validate is not read - and not due diligence
This endpoint answers “is this invoice conformant?” - and only that.
- For “what does this invoice say?” - the full EN 16931 semantic model,
parties, lines, totals, payment IBAN - use
/invoice/read. The two are designed as a pair: read extracts faithfully without judging, validate judges without extracting. - For “does this counterparty exist?” - registry existence of a
SIREN/SIRET, live EU VAT status - the plausibility layer deliberately stops
at syntax; the registry checks are separate endpoints (see the live
/catalog). - For “is this transaction taxed the way the invoice says?” - who charges
the VAT, where the supply is taxed, which mention is compulsory - use
POST /invoice/vat-treatment. Thevat_consistencyblock flags what a document contradicts on its own face; determining the treatment needs facts the document does not carry. - For “issue your own invoice” - business JSON to a CII XML guaranteed
conformant by this same four-layer rule set - use
/invoice/generate. The three endpoints close the cycle read → validate → generate: receive and read it, decide to accept it, issue your own.
National and network CIUS rule sets are covered, on top of the core, via
?ruleset=:
XRechnung (German BR-DE) and Peppol BIS Billing 3.0. And once a BIS
invoice is proven valid, the natural next question is whether its recipient can
actually receive it: validate the BIS invoice, then check the Peppol
participant with /company/peppol - the verdict
judges the document, the directory lookup judges the participant’s
reachability. Two grids, two paid calls, one flow.
Out of scope in v1, stated plainly: no OCR (a scanned PDF without embedded XML is a typed 400, never reconstructed), no duplicate or fraud detection.
Errors
Only requests the service cannot answer leave the 200 range. Typed 4xx
responses are not billed, and a twisted document is never a 5xx. The
codes are the same as /invoice/read, plus the
?profile=/?ruleset= rejections:
| Status | code | Case |
|---|---|---|
| 400 | INVALID_INPUT | JSON body is not a { "document_base64": ... } object (a JSON array or scalar is refused, naming the expected form), the body could not be read, an unknown query key is present (?profil= typo - named; only profile and ruleset admitted), ?profile= is neither declared nor strict, or ?ruleset= is not en16931/xrechnung/peppol |
| 400 | invalid_base64 | document_base64 is not valid standard base64 (after whitespace tolerance) |
| 400 | unreadable_body | Neither PDF nor XML magic bytes - e.g. a scanned image: OCR is out of scope |
| 400 | malformed_xml | Truncated or malformed XML, including the embedded XML of a PDF |
| 400 | unknown_root | Well-formed XML whose root is neither CII (CrossIndustryInvoice) nor UBL (Invoice/CreditNote) |
| 400 | malformed_pdf | PDF the reader cannot parse |
| 400 | no_embedded_xml | PDF without an invoice XML attachment (a plain PDF, or Factur-X claimed wrongly) |
| 400 | invalid_decimal | An amount in the document is not a readable decimal |
| 413 | PAYLOAD_TOO_LARGE | Body over the 10 MB cap (announced or read), or an embedded attachment whose decompression exceeds the same bound |
| 404 | NOT_FOUND | Unknown route |
| 500 | INTERNAL | Internal error (detail logged, not exposed) |
{ "error": "unknown profile mode; accepted values are 'declared' and 'strict'", "code": "INVALID_INPUT" }
Attribution
The verdict is computed against the official EN 16931 validation artefacts
(CEN, published under EUPL 1.2) and the Factur-X specification (FNFE-MPE); the
CIUS packs are computed against the XRechnung artefacts (KoSIT) and the
Peppol BIS Billing 3.0 artefacts (OpenPeppol). The exact versions are cited
in every response (summary.artefacts_version, data.ruleset.artefacts_version
on a pack call, provenance.freshness.as_of). The invoice data itself comes
from the document you posted.
See also
POST /invoice/read- the faithful extraction of the same document into the full EN 16931 semantic model.POST /invoice/generate- issue your own invoice: business JSON to CII XML, guaranteed conformant by the same rule set.POST /invoice/vat-treatment- the VAT verdict on the transaction, where this endpoint judges the document.POST /invoice/late-payment- once a conformant invoice goes unpaid, what it owes in statutory interest.GET /company/peppol- after?ruleset=peppolproves the BIS document valid, check that the recipient is actually reachable on the Peppol network: validate the invoice, then check the participant.- For agents - discovery surfaces, the live
/catalogand how settlement works. - Quickstart - the 402 -> pay -> replay loop with runnable snippets.
GET /iban/resolve- resolve and check the payment IBAN a validated invoice carries before paying it.