France switches mandatory B2B e-invoicing on September 1, 2026. Every VAT-registered French business has to be able to receive structured invoices on that date, and large and mid-sized enterprises have to be able to issue them. The legal invoice is no longer a plain PDF emailed to the customer; it is a structured artifact carrying EN 16931 semantics, delivered through an Approved Platform connected to the public tax hub. Factur-X is the option most existing PDF pipelines can keep using, because it puts a CII XML payload inside a regular-looking PDF/A-3 file. This guide walks through the dates, the format choices, the PDF embedding mechanics, and the validation gates a developer has to clear before the September deadline.
France's September 2026 deadline
The French reform finally has firm dates. From September 1, 2026, every VAT-registered company in France must be able to receive electronic invoices, and large enterprises (over 1.5 billion euros turnover or over 5,000 employees) plus mid-sized enterprises (250 to 5,000 employees and 50 million to 1.5 billion euros turnover) must be able to issue them. Small and micro businesses follow on September 1, 2027 for the issuance obligation (BDO France preparation update, EY simplification alert).
A pilot phase runs from late February 2026 through the end of August 2026, giving accredited Approved Platforms and their customers a six-month window to test the system in live conditions before the wall (Sovos France pilot brief). There is no derogation for foreign suppliers invoicing French B2B customers. If the customer is French, the rule applies, regardless of where the invoice is generated.
Belgium has been live since January 1, 2026 on Peppol BIS Billing 3.0, and Germany has required reception of EN 16931 invoices since January 1, 2025. France is the last of the three large 2026 mandates to flip, but it is also the one with the most syntax flexibility: UBL, pure CII, and Factur-X are all accepted formats (EU Commission factsheet on France).
What Factur-X actually is
Factur-X is a hybrid invoice format. A Factur-X file is a PDF/A-3 document with one CII XML attachment, named exactly factur-x.xml, that carries the same invoice data in machine-readable form. The PDF is the visual rendering for humans; the XML is the legal data for accounting systems and tax authorities. Both travel together as a single file, so accounts payable teams open one attachment and get both views.
The same standard is published under two names. FNFE-MPE publishes the French version as Factur-X, FeRD publishes the German version as ZUGFeRD, and the underlying XSD and Schematron artifacts are identical. Factur-X 1.07.3 and ZUGFeRD 2.3.3 are the same file format, released jointly on May 7, 2025 and legally effective from May 15, 2025 (FNFE-MPE 1.07.3 release notes, zeit.io ZUGFeRD 2.3.3 effective date).
The XML inside the PDF follows the UN/CEFACT Cross Industry Invoice schema (D22B under the Supply Chain Reference Data Model, backward compatible with D16B). The PDF wraps it in PDF/A-3, which is the only PDF/A flavour permitting arbitrary file attachments (ISO 19005-3). The two pieces are linked via the PDF AFRelationship key, which must hold the value Alternative because the XML is a different representation of the same invoice rather than a separate document.
Profile selection: MINIMUM vs BASIC WL vs BASIC vs EN 16931 vs EXTENDED
Factur-X defines five profiles, plus an EXTENDED-CTC-FR variant aligned with the French reform. Each profile constrains which CII elements are mandatory, which are optional, and which are forbidden. Pick the wrong profile and an Approved Platform will reject the invoice on the validator stage.
| Profile | Header-level data | Line-level data | Tax breakdown | Use case |
|---|---|---|---|---|
| MINIMUM | Yes | No | Totals only | Cost accounting attachment, not a legal invoice |
| BASIC WL (without lines) | Yes | No | Per VAT rate | Cost accounting with VAT breakdown, not a legal invoice |
| BASIC | Yes | Yes (essential fields) | Per VAT rate | Simple B2B invoices, single tax rate |
| EN 16931 | Yes | Yes (full semantic model) | Per VAT rate, per category | The mandate-compliant baseline for French and German B2B |
| EXTENDED | Yes | Yes (extra industry codes) | Per VAT rate, per category, multi-currency | Complex invoices: multi-currency, advanced allowances, industry codes |
For the French September 2026 mandate, the practical floor is the EN 16931 profile. MINIMUM and BASIC WL are explicitly marked as "not suitable as a legal invoice" in the FNFE-MPE specification because they omit line-level detail. BASIC works for very simple invoices but does not carry the full set of EN 16931 mandatory fields, and several Approved Platforms reject it for that reason. EXTENDED is reserved for invoices that genuinely need fields outside the European semantic model, such as multi-currency conversions or industry-specific shipping references.
In practice, default to EN 16931 unless your invoicing model has a specific reason to upgrade. Most invoicing SaaS today targets that profile because it is both compliant for France and Germany and acceptable as a Peppol BIS Billing 3.0 alternative payload for Belgium.
What changed in Factur-X 1.07.3 and ZUGFeRD 2.3.3
The May 2025 release is a patch, not a redesign. The underlying schema is unchanged. What changed are the code lists, the validator artifacts, and the rounding rules for EXTENDED (VATupdate release note, vatcalc patch summary).
| Change | Impact on a 1.07.2 pipeline |
|---|---|
| ZWL currency code removed | Any invoice that issued a Zimbabwean dollar amount will fail validation |
| ISO 6523 codes 0239 and 0240 added | New identifier schemes available for party identification |
| VATEX-EU-153 added to VAT exemption reasons | Use this code where applicable; older code lists no longer pass |
| 24 France-specific VAT exemption codes added | French invoices using national exemption regimes now have proper codes |
| EXTENDED rounding tolerance permitted | Small rounding differences that previously failed BR-CO rules now validate |
| XSD and Schematron updated for all five profiles | Old validator JARs need swapping; CI pipelines will report mismatches |
| Backward compatible with UN/CEFACT D16B | Existing producers do not need to rewrite namespaces |
A pipeline that produced valid 1.07.2 invoices does not automatically produce valid 1.07.3 invoices. The code list updates apply from May 15, 2025, so any new invoice issued after that date must use the refreshed lists. The most common breaking surface is the VAT exemption reason code: French B2B invoices to overseas territories, intracommunity supplies, and reverse-charge cases all moved to the new code list and will be rejected by a strict validator if the old codes are kept.
The EXTENDED rounding tolerance is the change that matters most for accounting-driven invoice generation. Before 1.07.3, EXTENDED applied the same BR-CO-15 strict equality check as EN 16931, so a one-cent rounding drift between summed line taxes and invoice-level tax would fail. From 1.07.3, that drift is explicitly permitted within a defined tolerance, matching real accounting software behavior.
Generating the hybrid PDF: the architecture
A Factur-X pipeline has three logical components: a CII XML generator, a PDF/A-3 renderer, and an attachment step. The cleanest split is:
- Compute the invoice data once, as a structured object in your accounting model.
- Serialize the data to a CII XML file for the chosen profile.
- Render an HTML invoice from the same data, then convert it to PDF/A-3.
- Attach the CII XML inside the PDF with
AFRelationship = /Alternative. - Validate the result against both the XSD/Schematron for the profile and VeraPDF for PDF/A-3.
- Submit through an Approved Platform.
Steps 2 and 3 are independent and can run in parallel. The shared input is your invoice object; the two outputs are an XML file and a PDF file. Step 4 merges them.
PDF4.dev handles step 3: it takes an HTML template plus a data object and renders a PDF that becomes the human-readable side of the Factur-X file. The XML generation and the embedding step are not yet part of the PDF4.dev API surface, but they integrate naturally with any open-source CII library on the side. For a Java pipeline, Mustang covers both XML generation and embedding end-to-end. For Python, factur-x plus pikepdf cover the same ground.
Generating the CII XML
A minimal EN 16931 invoice with two lines and standard French VAT looks like this. Element names are inherited from UN/CEFACT CII; the namespaces are stable across Factur-X versions.
<?xml version="1.0" encoding="UTF-8"?>
<rsm:CrossIndustryInvoice
xmlns:rsm="urn:un:unece:uncefact:data:standard:CrossIndustryInvoice:100"
xmlns:ram="urn:un:unece:uncefact:data:standard:ReusableAggregateBusinessInformationEntity:100"
xmlns:udt="urn:un:unece:uncefact:data:standard:UnqualifiedDataType:100">
<rsm:ExchangedDocumentContext>
<ram:GuidelineSpecifiedDocumentContextParameter>
<ram:ID>urn:cen.eu:en16931:2017</ram:ID>
</ram:GuidelineSpecifiedDocumentContextParameter>
</rsm:ExchangedDocumentContext>
<rsm:ExchangedDocument>
<ram:ID>INV-2026-0042</ram:ID>
<ram:TypeCode>380</ram:TypeCode>
<ram:IssueDateTime>
<udt:DateTimeString format="102">20260901</udt:DateTimeString>
</ram:IssueDateTime>
</rsm:ExchangedDocument>
<rsm:SupplyChainTradeTransaction>
<ram:IncludedSupplyChainTradeLineItem>
<ram:AssociatedDocumentLineDocument>
<ram:LineID>1</ram:LineID>
</ram:AssociatedDocumentLineDocument>
<ram:SpecifiedTradeProduct>
<ram:Name>API rendering credits, pack of 10000</ram:Name>
</ram:SpecifiedTradeProduct>
<ram:SpecifiedLineTradeAgreement>
<ram:NetPriceProductTradePrice>
<ram:ChargeAmount>0.0090</ram:ChargeAmount>
</ram:NetPriceProductTradePrice>
</ram:SpecifiedLineTradeAgreement>
<ram:SpecifiedLineTradeDelivery>
<ram:BilledQuantity unitCode="C62">10000</ram:BilledQuantity>
</ram:SpecifiedLineTradeDelivery>
<ram:SpecifiedLineTradeSettlement>
<ram:ApplicableTradeTax>
<ram:TypeCode>VAT</ram:TypeCode>
<ram:CategoryCode>S</ram:CategoryCode>
<ram:RateApplicablePercent>20.00</ram:RateApplicablePercent>
</ram:ApplicableTradeTax>
<ram:SpecifiedTradeSettlementLineMonetarySummation>
<ram:LineTotalAmount>90.00</ram:LineTotalAmount>
</ram:SpecifiedTradeSettlementLineMonetarySummation>
</ram:SpecifiedLineTradeSettlement>
</ram:IncludedSupplyChainTradeLineItem>
<ram:IncludedSupplyChainTradeLineItem>
<ram:AssociatedDocumentLineDocument>
<ram:LineID>2</ram:LineID>
</ram:AssociatedDocumentLineDocument>
<ram:SpecifiedTradeProduct>
<ram:Name>Priority support, monthly</ram:Name>
</ram:SpecifiedTradeProduct>
<ram:SpecifiedLineTradeAgreement>
<ram:NetPriceProductTradePrice>
<ram:ChargeAmount>50.00</ram:ChargeAmount>
</ram:NetPriceProductTradePrice>
</ram:SpecifiedLineTradeAgreement>
<ram:SpecifiedLineTradeDelivery>
<ram:BilledQuantity unitCode="C62">1</ram:BilledQuantity>
</ram:SpecifiedLineTradeDelivery>
<ram:SpecifiedLineTradeSettlement>
<ram:ApplicableTradeTax>
<ram:TypeCode>VAT</ram:TypeCode>
<ram:CategoryCode>S</ram:CategoryCode>
<ram:RateApplicablePercent>20.00</ram:RateApplicablePercent>
</ram:ApplicableTradeTax>
<ram:SpecifiedTradeSettlementLineMonetarySummation>
<ram:LineTotalAmount>50.00</ram:LineTotalAmount>
</ram:SpecifiedTradeSettlementLineMonetarySummation>
</ram:SpecifiedLineTradeSettlement>
</ram:IncludedSupplyChainTradeLineItem>
<ram:ApplicableHeaderTradeAgreement>
<ram:SellerTradeParty>
<ram:Name>PDF4 SAS</ram:Name>
<ram:PostalTradeAddress>
<ram:CountryID>FR</ram:CountryID>
</ram:PostalTradeAddress>
<ram:SpecifiedTaxRegistration>
<ram:ID schemeID="VA">FR12345678901</ram:ID>
</ram:SpecifiedTaxRegistration>
</ram:SellerTradeParty>
<ram:BuyerTradeParty>
<ram:Name>Acme France SARL</ram:Name>
<ram:PostalTradeAddress>
<ram:CountryID>FR</ram:CountryID>
</ram:PostalTradeAddress>
<ram:SpecifiedTaxRegistration>
<ram:ID schemeID="VA">FR98765432109</ram:ID>
</ram:SpecifiedTaxRegistration>
</ram:BuyerTradeParty>
</ram:ApplicableHeaderTradeAgreement>
<ram:ApplicableHeaderTradeSettlement>
<ram:InvoiceCurrencyCode>EUR</ram:InvoiceCurrencyCode>
<ram:ApplicableTradeTax>
<ram:CalculatedAmount>28.00</ram:CalculatedAmount>
<ram:TypeCode>VAT</ram:TypeCode>
<ram:BasisAmount>140.00</ram:BasisAmount>
<ram:CategoryCode>S</ram:CategoryCode>
<ram:RateApplicablePercent>20.00</ram:RateApplicablePercent>
</ram:ApplicableTradeTax>
<ram:SpecifiedTradeSettlementHeaderMonetarySummation>
<ram:LineTotalAmount>140.00</ram:LineTotalAmount>
<ram:TaxBasisTotalAmount>140.00</ram:TaxBasisTotalAmount>
<ram:TaxTotalAmount currencyID="EUR">28.00</ram:TaxTotalAmount>
<ram:GrandTotalAmount>168.00</ram:GrandTotalAmount>
<ram:DuePayableAmount>168.00</ram:DuePayableAmount>
</ram:SpecifiedTradeSettlementHeaderMonetarySummation>
</ram:ApplicableHeaderTradeSettlement>
</rsm:SupplyChainTradeTransaction>
</rsm:CrossIndustryInvoice>Two details that catch first-time implementers. The GuidelineSpecifiedDocumentContextParameter value is what tells a validator which profile to apply; the EN 16931 value is urn:cen.eu:en16931:2017, and EXTENDED is urn:cen.eu:en16931:2017#conformant#urn:factur-x.eu:1p0:extended. The IssueDateTime uses CII date format 102 (which means YYYYMMDD), not ISO-8601 with hyphens; using 2026-09-01 in that field is the single most common XSD error in production logs.
Embedding XML in PDF/A-3
Once the CII XML and the PDF/A-3 renderings are ready, the attach step links them. The relationship must be Alternative because the XML represents the same invoice in a different syntax. Using Source, Data, Supplement, or Unspecified results in a structurally valid PDF that compliant invoice readers will not recognize as a Factur-X file.
import { PDFDocument, PDFName, PDFHexString } from "pdf-lib";
import { readFile, writeFile } from "node:fs/promises";
const pdfBytes = await readFile("./invoice-human.pdf");
const xmlBytes = await readFile("./factur-x.xml");
const pdfDoc = await PDFDocument.load(pdfBytes);
const xmlStream = pdfDoc.context.flateStream(xmlBytes, {
Type: "EmbeddedFile",
Subtype: "application/xml",
Params: {
ModDate: PDFHexString.fromText(new Date().toISOString()),
},
});
const xmlStreamRef = pdfDoc.context.register(xmlStream);
const fileSpec = pdfDoc.context.obj({
Type: "Filespec",
F: PDFHexString.fromText("factur-x.xml"),
UF: PDFHexString.fromText("factur-x.xml"),
AFRelationship: "Alternative",
Desc: PDFHexString.fromText("Factur-X invoice (EN 16931)"),
EF: { F: xmlStreamRef, UF: xmlStreamRef },
});
const fileSpecRef = pdfDoc.context.register(fileSpec);
pdfDoc.catalog.set(PDFName.of("AF"), pdfDoc.context.obj([fileSpecRef]));
await writeFile("./invoice-facturx.pdf", await pdfDoc.save());The PDF structure after the attach step has three new pieces. A new EmbeddedFile stream contains the XML bytes. A Filespec dictionary points to the stream and carries the AFRelationship key with value Alternative. The document catalog gains an AF array referencing the Filespec, which is what compliant readers walk when they look for associated files.
Validation: do not ship rejected invoices
Validation runs on two surfaces. The XML payload is validated against the XSD and Schematron artifacts for the chosen profile, and the PDF envelope is validated against the PDF/A-3 specification. Both must pass.
| Validator | Target | Distribution |
|---|---|---|
| FNFE-MPE XSD + Schematron | CII XML profile compliance | Included in the Factur-X info package on the FNFE-MPE site |
| Mustang validator | CII XML and PDF/A-3 envelope | Java JAR from the Mustang project releases |
| VeraPDF | PDF/A-3 conformance | Open-source from verapdf.org |
| pyFacturX | Python validation of XML and PDF | PyPI package |
| Quba ZUGFeRD validator | XML and PDF (German branding) | Quba site |
A practical CI pipeline runs Mustang for the XML side and VeraPDF for the PDF side on every generated invoice. Both produce machine-readable output (JSON for VeraPDF, XML for Mustang) that integrates into a build report. Catching a missing AFRelationship in CI costs one second; catching it after an Approved Platform rejects the invoice costs a phone call to a customer.
PPF vs PDP: which submission channel
The submission architecture changed in the most recent revision of the French reform. The PPF is no longer the operational portal businesses send invoices to. It is now a tax data hub and a buyer directory (Avalara compliance tour).
| Channel | Role | When you interact with it |
|---|---|---|
| PPF (Portail Public de Facturation) | Central tax data hub and buyer directory operated by AIFE for DGFiP | Indirectly; your Approved Platform forwards invoice lifecycle data and tax data to it |
| AP (Approved Platform, formerly PDP) | Accredited private platform that exchanges invoices on the supplier and buyer side | Directly; you integrate one AP API and use it for all sending and receiving |
| OD (formerly Compatible Solution, SC) | Software that connects to an AP but is not itself accredited | Indirectly; your invoicing tool sends to its associated AP |
For most SaaS, the integration target is a single AP. You pick the platform once, integrate one API, and that platform exchanges with every other AP in the network and forwards data to the PPF. There is no direct PPF API for invoice transmission in the current architecture. The list of accredited APs is maintained by the DGFiP and updated as new platforms complete certification (service-public.fr e-invoicing overview).
Using PDF4.dev for the human-readable side
PDF4.dev focuses on the visual rendering. The HTML template plus data model is mapped to a PDF that becomes the human-readable side of the Factur-X file. The CII XML and the embedding step happen outside the PDF4.dev API call, using one of the libraries above.
A minimal EN 16931 invoice template in PDF4.dev:
<style>
body { font-family: 'Inter', sans-serif; padding: 20mm; color: #111827; }
h1 { font-size: 28px; margin-bottom: 4px; }
.meta { color: #6b7280; font-size: 12px; }
table { width: 100%; border-collapse: collapse; margin-top: 24px; }
th, td { padding: 8px 12px; text-align: left; border-bottom: 1px solid #e5e7eb; }
th { background: #f9fafb; font-size: 11px; text-transform: uppercase; }
.totals { margin-top: 16px; width: 50%; margin-left: auto; }
.grand { font-weight: 700; font-size: 18px; }
</style>
<h1>Invoice {{invoice_number}}</h1>
<div class="meta">
Issued {{formatDate issue_date "dd MMM yyyy"}} ยท
Due {{formatDate due_date "dd MMM yyyy"}}
</div>
<div style="display: flex; gap: 40px; margin-top: 24px;">
<div>
<div class="meta">From</div>
<strong>{{seller.name}}</strong><br />
{{seller.address}}<br />
VAT {{seller.vat}}
</div>
<div>
<div class="meta">To</div>
<strong>{{buyer.name}}</strong><br />
{{buyer.address}}<br />
VAT {{buyer.vat}}
</div>
</div>
<table>
<thead>
<tr><th>Description</th><th>Qty</th><th>Unit</th><th>VAT</th><th>Total</th></tr>
</thead>
<tbody>
{{#each lines}}
<tr>
<td>{{description}}</td>
<td>{{quantity}}</td>
<td>{{formatCurrency unit_price "EUR" "fr-FR"}}</td>
<td>{{vat_rate}}%</td>
<td>{{formatCurrency line_total "EUR" "fr-FR"}}</td>
</tr>
{{/each}}
</tbody>
</table>
<table class="totals">
<tr><td>Subtotal</td><td>{{formatCurrency subtotal "EUR" "fr-FR"}}</td></tr>
<tr><td>VAT (20%)</td><td>{{formatCurrency vat_total "EUR" "fr-FR"}}</td></tr>
<tr class="grand"><td>Total due</td><td>{{formatCurrency grand_total "EUR" "fr-FR"}}</td></tr>
</table>Rendered via the API:
curl -X POST https://pdf4.dev/api/v1/render \
-H "Authorization: Bearer p4_live_your_key" \
-H "Content-Type: application/json" \
-d '{
"template_id": "tmpl_facturx_en16931",
"data": {
"invoice_number": "INV-2026-0042",
"issue_date": "2026-09-01",
"due_date": "2026-10-01",
"seller": {
"name": "PDF4 SAS",
"address": "Paris, France",
"vat": "FR12345678901"
},
"buyer": {
"name": "Acme France SARL",
"address": "Lyon, France",
"vat": "FR98765432109"
},
"lines": [
{
"description": "API rendering credits, pack of 10000",
"quantity": 1,
"unit_price": 90.00,
"vat_rate": 20,
"line_total": 90.00
},
{
"description": "Priority support, monthly",
"quantity": 1,
"unit_price": 50.00,
"vat_rate": 20,
"line_total": 50.00
}
],
"subtotal": 140.00,
"vat_total": 28.00,
"grand_total": 168.00
}
}' -o invoice-human.pdfThe resulting PDF goes through the Ghostscript PDF/A-3 conversion described in the PDF/A compliance guide, then through the attach step from the previous section. The final file is what an Approved Platform accepts.
FAQ traps
Six recurring failure modes that show up in Approved Platform rejection logs.
The XML attachment name has to be exactly factur-x.xml. Compliant readers do a case-sensitive filename lookup. Factur-X.xml, FacturX.xml, or invoice.xml all turn the file into a regular PDF with an unrecognized attachment. Older ZUGFeRD readers also accept zugferd-invoice.xml for backwards compatibility, but the canonical name today is factur-x.xml.
The AFRelationship value has to be Alternative, not Data. Some PDF libraries default to Unspecified or Data because those are valid PDF values, but Factur-X requires Alternative to declare that the XML is the same invoice in another representation. A wrong relationship value is the silent failure mode: the PDF opens fine, the human-readable side looks perfect, the accounting system never finds the data.
The CII date format is 102 (YYYYMMDD), not ISO-8601. Putting 2026-09-01 in an IssueDateTime element triggers an XSD violation immediately. The same trap applies to DueDateDateTime and TaxPointDate.
The GuidelineSpecifiedDocumentContextParameter has to match the profile. The most common mismatch is generating an EXTENDED-shaped XML with the EN 16931 URN, or vice versa. The Schematron rules differ between profiles, and a mismatch fails validation even if the data looks correct.
Timezone drift on the IssueDate. CII dates are interpreted in the supplier's local timezone but stored without an offset. A worker running in UTC that derives the date from new Date().toISOString().slice(0, 8).replace(/-/g, "") on September 1, 2026 at 23:30 Paris time writes 20260902 and the invoice falls into the wrong VAT period.
The PDF/A-3 declaration in XMP. Playwright and Chromium do not emit PDF/A-3 XMP metadata by default. A PDF that contains the right CII XML attachment but does not declare PDF/A-3 conformance in its XMP block fails the envelope validation step, even though the visual content is fine. The fix is the Ghostscript pass with -dPDFA=3 after rendering.
What is next: 2027 EU mandate harmonization
September 2026 is a national deadline, but the European VAT in the Digital Age (ViDA) package pushes a broader cross-border harmonization from July 1, 2030, with intermediate steps around 2027 for digital reporting. Germany's full issuance obligation kicks in for businesses over 800,000 euros annual turnover on January 1, 2027 and for all remaining businesses on January 1, 2028. France's small and micro businesses join the issuance obligation on September 1, 2027.
The pragmatic path for any SaaS issuing invoices to French or German B2B customers is to settle on Factur-X 1.07.3 / EN 16931 today, build the embedding pipeline once, and validate every generated invoice in CI. The format will keep getting patches (Factur-X 1.08 / ZUGFeRD 2.4 was already announced in December 2025 with further code list updates), but the architecture stays the same: CII XML inside a PDF/A-3 envelope, sent through an Approved Platform, mirrored to the PPF for tax data. The deadline is firm, the format is stable enough to build against, and the only thing left is the implementation.
Start generating PDFs
Build PDF templates with a visual editor. Render them via API from any language in ~300ms.



