Read an inbound Factur-X, CII or UBL e-invoice before paying it

One paid call turns a Factur-X, CII or UBL document into the EN 16931 semantic model — parties, lines, VAT, IBAN, dates — faithfully, with no OCR guesswork.

By Matthias Begot · · Updated

From 1 September 2026, an agent working a French accounts-payable inbox stops receiving PDFs a human reads and starts receiving structured documents — Factur-X, CII or UBL. Before it can decide anything about one of them, it needs the document as data: who issued it, against which VAT number, to which IBAN, for how much, due when. That is one paid call — POST /invoice/read — which projects the document into the EN 16931 semantic model exactly as the document states it, with no conformance judgement and no OCR reconstruction.

Why the September 2026 inbox is different

The French e-invoicing reform makes reception the universal obligation before it makes issuance one. From 1 September 2026, every VAT-registered business established in France must use an approved platform to receive its electronic invoices — micro-enterprises included — while large and intermediate-sized enterprises must also issue them; small and medium enterprises follow on 1 September 2027 (impots.gouv.fr).

Reception is the asymmetric half. A company controls what it issues: one format, one template, one tool. It controls nothing about what arrives. The inbox will carry all three syntaxes of the minimum base — hybrid Factur-X PDF/A-3, bare CII, bare UBL — written by every ERP in Europe, at whatever version of the standard the sender happens to be on. The current external specifications are version 3.2, dated 30 April 2026, and they lean on three AFNOR standards (XP Z12-012/013/014) for the format and profile detail (impots.gouv.fr).

The format itself is a moving target

Anyone tempted to hardcode a parser should look at the release cadence of the Franco-German standard alone. ZUGFeRD 2.5 / Factur-X 1.09 was published on 10 June 2026, to be used from 1 July 2026; it moved to UN/CEFACT CII D22B (backward compatible with D16B) and began pulling in elements of the revised EN 16931-1:2026 (FeRD). Six weeks later, the corrigendum ZUGFeRD 2.5.2 landed on 4 August 2026, valid from 1 September 2026, cleaning up schema and rule inconsistencies across the EXTENDED, BASIC WL, BASIC and EN 16931 profiles (FeRD).

Two releases in under two months, the second one landing the same day the mandate starts. Reading inbound invoices is not a problem you solve once.

Read answers one question: what does this document say?

/invoice/read is deliberately narrow. It answers “what does this invoice say?” and nothing else:

  • The payment IBAN comes back as written, not repaired, not checked.
  • Totals are not recomputed — a document whose lines do not add up returns the totals it states.
  • The declared Factur-X profile (BT-24) is relayed, not verified.
  • Monetary amounts are exact decimal strings at the source scale: "19.90" stays "19.90", never a float, never re-rounded.

That is the point rather than a limitation. A faithful projection is the raw material every downstream check needs; an extractor that quietly “fixes” an inconsistency destroys the evidence that something was wrong. The normative verdict — EN 16931 business rules by BR-xx id, arithmetic consistency, identifier plausibility — is the job of POST /invoice/validate. The pair is designed as a division of labour: read extracts without judging, validate judges without extracting.

There is also no OCR. A scanned PDF with no embedded XML is refused with a typed 400, never reconstructed from pixels. An invoice an agent is about to pay is the wrong place for a confidence score.

The call

POST the document as the body, in any of three equivalent forms:

Content-TypeBody
application/pdfRaw bytes of a Factur-X/ZUGFeRD PDF
application/xmlRaw bytes of a CII D16B or UBL 2.1 Invoice/CreditNote
application/json{ "document_base64": "..." }

The content type only distinguishes the JSON form. The actual format is detected from magic bytes (%PDF or the XML root element), so a PDF posted as application/xml is still read as a PDF — a useful property when the document arrives from a platform whose headers you do not control.

POST /invoice/read
Content-Type: application/pdf

%PDF-1.7 ... (raw Factur-X bytes)

A real response: the credit note that corrects an invoice

Here is the actual service answer for the official FNFE Avoir_FR_type381_BASIC.pdf sample — a Factur-X BASIC credit note — abridged to the fields an agent acts on.

{
  "data": {
    "format": { "kind": "facturx", "declared_profile": "basic" },
    "invoice": {
      "number": "AV-2017-0005",
      "issue_date": "2017-11-16",
      "type_code": "381",
      "currency": "EUR",
      "due_date": "2017-11-16",
      "specification_id": "urn:cen.eu:en16931:2017:compliant:factur-x.eu:1p0:basic",
      "notes": [
        { "subject_code": null, "content": "Avoir suite à bidon 10L d'huile d'olive percé et carton de nougat renversé" }
      ],
      "preceding_invoices": [ { "number": "FA-2017-0010", "issue_date": "2017-11-13" } ],
      "seller": {
        "name": "Au bon moulin",
        "legal_registration_id": { "value": "99999999800010", "scheme": "0002" },
        "vat_id": "FR11999999998",
        "address": { "line1": "1242 chemin de l'olive", "city": "Malaucène", "postal_code": "84340", "country": "FR" }
      },
      "buyer": {
        "name": "Ma jolie boutique",
        "legal_registration_id": { "value": "78787878400035", "scheme": "0002" },
        "vat_id": "FR19787878784"
      },
      "payment_means": [],
      "totals": { "lines_net": "218.48", "net": "218.48", "vat_total": "14.99", "grand_total": "233.47", "prepaid": "0.00", "due": "233.47" },
      "vat_breakdown": [
        { "taxable_amount": "20.48", "tax_amount": "4.10", "category": "S", "rate": "20.00" },
        { "taxable_amount": "198.00", "tax_amount": "10.89", "category": "S", "rate": "5.50" }
      ],
      "lines": [
        { "id": "1", "quantity": "5.000", "unit": "C62", "unit_price": "4.10", "net_amount": "20.48",
          "item_name": "Nougat de l'Abbaye 250g", "vat_category": "S", "vat_rate": "20.00",
          "standard_item_id": { "value": "3518370400049", "scheme": "0160" } },
        { "id": "2", "quantity": "10.000", "unit": "LTR", "unit_price": "19.80", "net_amount": "198.00",
          "item_name": "Huile d'olive à l'ancienne", "vat_category": "S", "vat_rate": "5.50" }
      ]
    },
    "coverage": {
      "absent_fields": ["vat_accounting_currency", "buyer_reference", "sales_order_reference",
                        "payment_terms", "business_process", "payment_means", "allowances", "charges"],
      "notes": []
    }
  },
  "provenance": { "source": "invoice-document", "fetched_at": "2026-08-10T17:41:33Z", "freshness": { "kind": "live" } }
}

Four facts an agent can route on immediately, none of which require reading the PDF’s visual layer:

  • type_code: "381" — this is a credit note, not an invoice. It reduces a payable; it is not something to pay.
  • preceding_invoices names the document it corrects, FA-2017-0010 of 13 November 2017 — the reconciliation key, structured, not a free-text “re:” line.
  • payment_means: [] and "payment_means" in absent_fields: the document carries no IBAN, and says so explicitly. An agent never has to infer whether an empty array means “no payment means” or “we failed to read them”.
  • provenance.source: "invoice-document" with freshness.kind: "live" — this data came from the bytes you posted, not from a reference dataset. No attribution is owed, and nothing here can go stale.

coverage is the field that stops an agent guessing

Every extractor returns null for missing data. Very few tell you whether the null means the document is silent or the profile does not carry that concept. coverage.absent_fields lists, by JSON name, the top-level fields the document does not state — so silence is always explicit.

The showcase is the Factur-X MINIMUM and BASIC WL profiles, which carry no invoice lines by design. The response returns lines: [] and names "lines" in absent_fields. Without that second signal, an agent reading a BASIC WL invoice would conclude the supplier billed nothing.

coverage.notes covers reading particularities rather than missing data — the embedded XML was found under a non-standard attachment name, or the PDF carried several attachments and this is the one that was read. Both are situations where a silent choice would be the dangerous behaviour.

declared_profile does not invent a CIUS

Profile handling shows the same discipline. Post a Peppol BIS Billing 3.0 UBL invoice and the answer is:

{ "kind": "ubl", "declared_profile": null }

with the full URN preserved on the semantic model:

"specification_id": "urn:cen.eu:en16931:2017#compliant#urn:fdc:peppol.eu:2017:poacc:billing:3.0"

declared_profile is null because Peppol BIS is a CIUS, not a Factur-X profile — the service will not coin a name for a family it does not enumerate. The raw specification_id is always there, verbatim, for an agent that wants to route on the exact customisation. An XRechnung CII document, by contrast, comes back as { "kind": "cii", "declared_profile": "en16931" } with specification_id: "urn:cen.eu:en16931:2017" — what the document declares, no more.

This is also why the format churn described above does not break the endpoint: the profile is read as the last segment of whatever URN the document carries, and relayed without verification. A newer Factur-X release passes through the reader; deciding whether the document lives up to the profile it claims is /invoice/validate’s job, and that side is pinned to a stated artefact set.

An inbound invoice is untrusted input

Everything in this inbox was written by someone else. The runtime is stateless pure computation — no network call, no secret, no store — and hostile documents come back as typed, unbilled 4xx rather than as a parser incident. Actual responses:

InputAnswer
PNG bytes (a scan)400 unreadable_body — “document is neither a PDF nor an XML invoice (expected %PDF- or an XML root element)“
PDF with no XML attachment400 no_embedded_xml — “a Factur-X/ZUGFeRD document must embed its CII XML as an attachment”
XML with an external entity400 malformed_xml — “unsupported XML entity reference: &xxe;
A zip-bomb attachment413 PAYLOAD_TOO_LARGE

The 10 MB body cap is enforced on the announced Content-Length, on the bytes actually read, and — this is the one that matters — on the decompression of the embedded XML attachment, so a small PDF cannot expand into a large one inside the reader. A query parameter is rejected too: the endpoint takes none, so ?x= is treated as a typo rather than silently ignored.

The x402 golden rule, applied to a bad invoice

The agent pays for the answer to its question, and the question is “what does this document say?”. So a readable document is a 200, billed, even when the invoice is inconsistent or non-conformant — an inconsistency faithfully relayed is precisely what the downstream check needs to see. Only requests the service cannot answer leave the 200 range, and those are not billed: a malformed body, an unreadable or scanned document, a body over the cap.

The authoritative price and accepted rails are served live by the catalog — never hardcoded in an article. The payment loop is the usual discover → 402 → pay → replay cycle; the Quickstart walks it with runnable snippets and For agents covers the discovery surfaces.

The inbound chain read sits at the head of

Extraction is step one, and its value is that every later step becomes a lookup with a real key instead of a regex over a PDF.

  1. Read the document: POST /invoice/read. You now hold seller.vat_id, seller.legal_registration_id, the IBAN in payment_means, due_date, and the totals.
  2. Judge the document with POST /invoice/validate if conformance matters — the EN 16931 verdict with official BR-xx rule ids.
  3. Check the payee before paying. The IBAN was relayed as written, so resolve it with GET /iban/resolve — bank, BIC, SEPA reachability — and screen it with GET /iban/screen, which covers bank and jurisdiction sanctions risk, not the account holder’s name.
  4. Check the supplier exists and is who it says. seller.vat_id goes to GET /company/vat for the live VIES check; legal_registration_id (scheme 0002 is SIRET) goes to GET /company/resolve.
  5. Compute what is actually owed on time. due_date and the totals feed POST /invoice/late-payment for statutory interest and the recovery indemnity.
  6. Answer in kind. When your agent issues the credit note or the invoice back, POST /invoice/generate closes the cycle — read → validate → generate.

Each step is its own settled call, so an agent pays only for the checks a particular document actually warrants. A €40 credit note from a known supplier stops at step 1; a first invoice from a new counterparty with an unfamiliar IBAN runs the chain.

What read will not do

  • No OCR, ever. A scanned or image-only PDF is refused, not reconstructed.
  • No conformance verdict, no repair. That is /invoice/validate — see also pre-flighting a supplier for the counterparty side of the same decision.
  • No transmission. The endpoint reads a document you already hold; it is not an approved platform and it does not send, receive or archive anything on your behalf.
  • No signature or seal verification, and no PDF/A-3 conformance check on the carrier.
  • No VAT determination. Whether the mechanism on the document is the right one is /invoice/vat-treatment’s question, not a reading question.

Used for what it is — one settled call that turns an arbitrary European e-invoice into fields an agent can branch on — read is the cheapest step in the inbound chain and the one every other step depends on. For the full request contract, the field tables and the error codes, see the POST /invoice/read documentation.