Should an agent book this invoice? The EN 16931 verdict

One paid call returns a deterministic EN 16931 verdict on an inbound e-invoice — official BR-xx rule ids, observed vs expected values, plus XRechnung and Peppol rule packs.

By Matthias Begot ·

An agent that receives an invoice has to decide something binary: book it, or send it back. POST /invoice/validate answers exactly that question and nothing else — is this document conformant to EN 16931? — and it answers it the only way an accounting pipeline can use: deterministically and with sources. Every violation comes back with its official CEN rule id (BR-CO-14, BR-DEC-19, BR-S-08, BR-CL-xx), the normative rule text, the semantic location in BT/BG terms, and the observed and expected values whenever the arithmetic can be recomposed. No model guesses. The same document always gets the same verdict, and the response cites the exact version of the normative artefacts it was judged against.

The rejection has to be actionable, or it is not a decision

“This invoice looks wrong” is not something an agent can act on. It cannot book the invoice, and it cannot send a supplier a rejection they can fix. It escalates to a human, and the automation was pointless.

That is why the negative verdict — not the positive one — is the actual product here. BR-CO-14 fatal, path BG-22/BT-110, observed 21.73, expected 20.73 is a sentence an agent can put in a reply: your VAT total does not match the sum of your VAT breakdown, we computed 20.73, you wrote 21.73. The supplier’s own software will recognise the rule id, because it is the id from the official CEN artefacts, not one we invented.

The x402 consequence is direct: a readable document gets a 200, billed, whether the verdict is valid or invalid. You asked “is this invoice conformant?” and you got the answer. Only requests the service cannot answer — a malformed body, a scanned PDF with no embedded XML, a body over the cap, an unknown query parameter — leave the 200 range, and those are not billed.

Four layers, reported separately

The verdict is not a boolean with a paragraph attached. It is four independent layers, each reported on its own so you can see which one objected:

  1. XSD — schema validity against the official schemas (CII D16B, UBL 2.1 Invoice/CreditNote, or the XSD of the declared Factur-X profile). Any XSD error is fatal.
  2. EN 16931 business rules — every violated rule with rule_id, severity, the normative message, the BT/BG path, and observed/expected when they can be computed.
  3. Factur-X / ZUGFeRD profile (CII only) — the profile declared in BT-24, the content’s conformance to that profile’s XSD, and the content_profile: the smallest nested profile whose XSD actually accepts the document. A gap between declared and content is reported as profile_mismatch.
  4. Identifier plausibility — IBAN mod-97, BIC format, SIREN/SIRET Luhn, VAT syntax by country prefix, calendar validity of BT-2 and BT-9, the shape of the electronic addresses BT-34/BT-49, and the monetary use of the currency code BT-5. Every check reports pass, fail or not_presentnever silent, so “the IBAN checked out” and “there was no IBAN” are never confused.

Layers 1 to 3 are the standard. Layer 4 is ours, and it is honest about being ours: it lives outside the XSLT oracle, because no EN 16931 business rule has an opinion on the content of an IBAN or a SIREN. Adding it does not touch the equality guarantee against the reference validator.

What a real refusal looks like

Actual service response for a CII invoice whose totals were mutated: the VAT total disagrees with the VAT breakdown, and that cascades into the grand total. Two rules fire, each 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", "content_profile": "en16931" },
      "plausibility": { "ok": true, "iban_checksum": { "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", "BR-CO", "BR-DEC", "BR-S / BR-Z / BR-E / BR-AE / BR-IC / BR-G / BR-O / BR-AF / BR-AG / BR-B", "BR-CL"], "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" }
  }
}

Two details carry more weight than the violations themselves.

data.document is a minimal identification echo — format, number, seller, grand total, currency — so the verdict is attributable to a document without a second call. The full semantic model is a different job, done by POST /invoice/read.

summary.coverage states what the verdict actually evaluated, in every response, with a not_implemented list beside it. A verdict that does not declare its own scope is asking to be over-trusted.

One surface, three markets

EN 16931 is the core, but almost nobody is only subject to the core. A national or network CIUS sits on top, and it is the CIUS that rejects your invoice in practice. ?ruleset= layers one on:

?ruleset=Rule idsFaithful toThe obligation behind it
en16931 (default)BR-xx, BR-CO, BR-DEC, BR-CLCEN validation artefactsthe French reform core
xrechnungBR-DE-xxthe KoSIT reference validatorthe German mandate, UBL and CII alike
peppolPEPPOL-EN16931-Rxxxthe OpenPeppol reference validatorthe Peppol network, UBL 2.1 only

The pack is additive: the four core layers always run, and the pack’s own verdict arrives in a separate data.ruleset block with its own violations, its own warnings, its contractual coverage and its own pinned artefacts_version. A BR-DE failure therefore shows up with the core business_rules layer still ok: true — which is precisely the situation you want reported that way, because it tells you the invoice is a valid EN 16931 invoice that a German buyer will still refuse.

Under the default en16931, no ruleset field is serialised at all. The response is bit-for-bit what it was before the packs existed; integrations that predate them change nothing.

A CII document asked about Peppol is a verdict, not an error

This is the teaching case for the x402 golden rule, so it is worth stating plainly. Peppol BIS Billing 3.0 mandates UBL 2.1; the network does not accept CII. So a CII document sent with ?ruleset=peppol cannot be Peppol-valid.

That is a legitimate question with a normative answer, and the answer is no. It comes back as a 200, billed, verdict invalid, carrying one dedicated fatal violation peppol_requires_ubl with observed: "cii" and expected: "ubl" — never a 4xx, never a 5xx.

Contrast POST /invoice/generate, where the same combination is a 400: there you are asking the service to fabricate a document that cannot exist, and an incoherent instruction is a bad request. Here you are asking about a document that already exists. Judging is not building.

?profile=declared or strict — two different questions

Factur-X profiles (MINIMUM, BASIC WL, BASIC, EN 16931, EXTENDED) are nested subsets, and the smaller ones deliberately carry fewer fields than full EN 16931 requires. Failing a MINIMUM invoice for missing fields it is not allowed to carry would be noise, so the yardstick is a parameter:

  • declared (default) — “is this document conformant to what it claims to be?” The business rules are restricted to the subset applicable to the declared profile.
  • strict — “is this invoice usable as a full EN 16931 invoice?” The complete rule set is evaluated even when the declared profile is poorer. This is the mode that matters if you are checking a supplier against the French reform rather than against their own claim.

The mode is echoed in summary.profile_mode. For UBL the two are equivalent — Factur-X profiles are a CII notion, the profile layer reports not_applicable with a reason, and that is a documented no-op rather than an error. A mistyped key (?profil=) is a named 400, never a silent fallback to the default.

Plausibility is syntax; existence is a different call

The plausibility layer answers “could this identifier exist?” — it never checks that it does. That boundary is not a limitation to apologise for, it is what keeps the endpoint stateless, offline and reproducible. Whether the counterparty is real is a separate paid question:

The natural chaining for an inbound document is: validate it, then check the counterparty, then check the IBAN before paying it. Three grids, three settled calls, one flow — and each one refusable on its own evidence.

One subtlety worth knowing if you handle French documents: under the ISO 6523 scheme 0002 (SIRENE), the value is accepted as 9 digits (SIREN) or 14 digits (SIRET), because that is how the official FNFE Factur-X samples declare it, and for a 14-digit value the Luhn key is checked on the SIREN part. A real French SIRET is a pass, not a spurious fail. Scheme 0009 stays strict SIRET. Fidelity to how the ecosystem actually writes identifiers beats theoretical purity.

INV-VAT-* is informational, and says so in the response

An invoice can satisfy every BR- rule and still be refused by a tax authority: an intra-Community supply with no reverse-charge mention, a VAT rate that does not exist in the country of taxation, a reverse charge claimed between two parties of the same State. data.vat_consistency reports those observations — with three hard guarantees:

  • the ids are INV-VAT-xx, never BR-. An Invoket check never appears among the EN 16931 violations, nor among a CIUS pack’s;
  • it is informational: informational: true and an explicit note say so in the payload itself, and it does not change data.verdict;
  • it is contractual: checked[] lists what was judged, findings[] what was observed, and not_applicable[] what could not conclude with its reason.

Each finding cites its articles in the form GET /legal/eu-act can serve, so the legal basis is itself checkable rather than asserted.

And the block refuses to guess where it cannot know. The place of taxation cannot be read off a document — it depends on the nature of the operation, which an invoice does not carry — so on a cross-border invoice the rate check comes back not_applicable, pointing at POST /invoice/vat-treatment, which receives that nature and renders the full verdict. The document check informs; it never votes.

Read the artefact pin, because the standard is moving under you

Every response carries summary.artefacts_version — a single string naming the core and the pack artefacts, e.g. en16931-1.3.16+facturx-1.08+xrechnung-3.0.2+peppol-bis-3.0.20. Same version, same document, same verdict.

That pin is not bookkeeping. It is the only way to know what a “compliant” answer meant, because the ground is moving on three independent clocks right now:

  • Peppol BIS Billing 3.0 ships on a semi-annual train. Version 3.0.20 was published on 24 November 2025 and became mandatory on 23 February 2026, followed by a hotfix on 27 January 2026; the May 2026 release becomes mandatory on 17 August 2026, with the current documentation available only until 16 August (docs.peppol.eu, Peppol May 2026 Release).
  • XRechnung moves on its own. The KoSIT validator configuration v2026-01-31 was published on 5 February 2026 and compiles against CEN Schematron rules 1.3.15, while the current CEN artefacts are at v1.3.16 (itplr-kosit, ConnectingEurope). Two “compliant” validators can legitimately disagree, and only the pinned versions explain why.
  • EN 16931 itself is being revised. CEN approved EN 16931-1:2026 in February 2026, with publication expected mid-2026, and KoSIT has announced XRechnung 4.0 on that basis for mid-to-late 2026, gated on the CEN syntax bindings and validation artefacts (XStandards Einkauf).

A validator that returns a bare valid: true without naming its artefacts is telling you nothing durable. Compare the version in the response against the release your own obligation is written to, and you know exactly where you stand.

Where the verdict sits in the cycle

The invoice endpoints are one rule set seen from several angles:

  1. POST /invoice/read — extract an inbound document into the EN 16931 semantic model, faithfully, without judging it.
  2. POST /invoice/validate — the verdict on that document, with official rule ids and observed-vs-expected values.
  3. POST /invoice/generate and POST /invoice/generate-pdf — the invoice you issue, re-proved against this same engine before it is served.
  4. POST /invoice/late-payment — once a conformant invoice goes unpaid, what it owes in statutory interest.

Read and validate are designed as a pair: read extracts without judging, validate judges without extracting. The inbound flow is covered in reading an inbound e-invoice, and the outbound one in generating an invoice that proves its own conformance.

The authoritative endpoint listing and price live in the catalog, served live and never hardcoded in an article. The payment loop is the usual discover → 402 → pay → replay; the Quickstart walks it with runnable snippets and For agents covers the discovery surfaces.

What the verdict will not do

  • No OCR. A scanned PDF with no embedded XML is a typed 400, not billed, never reconstructed. A picture of an invoice is not an invoice.
  • No duplicate or fraud detection. The endpoint judges one document against a rule set; it has no memory of what you validated yesterday, by design — the runtime is stateless pure computation, with no network call, no secret and no store.
  • No registry lookup. Plausibility stops at syntax, deliberately (see above).
  • No tax verdict. vat_consistency flags what a document contradicts on its own face; determining the treatment needs facts the document does not carry.
  • Faithful to the reference validator, quirks included. The official artefacts have known misalignments between their UBL and CII bindings — code lists differ, some rules branch differently — so 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 and report the divergences upstream rather than patching verdicts locally.

Used for what it is — a settled call that turns a received document into a sourced, reproducible accept-or-reject decision — validation is the step that lets an agent book invoices without a human opening each one. The full request contract, the four-layer response schema, the ruleset matrix and the complete error table are in the POST /invoice/validate documentation.