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:

  1. XSD - schema validity against the official XSDs (CII D16B, UBL 2.1 Invoice/CreditNote, or the XSD of the declared Factur-X profile).
  2. EN 16931 business rules - every violated rule with its official id, severity, normative message, BT/BG path and observed/expected values.
  3. 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.
  4. 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 scheme 0002 (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 reports pass, fail or not_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-TypeBody
application/pdfThe raw bytes of a Factur-X/ZUGFeRD PDF
application/xml or text/xmlThe 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 (ids BR-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 (ids PEPPOL-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:

PackUBL 2.1CII 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: valid or invalid - any fatal finding in any layer makes the verdict invalid; warnings_present flags non-fatal findings. A pack’s fatal violation makes the verdict invalid just 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=xrechnung or peppol was requested (omitted entirely under the default en16931, so the default response is bit-for-bit unchanged). The pack’s sourced report - see the ruleset report below.
  • provenance.freshness: kind is snapshot and as_of dates the normative artefacts the verdict was computed against - there is no fetch and no clock behind it. The precise artefact pin is summary.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:

FieldDescription
rule_idThe official CEN rule id - BR-CO-14, BR-DEC-19, BR-S-08, …
severityfatal or warning, as the artefacts define it
messageThe normative rule text
pathThe semantic location in BT/BG terms (e.g. BG-22/BT-110)
observed / expectedPresent 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 a pass, not a spurious fail.
  • 0009 stays 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 value must be the bare identifier. The EAS code belongs to the schemeID attribute, so a value that repeats it - 0208:0642943543 - or carries any other scheme-looking prefix - FR:0642943543 - is a fail. A Peppol access point would never route it.
  • The declared scheme’s format is checked whenever Peppol imposes one of fatal severity - 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 the peppol pack runs, and they now apply whatever ?ruleset= you ask for: a document that ?ruleset=peppol calls invalid on PEPPOL-COMMON-R043 is no longer quietly valid under the default en16931. Schemes whose upstream Peppol severity is only warning (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 for 2052 XXX states 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, never BR-: these are Invoket checks, not standardised rules. An INV-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, in vat_consistency.note and in informational: 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_idWhat it observes
INV-VAT-01An 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-02A 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-03Category 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-04The 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-02 on 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-03 on a domestic French invoice carrying category AE: “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:

FieldDescription
rulesetthe pack requested - xrechnung or peppol
checked_rulesetsthe packs actually evaluated, core included - ["en16931", "xrechnung"]; coverage is never implicit
oktrue iff the pack carries no fatal violation
violationsthe 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
warningsthe pack’s non-blocking findings (warning/information severity), distinct from violations; they set warnings_present but never change the verdict
coveragethe 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_versionthe 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 - families lists the rule families actually evaluated; not_implemented names 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 in data.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. The vat_consistency block 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:

StatuscodeCase
400INVALID_INPUTJSON 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
400invalid_base64document_base64 is not valid standard base64 (after whitespace tolerance)
400unreadable_bodyNeither PDF nor XML magic bytes - e.g. a scanned image: OCR is out of scope
400malformed_xmlTruncated or malformed XML, including the embedded XML of a PDF
400unknown_rootWell-formed XML whose root is neither CII (CrossIndustryInvoice) nor UBL (Invoice/CreditNote)
400malformed_pdfPDF the reader cannot parse
400no_embedded_xmlPDF without an invoice XML attachment (a plain PDF, or Factur-X claimed wrongly)
400invalid_decimalAn amount in the document is not a readable decimal
413PAYLOAD_TOO_LARGEBody over the 10 MB cap (announced or read), or an embedded attachment whose decompression exceeds the same bound
404NOT_FOUNDUnknown route
500INTERNALInternal 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=peppol proves 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 /catalog and 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.