Skip to main content
A hybrid e-invoice (Factur-X in France, ZUGFeRD in Germany) is a PDF/A-3 file that people can read, carrying a machine-readable EN 16931 invoice XML as an embedded attachment. The PDF must be PDF/A-3 because that is the archival profile that permits arbitrary embedded files — PDF/A-2 forbids them — and every Factur-X profile requires it. The problem splits into two halves that different tools own:
  1. A conformant container — a PDF/A-3 with embedded fonts and the attachment slot. This is Forme’s half, verified by veraPDF in CI.
  2. 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.
If you already generate the XML yourself, Forme can embed it directly in the render call — that one-pass path is the E-invoicing page. This guide is the other arrangement, common when a PDP handles the regulatory side: you send the service a PDF and the invoice data, and it returns the Factur-X. Any service with that shape fits. The worked example below uses SuperPDP because its API is public and documents the conversion call precisely; nothing about the pipeline is specific to it. The full example is runnable from the Forme repo: 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

The point of rendering PDF/A-3b before the call is that the container is already conformant when the service sees it — fonts embedded, sRGB output intent, XMP identification — and you have validated that claim in your own process rather than trusting anyone’s marketing, theirs or ours.

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 literal urn: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): S for standard rate, E exempt, 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 0002 for 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), B1 for 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.
The practical lesson: the service’s validation errors are the spec. The example’s data was corrected entirely by reading the schematron rule IDs the endpoint returned (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 takes multipart/form-data with two parts: the invoice (JSON semantic model or raw CII XML) and the PDF.
A 400 carries the schematron findings verbatim — treat it as the validator output it is, not as noise. If you generate your own CII XML the same endpoint takes it in place of the JSON: 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:
Mustang’s report for the run: 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.
All three end at the same place: XML that validates, inside a PDF/A-3 that validates.

What Forme does and does not do

Forme emits the container: PDF/A-3 with embedded fonts and a conformant embedded-file slot, verified by veraPDF on every commit, plus the one-pass Factur-X embedding when you supply the XML. Forme does not generate EN 16931 XML, and this page’s pipeline is not “Factur-X support” — the semantic half belongs to the service or library you choose. The distinction matters: a conformant container around non-conformant XML is not a legal e-invoice, and no PDF library can fix that for you.