# Generate a Factur-X e-invoice that proves its own conformance > One paid call turns a business JSON into an EN 16931 invoice — CII, UBL, Peppol BIS 3.0 or a full Factur-X PDF/A-3B — that has already re-passed the official rule set before it is served. Published 2026-08-10 · Updated 2026-08-15 · HTML version: https://invoket.com/blog/generate-an-e-invoice-that-proves-its-own-conformance --- An agent can write an invoice. What it cannot do, on its own, is **prove** the invoice is conformant — and from September 2026 in France, and already today in Belgium, an invoice that is not conformant is not an invoice. Two Invoket endpoints close that gap by refusing to serve their own output until it has passed the official rule set: [`POST /invoice/generate`](/docs/api/invoice-generate) returns the **XML core** (CII D16B or UBL 2.1), and [`POST /invoice/generate-pdf`](/docs/api/invoice-generate-pdf) returns the **complete Factur-X document** — a readable PDF/A-3B carrying that same XML. Both take the same business JSON, and both return `data.validated: true` **by construction**: a post-condition that fails is a 500, never served and never billed. ## The deadline is not the interesting part, but it sets the clock France first. From **1 September 2026**, every VAT-registered business established in France must be able to **receive** electronic invoices through an approved platform; large and intermediate-sized enterprises must also **issue** them from that date, and micro, small and medium enterprises follow no later than **1 September 2027** ([impots.gouv.fr](https://www.impots.gouv.fr/professionnel/questions/partir-de-quand-suis-je-concerne-par-la-reforme-de-la-facturation), [economie.gouv.fr](https://www.economie.gouv.fr/tout-savoir-sur-la-facturation-electronique-pour-les-entreprises)). Belgium is further along and shows what enforcement looks like. Domestic B2B e-invoicing over Peppol has been mandatory since **1 January 2026**; the three-month tolerance ended on 31 March, and since **1 April 2026** a graduated penalty regime applies — €1,500, then €3,000, then €5,000 for repeat offences within three months ([Loyens & Loeff](https://www.loyensloeff.com/insights/news--events/news/e-invoicing-in-belgium-as-from-1-january-2026-key-provisions-of-the-long-awaited-royal-decree/)). The mandated format there is **Peppol BIS Billing 3.0**, which is UBL 2.1, not CII — a syntax difference that is invisible to a business user and fatal to a naive generator. So an agent issuing invoices in Europe now has to hit a moving, jurisdiction- specific target where "it looked right" carries a price tag. ## Writing valid XML is the easy half; proving it is the product Emitting a plausible `` is a templating exercise. Any model can do it, and that is exactly the problem: the output looks correct in all the ways a human reviewer checks and fails in the ways a Schematron engine checks. EN 16931 conformance is several hundred business rules with official ids — `BR-xx`, `BR-CO-xx`, `BR-DEC-xx`, the VAT-category families `BR-S`/`BR-Z`/`BR-E`/`BR-AE`, the code-list rules `BR-CL-xx` — plus XSD validity, plus profile conformance, plus identifier plausibility. `/invoice/generate` therefore does not stop at serialisation. Before any byte leaves the service, the XML it just produced is **re-parsed and re-passed through the four layers of [`/invoice/validate`](/docs/api/invoice-validate)** in strict mode, on the same pinned artefacts — the same yardstick sold separately as a verdict on incoming documents. If that re-validation does not return `valid`, the request ends as a `500 INTERNAL`: the output is never served, the detail is logged, and **the call is not billed**. A 5xx on this endpoint means we broke a promise, not that your input was bad. `/invoice/generate-pdf` adds a second post-condition on top. The served PDF is opened by **our own [`/invoice/read`](/docs/api/invoice-read)** and the embedded `factur-x.xml` is extracted and compared **bit for bit** against the `data.xml` returned flat in the response. The readable document and the machine document cannot drift apart, because the service proves they have not before it hands either one over. That is the whole pitch: `validated: true` is not a claim about the generator, it is the echo of a check that already ran. ## One input, two deliverables | | [`/invoice/generate`](/docs/api/invoice-generate) | [`/invoice/generate-pdf`](/docs/api/invoice-generate-pdf) | |---|---|---| | Output | XML core, `data.xml` | `data.pdf_base64` **and** `data.xml` | | Syntaxes | CII D16B (default) or UBL 2.1 | CII D16B only (Factur-X embeds CII) | | Peppol BIS 3.0 | Yes, `ruleset: "peppol"` with `syntax: "ubl"` | No | | Post-conditions | Re-validated | Re-validated, re-read bit-for-bit, PDF/A-3B | | Input schema | Identical business JSON | Identical business JSON | The input is deliberately unglamorous — parties, lines, VAT, an optional payment IBAN — and the endpoint does the EN 16931 mapping: ```json { "invoice_number": "FA-2026-0042", "issue_date": "2026-07-12", "due_date": "2026-08-11", "currency": "EUR", "seller": { "name": "Menuiserie Dubois SARL", "legal_id": { "value": "732829320", "scheme": "0002" }, "vat_id": "FR44732829320", "address": { "line1": "12 rue des Artisans", "city": "Lyon", "postal_code": "69003", "country": "FR" } }, "buyer": { "name": "Agence Lumen SAS", "vat_id": "FR23342663721", "address": { "line1": "4 avenue du Parc", "city": "Paris", "postal_code": "75011", "country": "FR" } }, "lines": [ { "description": "Fabrication comptoir chene massif", "qty": "1", "unit": "C62", "unit_price": "1200.00", "vat": { "category": "S", "rate": "20" } }, { "description": "Pose et finition sur site", "qty": "6", "unit": "HUR", "unit_price": "85.00", "vat": { "category": "S", "rate": "20" } } ], "payment": { "iban": "FR7630006000011234567890189", "terms": "30 jours nets" } } ``` Note what is absent: no totals. The response fills them in and shows its work. ```json { "data": { "xml": "\n", "profile": "urn:cen.eu:en16931:2017", "validated": true, "computed": { "totals": { "lines_net": "1710", "net": "1710", "vat_total": "342", "grand_total": "2052", "due": "2052" }, "vat_breakdown": [ { "taxable_amount": "1710", "tax_amount": "342", "category": "S", "rate": "20" } ] } }, "provenance": { "source": "en16931-artefacts", "fetched_at": "2026-07-12T14:18:41Z", "freshness": { "kind": "snapshot", "as_of": "2026-07-11T00:00:00Z" } } } ``` `data.computed` is the read-back: the BG-22 document totals and the BG-23 VAT breakdown **actually serialized into the XML**, as exact decimal strings. An agent never has to re-parse the document to learn what it just sent. ## Totals: computed if absent, verified if provided, never repaired This is the policy that decides whether a generator is trustworthy under automation. - **Omit `totals`** — the recommended path — and the document totals and VAT breakdown are computed from the lines with the official `BR-CO` rounding rules. - **Provide `totals`** and they are **verified** against that same computation. A divergence is a `400` carrying an `inconsistent_totals` issue that names the BT term and the value the engine computed: ```json { "code": "INVALID_INVOICE_INPUT", "error": "invoice input is invalid: 1 actionable issue(s) listed in issues[]", "issues": [ { "code": "inconsistent_totals", "term": "BT-112", "message": "provided total BT-112 = 2152.00 is inconsistent with the value computed from the invoice body (2052, EN 16931 BR-CO rules)" } ] } ``` The service **never silently repairs a total**. Every amount in the XML is either yours and verified, or computed and echoed back. A generator that quietly corrects an agent's arithmetic is hiding the bug that produced it — and in an accounting pipeline, the divergence is the signal. The same discipline applies to decimals. `qty`, `unit_price`, `vat.rate` and `totals.*` accept a JSON number or a **plain decimal string**; scientific notation (`"1e3"`) is refused with an `invalid_decimal` issue naming the field, because an agent that emits it has most likely made a scale error, and `1e3` euros should never be reinterpreted on its behalf. ## Peppol BIS 3.0: `syntax: "ubl"` plus `ruleset: "peppol"` For the Belgian mandate and for anything transmitted over the Peppol network, `ruleset: "peppol"` layers the `PEPPOL-EN16931-Rxxx` pack on top of the EN 16931 core — faithful to OpenPeppol, with sourced rule ids, and replayed at re-validation exactly like the core rules. The service writes the `CustomizationID` and `ProfileID` headers itself; the agent does not supply them. What the agent does supply are the **electronic addresses**. Peppol requires the seller's (BT-34, `R010`) and the buyer's (BT-49, `R020`), each declared as `electronic_address: { value, scheme }` with an official **EAS** code — `0208` for a Belgian enterprise number, `0009` for a SIRET. `value` is the **bare identifier**: the scheme code lives in `scheme` and must not be repeated inside `value`. A missing address is a sourced, unbilled 400: ```json { "code": "INVALID_INVOICE_INPUT", "error": "invoice input is invalid: 1 actionable issue(s) listed in issues[]", "issues": [ { "code": "rule_violation", "rule_id": "PEPPOL-EN16931-R010", "message": "[PEPPOL-EN16931-R010] Buyer electronic address (BT-49) MUST be provided.", "path": "/*/AccountingCustomerParty/Party/EndpointID" } ] } ``` Two design choices worth calling out: - **Fatal blocks, warning informs.** Missed fatal Peppol rules join the EN 16931 violations in `issues[]`. Passed warning-severity rules do not block: they come back in `data.warnings[]` on the 200, so the agent sees the non-binding recommendations without the invoice being withheld. - **`ruleset: "peppol"` with `syntax: "cii"` is a 400, not a verdict.** Peppol mandates UBL, so the request contradicts itself. Contrast the validation side, where a CII document sent with `?ruleset=peppol` is a **billed 200 with an `invalid` verdict** — there you asked a legitimate question about a document that exists. Here you are fabricating one, and an incoherent instruction is a bad request. Address shape is checked on **every** call, with or without the Peppol ruleset: an EAS code repeated inside `value`, or a malformed identifier for a scheme Peppol treats as fatal, is an `implausible_identifier` issue. Nothing that a Peppol access point could not route comes back `validated: true`. ## The rule EN 16931 does not enforce: the VAT mention An invoice can satisfy every `BR-` rule and still be rejected by a tax authority. An intra-Community supply or a cross-border B2B service without its **reverse-charge mention** is irregular under Article 226 of Directive 2006/112/EC — and no business rule says so, because the rules check structure, not tax treatment. The optional `vat_treatment` field has the transaction judged by the engine behind [`POST /invoice/vat-treatment`](/docs/api/invoice-vat-treatment) and writes the determined mention into **BT-120**, the field the standard provides for it — in the CII, in the UBL, and in both the embedded XML and the rendered page of the Factur-X PDF. Three properties keep it honest: - The mention is written **only** on VAT breakdown groups whose category can carry it (`E`, `AE`, `K`, `G`, `O` — where no VAT is charged), never on a taxed group. - A `vat.exemption_reason` you supplied is **kept exactly as written**, and `notes[]` says so. - A country in `vat_treatment` that contradicts the country the invoice declares (BT-40 / BT-55) is a `400 vat_treatment_mismatch`, **not billed** — a verdict rendered on other countries judges a different transaction, and the mention it produced would be false. An uncovered regime returns `mechanism: "undetermined"` with its rationale; the invoice is still generated, and **no mention is invented**. The [VAT-treatment article](/blog/vat-treatment-for-a-sales-ledger-in-one-settlement) covers that verdict on its own terms. ## Errors are actionable and unbilled, all in one turn The x402 golden rule applies literally here. The question is "give me a conformant invoice", so the **only billed case is a 200 with a generated, self-validated document**. Everything an agent can fix is a 400: | Status | Case | Billed | |---|---|---| | 200 | Invoice generated and passed its own validation (`validated: true`) | yes | | 400 `INVALID_INPUT` | Not the schema: bad JSON, an unknown field, or a query parameter (the endpoint takes none) | no | | 400 `INVALID_INVOICE_INPUT` | Schema-valid but not generatable conformant — all issues in `issues[]` | no | | 413 | Body over the 10 MB cap | no | | 500 | A violated post-condition — an internal bug, never served | no | The detail that saves round trips: **every independent issue is reported together**, each with a stable `code`, a `message`, and the normative vocabulary where it exists (`term` in BT/BG, `rule_id` in BR-xx or PEPPOL-Rxxx, `path`). An agent fixes its payload in one turn rather than discovering violations one 400 at a time. Unknown fields are rejected rather than ignored, so a typo names itself instead of silently dropping a value. ## Read the pinned artefact version — the standard moves Every response carries `provenance.freshness.as_of`, dating the exact normative artefact set the invoice was generated and validated against — EN 16931 validation artefacts, the Factur-X specification, and the OpenPeppol BIS pack, each at a named version. There is no fetch and no wall clock behind the guarantee: it is pinned. That pin matters because the ground moves faster than most integrations. FNFE-MPE and FeRD published **Factur-X 1.09 / ZUGFeRD 2.5** on 10 June 2026, applicable from 1 July, then the corrigendum **1.09.2 / 2.5.2** in August ([fnfe-mpe.org](https://fnfe-mpe.org/factur-x/factur-x_en/)). Peppol BIS Billing 3.0 is updated **semi-annually, in May and November** ([docs.peppol.eu](https://docs.peppol.eu/poacc/billing/3.0/release-notes/)). A generator that does not tell you which artefacts it used is telling you nothing durable — compare the `as_of` in the response against the release your own obligation is written to, and you know exactly where you stand. ## Where generation sits in the invoice cycle The four invoice endpoints are one rule set seen from four 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`](/docs/api/invoice-validate) — the verdict on a document you received, with official rule ids and observed-vs-expected values. 3. **`POST /invoice/generate`** — the XML core you issue, re-proved against that same verdict engine. 4. [`POST /invoice/generate-pdf`](/docs/api/invoice-generate-pdf) — the same core wrapped in a PDF/A-3B a human can open and a machine can parse. For a Peppol issuance, three settled calls make one flow: generate the BIS 3.0 invoice, [re-prove it](/docs/api/invoice-validate) with `?ruleset=peppol` if you want the verdict as a separate artefact to file, and check the recipient is actually reachable with [`GET /company/peppol`](/docs/api/company-peppol) before you send. The inbound counterpart — what to do with an invoice that arrives — is covered in [reading an inbound e-invoice](/blog/read-an-inbound-e-invoice-before-an-agent-pays-it). 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 generation will not do - **No numbering.** `invoice_number` (BT-1) is yours: the service does not allocate, sequence or de-duplicate invoice numbers. - **No archiving, no signature, no transmission.** The runtime is stateless pure computation — no network call, no secret, no store. It is not an approved platform and it does not send anything on your behalf. - **No registry lookup.** Identifiers are checked for **syntactic** plausibility only — IBAN mod-97, SIREN/SIRET Luhn, VAT syntax. Whether the counterparty *exists* is [`/company/resolve`](/docs/api/company-resolve) and [`/company/vat`](/docs/api/company-vat), separate paid calls. - **No design.** The PDF page is a minimal deterministic render — the value is the conformance and the embedded XML, not the template. Same input, same PDF, byte for byte, because every value comes from the invoice and none from the clock. - **Bounded profiles in v1.** Factur-X **EN 16931** profile only in CII (no MINIMUM/BASIC/EXTENDED emission); XRechnung is validated but not generated; the UBL **CreditNote** (type 381) is not produced, though the CII credit note is. Used for what it is — a settled call that turns business data into a document whose conformance has already been proven against the official rule set — generation is the step that lets an agent issue invoices without a human verifying each one. The full request contract, the field-by-field schema and the complete issue-code table are in the [`POST /invoice/generate` documentation](/docs/api/invoice-generate) and the [`POST /invoice/generate-pdf` documentation](/docs/api/invoice-generate-pdf).