- A conformant container — a PDF/A-3 with embedded fonts and the attachment slot. This is Forme’s half, verified by veraPDF in CI.
- Standards-compliant invoice XML — the EN 16931 semantic model, its ~170 business terms, and the several hundred business rules over them. This half comes from a PDP (in France, a Plateforme de Dématérialisation Partenaire), an e-invoicing service, or your own EN 16931 implementation.
examples/factur-x-pdp/.
Everything below was executed against the live endpoint on
2026-09-11 and verified with veraPDF 1.30.2 and Mustangproject 2.26.0;
the one thing that did not verify is called out where it happened.
The pipeline
Step 1 — map your data to EN 16931
This is the part everyone has to do and nobody documents. The example starts from the invoice template’s own data shape and maps it to the EN 16931 semantic model (the field names below are SuperPDP’s JSON encoding of it; every field is annotated with its EN 16931 business term, so the mapping transfers to any encoding):
Fields with no equivalent in the standard: presentation strings like
the template’s
eyebrow label stay on the PDF only, which is fine — the
XML is the machine-readable half, not a transcript of the layout.
Fields the standard requires that a typical invoice template does not
carry — this is where the real work is:
process_control.specification_identifier(BT-24): the literalurn:cen.eu:en16931:2017, naming which rule set the invoice claims.- A VAT breakdown by rate (BG-23): taxable amount, tax amount, and category code per rate. A display total like “tax: 863.75” is not enough; the standard wants the rate-by-rate decomposition so the receiver can reconcile it.
- A VAT category code per line (BT-151):
Sfor standard rate,Eexempt, and so on — per line, not per invoice. - Structured party identifiers: country codes on every address, and a seller identifier the buyer can resolve (BR-CO-26 requires BT-29, BT-30 or BT-31 — a registration or VAT identifier, not just a name).
The jurisdiction layer
The EN 16931 core is not the end of it. Converting through a French PDP, the endpoint also enforced the French CTC profile on top — every one of these was a real rejection the example hit before it passed:- Seller SIREN, exactly 9 digits (BR-FR-10), and a buyer electronic
address for routing (BR-FR-12) — the example uses the SIREN with EAS
scheme
0002for both parties. - Three mandatory notes (BG-1) with subject codes: late-payment
penalties (
PMD), the statutory €40 recovery indemnity (PMT), and the early-payment discount mention (AAB) (BR-FR-05). French invoices carry these sentences by law; the standard makes them data. - A billing-mode code in
process_control.business_process_type(BR-FR-08),B1for ordinary goods invoicing. - VAT rates from the French rate list (BR-FR-16) — the schematron rejects a rate that is not an actual French rate, so a demonstration invoice with US sales tax cannot pass a French pipeline. The example invoice is accordingly a French one, 20% VAT, with the PDF and the XML agreeing — a hybrid invoice whose two halves disagree is worse than either half alone.
BR-CO-26, CII-SR-470, BR-FR-05…), the
same validator-first loop Forme’s own conformance work uses.
Step 2 — render the PDF/A-3b
pdfA: '3b' is the level Factur-X requires; 3b is sufficient for
every profile. PDF/A requires embedded fonts, and the base-14 families
are not embeddable — @formepdf/fonts-standard provides
metric-compatible substitutes the engine embeds in place, so layout is
byte-identical with or without compliance mode. The JSX path is the
same idea: <Document pdfa="3b" fonts={standardFonts()}>.
The example then runs veraPDF locally and refuses to post a PDF that
fails its own claim:
Step 3 — the conversion call
The endpoint takesmultipart/form-data with two parts: the invoice
(JSON semantic model or raw CII XML) and the PDF.
from=cii, and the invoice part is the XML with
type: 'application/xml'. Nothing else changes. (And if you have the
XML, also consider embedding it in the render
call and skipping the service round trip entirely.)
Step 4 — verify what came back
A returned file is a claim, not a fact. The example runs the result through the same two validators Forme’s CI uses:Parsed PDF: valid, XML: valid, Profile: urn:cen.eu:en16931:2017, Errors: [] — the returned Factur-X passed
PDF/A-3b, the EN 16931 schematron, and the French Flux2 schematron
Mustang 2.26.0 applies.
One thing did not verify, and it is worth stating plainly: sending
payment instructions (BG-16, a credit transfer with a valid French
IBAN as BT-84) made the endpoint’s own validation reject its CII
serialization with [CII-SR-470] Either the IBAN or a Proprietary ID (BT-84) shall be used, in every scheme spelling we tried. BG-16 is
optional in EN 16931, so the example omits it and the payment details
stay on the human-readable PDF. If your invoices must carry structured
payment data through a conversion service, test that path against your
service before relying on it.
Alternatives, honestly
- A PDP or e-invoicing API (as here): the service owns the XML generation and the regulatory keep-up. In France this is the mandated direction of travel anyway — invoices flow through a PDP.
- An EN 16931 library or service that only does XML — several exist across ecosystems (Mustangproject itself generates as well as validates, on the JVM). You then hold the XML and can use Forme’s one-pass embedding with no second service.
- In-house EN 16931 — a real project, not a weekend: the semantic model, the CII or UBL serialization, and ongoing schematron maintenance as the rule sets revise. Teams with heavy invoice volume and existing standards expertise do it; most don’t need to.