# 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. Published 2026-08-10 · HTML version: https://invoket.com/blog/should-an-agent-book-this-invoice-en-16931-verdict --- An agent that receives an invoice has to decide something binary: book it, or send it back. [`POST /invoice/validate`](/docs/api/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_present` — **never 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. ```json { "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`](/docs/api/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 ids | Faithful to | The obligation behind it | |---|---|---|---| | `en16931` (default) | `BR-xx`, `BR-CO`, `BR-DEC`, `BR-CL` | CEN validation artefacts | the French reform core | | `xrechnung` | `BR-DE-xx` | the **KoSIT** reference validator | the German mandate, UBL and CII alike | | `peppol` | `PEPPOL-EN16931-Rxxx` | the **OpenPeppol** reference validator | the 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`](/docs/api/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: - registry existence of a SIREN/SIRET → [`GET /company/resolve`](/docs/api/company-resolve) - live EU VAT status → [`GET /company/vat`](/docs/api/company-vat) - can the recipient actually receive a BIS invoice → [`GET /company/peppol`](/docs/api/company-peppol) - is the payment IBAN sane and reachable → [`GET /iban/resolve`](/docs/api/iban-resolve) 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`](/docs/api/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`](/docs/api/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](https://docs.peppol.eu/poacc/billing/3.0/release-notes/), [Peppol May 2026 Release](https://peppol.agid.gov.it/en/news/Peppol%20May%20Release%202026/)). - **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](https://github.com/itplr-kosit/validator-configuration-xrechnung/releases), [ConnectingEurope](https://github.com/ConnectingEurope/eInvoicing-EN16931/releases)). 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](https://xeinkauf.de/aktuelles/xrechnung/xrechnung-4-umsetzung/)). 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`](/docs/api/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`](/docs/api/invoice-generate) and [`POST /invoice/generate-pdf`](/docs/api/invoice-generate-pdf) — the invoice you issue, re-proved against this same engine before it is served. 4. [`POST /invoice/late-payment`](/docs/api/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](/blog/read-an-inbound-e-invoice-before-an-agent-pays-it), and the outbound one in [generating an invoice that proves its own conformance](/blog/generate-an-e-invoice-that-proves-its-own-conformance). The authoritative endpoint listing and price live in the [catalog](https://api.invoket.com/catalog), served live and never hardcoded in an article. The payment loop is the usual discover → `402` → pay → replay; the [Quickstart](/docs/quickstart) walks it with runnable snippets and [For agents](/docs/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](/docs/api/invoice-validate).