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-Type | Body |
|---|---|
application/pdf | Raw bytes of a Factur-X/ZUGFeRD PDF |
application/xml | Raw 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_invoicesnames the document it corrects,FA-2017-0010of 13 November 2017 — the reconciliation key, structured, not a free-text “re:” line.payment_means: []and"payment_means"inabsent_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"withfreshness.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:
| Input | Answer |
|---|---|
| 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 attachment | 400 no_embedded_xml — “a Factur-X/ZUGFeRD document must embed its CII XML as an attachment” |
| XML with an external entity | 400 malformed_xml — “unsupported XML entity reference: &xxe;” |
| A zip-bomb attachment | 413 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.
- Read the document:
POST /invoice/read. You now holdseller.vat_id,seller.legal_registration_id, the IBAN inpayment_means,due_date, and the totals. - Judge the document with
POST /invoice/validateif conformance matters — the EN 16931 verdict with officialBR-xxrule ids. - 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 withGET /iban/screen, which covers bank and jurisdiction sanctions risk, not the account holder’s name. - Check the supplier exists and is who it says.
seller.vat_idgoes toGET /company/vatfor the live VIES check;legal_registration_id(scheme0002is SIRET) goes toGET /company/resolve. - Compute what is actually owed on time.
due_dateand the totals feedPOST /invoice/late-paymentfor statutory interest and the recovery indemnity. - Answer in kind. When your agent issues the credit note or the invoice
back,
POST /invoice/generatecloses 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.