Get started
EU e-invoicing mandates 2026: a developer guide

EU e-invoicing mandates 2026: a developer guide

Belgium, Poland and France flip mandatory B2B e-invoicing in 2026. What developers need to ship: EN 16931 XML, Factur-X PDF/A-3, KSeF FA(3), Peppol BIS 3.0.

14 min read

Belgium, Poland and France all flip mandatory B2B e-invoicing live in 2026. The plain PDF you have been emailing to customers stops being a valid invoice in those countries, and what replaces it is structured XML conforming to the EN 16931 European semantic standard. This guide walks through the dates, formats and channel choices a developer needs to make to keep invoices flowing without losing the PDF that humans still want to look at.

The 2026 e-invoicing wall

Three EU countries cross the mandate line in 2026, each with its own format and channel. Belgium goes first on January 1, 2026, with no threshold and no phasing: every VAT-registered business has to issue and receive Peppol BIS Billing 3.0 invoices over the Peppol network (EY tax alert). Poland follows on February 1, 2026 for taxpayers above PLN 200 million (about 46 million euros) of 2024 turnover, then April 1, 2026 for everyone else, all using the proprietary KSeF FA(3) XML schema and the state-run KSeF platform (Tradeshift KSeF brief). France pulls the last lever on September 1, 2026 with a receive-only obligation for every business and an issuing obligation for large enterprises (over 1.5 billion euros turnover or over 5,000 employees) and mid-sized enterprises (250 employees to 5,000 or 50 million to 1.5 billion euros), with small and micro businesses joining on September 1, 2027 (EY France simplification alert).

If you sell to customers in any of those three countries from any country in the world, you have a 2026 deadline. The compliant artefact is structured XML. Your existing PDF pipeline is not enough on its own.

Country-by-country breakdown

Every country picks its own format and its own channel. The semantics are unified by EN 16931, but the syntax (UBL vs CII vs FA(3)) and the transport (Peppol vs KSeF vs PPF) are not. Here is the picture as of April 2026.

CountryIn-scopeEffective dateFormatChannelSource
BelgiumDomestic B2B, all VAT-registeredJanuary 1, 2026 (no phase)Peppol BIS Billing 3.0 (UBL, EN 16931)Peppol four-corner networkEU Commission factsheet
PolandDomestic B2BFebruary 1, 2026 (over PLN 200M turnover); April 1, 2026 (other VAT-registered); January 1, 2027 (micro)KSeF FA(3) XML (proprietary, EN 16931 mappable)KSeF state platform APIEY Poland alert
FranceDomestic B2B receive: all; issue: large + mid-sizedSeptember 1, 2026 (receive all, issue large + mid-sized); September 1, 2027 (issue small/micro)UBL, UN/CEFACT CII, or Factur-X PDF/A-3 (EN 16931)PPF directory + certified PDPEY France alert
GermanyDomestic B2B receive: all (since 2025); issue: phasedJanuary 1, 2025 (receive all); January 1, 2027 (issue over 800,000 euros); January 1, 2028 (issue all)EN 16931 (XRechnung UBL/CII or ZUGFeRD hybrid)Bilateral or PeppolFonoa EU changes
ItalyDomestic B2B/B2C/B2GIn force since 2019 (benchmark)FatturaPA XMLSDI Sistema di InterscambioKlippa regulations guide

A few traps in this table worth pulling out.

Belgium has no turnover threshold and no phased rollout, but it does have a three-month tolerance period through March 31, 2026 during which the tax authority will not apply penalties for non-compliance (Tradeshift Belgium tolerance). The legal obligation still starts January 1.

Poland is the odd one out because it does not use Peppol and does not accept any other EN 16931 syntax. Every B2B invoice has to be submitted to the KSeF API in the FA(3) schema, the state platform timestamps it, and only then does the invoice exist legally. There is no opt-out for foreign software that prefers UBL.

France is more flexible on the wire format. A supplier can send UBL, UN/CEFACT CII, or Factur-X. The transport always goes through a Plateforme de Dématérialisation Partenaire (PDP) connected to the Portail Public de Facturation (PPF), which acts as the central directory of buyers and routes invoices.

Germany and Italy are the bookends. Italy's SDI has been mandatory since 2019 and is the benchmark for what a fully clearance-based system looks like in production. Germany started receiving in January 2025 and ramps issuance through 2027 and 2028, so it will not actually flip in 2026, but anyone selling to German B2B customers needs to be able to receive XRechnung or ZUGFeRD invoices today.

The four formats developers will encounter

EN 16931 defines the semantic model: every invoice has a supplier party, a customer party, lines, totals, taxes, and so on. The standard does not pick a syntax. National mandates pick the syntax for you, and you will deal with four in 2026.

EN 16931 UBL (Peppol BIS Billing 3.0)

This is the OASIS Universal Business Language XML profile constrained by Peppol BIS Billing 3.0 (released November 2025, Peppol docs). Belgium's January 2026 mandate uses it as the default and preferred format. France accepts it as one of three syntaxes. Element names are inherited directly from EN 16931, identifiers come from ISO 6523 ICD codes for parties and from the Electronic Address Scheme (EAS) for routing.

A bare-bones UBL invoice looks like this:

<Invoice xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2"
         xmlns:cbc="urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2"
         xmlns:cac="urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2">
  <cbc:CustomizationID>urn:cen.eu:en16931:2017#compliant#urn:fdc:peppol.eu:2017:poacc:billing:3.0</cbc:CustomizationID>
  <cbc:ProfileID>urn:fdc:peppol.eu:2017:poacc:billing:01:1.0</cbc:ProfileID>
  <cbc:ID>INV-2026-001</cbc:ID>
  <cbc:IssueDate>2026-04-15</cbc:IssueDate>
  <cbc:DueDate>2026-05-15</cbc:DueDate>
  <cbc:InvoiceTypeCode>380</cbc:InvoiceTypeCode>
  <cbc:DocumentCurrencyCode>EUR</cbc:DocumentCurrencyCode>
  <!-- AccountingSupplierParty, AccountingCustomerParty, InvoiceLine, TaxTotal, LegalMonetaryTotal -->
</Invoice>

The CustomizationID is the part validators read first: it declares "this document conforms to EN 16931 and to the Peppol BIS Billing 3.0 customization." Get that wrong and a Peppol Access Point will reject the file before it leaves your tenant.

EN 16931 CII (UN/CEFACT Cross Industry Invoice)

CII is the alternative XML syntax based on the UN/CEFACT Cross Industry Invoice schema. It is functionally equivalent to UBL but the element names differ (rsm:CrossIndustryInvoice instead of Invoice, namespaces from urn:un:unece:uncefact). France accepts pure CII and so does Germany via XRechnung and ZUGFeRD's CII profiles.

CII matters most because Factur-X embeds CII (not UBL) inside the PDF, so even if you start your pipeline with UBL you will end up generating CII for hybrid invoices.

Factur-X / ZUGFeRD hybrid PDF/A-3

Factur-X and ZUGFeRD are the same standard with two names: Factur-X is the French branding by FNFE-MPE, ZUGFeRD is the German branding by FeRD. The current spec is Factur-X 1.07.2 / ZUGFeRD 2.3.2, released November 15, 2024 (VATupdate release note).

A Factur-X file is a PDF/A-3 with one CII XML attachment named factur-x.xml. The PDF page content is the human-readable invoice. The XML is the legal data. PDF/A-3 (ISO 19005-3) is the only PDF/A flavour that allows embedding arbitrary attachments, which is why the standard mandates it. The link between the PDF and the XML is declared via the AFRelationship key on the embedded file specification, and it must be Alternative (/AFRelationship /Alternative) so that consuming software knows the XML is a different representation of the same invoice rather than a separate document.

The standard has six profiles: MINIMUM, BASIC WL, BASIC, EN 16931, EXTENDED, and the EXTENDED-CTC-FR profile aligned with France's e-invoicing reform. The EN 16931 profile is the floor for cross-border B2B compliance.

KSeF FA(3)

Poland's FA(3) is a proprietary XML schema published by the Ministry of Finance. It maps onto EN 16931 semantically but the element names and the structure are Polish. There is no Peppol gateway for it: every invoice goes through the KSeF API directly. The KSeF platform stamps a unique number on the invoice, and that stamp is what makes the invoice legally exist. If you generate a beautiful UBL or Factur-X file for a Polish B2B customer, KSeF will not accept it.

Why PDF still matters

The structured XML is the legal invoice. The PDF is for humans, and humans are still in the loop in 2026.

Three places where the PDF stays load-bearing:

The accounts payable team on the receiving side opens email attachments, looks at amounts, and needs a layout that reads like an invoice. Many companies route XML invoices into their ERP and use the PDF as a backup for visual verification, dispute resolution, and audit trails. A non-technical user does not parse UBL.

Long-term archival is still PDF/A in most jurisdictions. France requires 10 years, Germany 10 years, Belgium 7 years, Poland 5 years (Klippa archival comparison). PDF/A-3 with embedded XML satisfies both the visual record and the data record in a single file, which is exactly why Factur-X and ZUGFeRD exist.

Customer expectations have not flipped overnight. Buyers without an ERP, B2C edge cases, manual approval workflows, all still expect a PDF. The Belgian tolerance period in Q1 2026 exists precisely because real businesses cannot rip out their PDF-only workflow on January 1.

Building a hybrid invoice pipeline

The sustainable architecture for 2026 is: render the human-readable invoice as PDF/A-3, generate the EN 16931 XML in parallel, embed the XML inside the PDF for Belgium / France / Germany, and call the appropriate transport API per country.

Here is the embedding step in two languages.

import { PDFDocument, PDFName, PDFHexString, PDFRawStream } from "pdf-lib";
import { readFile, writeFile } from "node:fs/promises";
 
// 1. Render the human-readable PDF/A-3 from your HTML template
//    (use your existing renderer here, e.g. PDF4.dev /api/v1/render)
const pdfBytes = await readFile("./invoice-human.pdf");
const xmlBytes = await readFile("./factur-x.xml");
 
const pdfDoc = await PDFDocument.load(pdfBytes);
 
// 2. Embed factur-x.xml as an associated file with AFRelationship = Alternative
const xmlStream = pdfDoc.context.flateStream(xmlBytes, {
  Type: "EmbeddedFile",
  Subtype: "text/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 XML invoice (EN 16931)"),
  EF: { F: xmlStreamRef, UF: xmlStreamRef },
});
const fileSpecRef = pdfDoc.context.register(fileSpec);
 
// 3. Wire it into the catalog: AF + Names tree
pdfDoc.catalog.set(PDFName.of("AF"), pdfDoc.context.obj([fileSpecRef]));
 
await writeFile("./invoice-facturx.pdf", await pdfDoc.save());

Two details developers miss the first time.

The XML attachment must be named exactly factur-x.xml for Factur-X (or zugferd-invoice.xml for older ZUGFeRD readers, kept for backwards compatibility). Compliant invoice readers look for that exact filename. A wrong name turns the file into a regular PDF with a random attachment.

The AFRelationship value must be /Alternative (capital A). /Source, /Data, /Supplement, and /Unspecified are valid PDF values but they do not declare the XML as the same invoice in another representation, and validators like Mustang or the FNFE-MPE validation tool will reject the file.

Once the hybrid PDF/A-3 is built, the transport step is country-specific. For Belgium, push it to a Peppol Access Point with the customer's Peppol participant identifier (0208:0123456789 for a Belgian VAT number under ICD 0208). For France, route it through your chosen PDP. For Germany, send it directly or via Peppol depending on the customer's preference. For Poland, drop the PDF entirely and call the KSeF API with the FA(3) XML.

Common pitfalls

A few sharp edges that have ruined production rollouts.

Peppol identifiers are not VAT numbers. A Peppol participant ID is <scheme>:<value> where <scheme> is an ISO 6523 ICD. Belgian VAT is scheme 0208, German VAT scheme is 9930, French SIRET is 0009, French SIREN is 0002. The participant ID for a Belgian company with VAT BE0123456789 is 0208:0123456789, not BE0123456789 and not 0208:BE0123456789. Sending the wrong scheme code is the most common reason a Peppol Access Point bounces the invoice.

Rounding is the second pitfall. EN 16931 says line totals are rounded to two decimals (BR-CO-04, BR-CO-09 business rules) but per-line tax can be rounded differently from invoice-level tax. A common bug: compute the tax in JavaScript with (price * qty * vatRate).toFixed(2), sum the lines, then re-derive invoice VAT from the gross total. The two numbers will not match by 0.01 EUR on multi-line invoices, validators flag it as BR-CO-15 violation, and Peppol Access Points reject the file. The fix is to compute tax per line first, round each line tax, and sum the rounded line taxes for the invoice tax total.

VAT rules are deceptively local. Reverse charge B2B intra-EU uses K exempt category code and a clause reference (Reverse charge) that has to appear both as a code and as a free-text note. Domestic French B2B with frais de transport refacturés uses different VAT subtotals from a Belgian B2B with intracommunity supply. The EN 16931 codes are S (standard rate), Z (zero rated), E (exempt), AE (reverse charge), K (intra-EU), G (export), O (out of scope). Mapping your domestic accounting flags to these codes is where most projects spend the longest.

Timezones. IssueDate, DueDate and TaxPointDate are dates, not datetimes. They are interpreted in the supplier's local timezone but serialized as YYYY-MM-DD with no offset. A worker running in UTC that picks new Date().toISOString().slice(0,10) for a French supplier at 23:30 Paris time will write tomorrow's date and break tax period reporting. Always derive the date in the supplier's timezone.

AFRelationship mistakes are silent. PDF/A-3 readers will happily accept any value, the file opens fine, the human-readable side looks great, the customer's accounting software just never finds the invoice data. Always validate the output with a Factur-X validator before promoting the pipeline to production.

KSeF deadlines are wall-clock, not request-clock. Polish FA(3) invoices have to be submitted to KSeF at the moment the supply is recognized, not when the user clicks "send" in the UI. A nightly batch that uploads invoices the next morning is technically late even if the invoice number sequence is fine. The KSeF API returns the official invoice number on success and that is the number that goes on any human-readable PDF rendition.

What this means for SaaS doing invoicing in 2026

If your SaaS bills customers in any of Belgium, Poland or France, your invoice generation pipeline is now a compliance system, not a feature. The questions that matter for the architecture review:

Where is the EN 16931 XML generated? It cannot be a side effect of a PDF template — it has to be the source of truth, and the PDF has to be a rendering of the same data so the two never drift.

Which transport channels do you support? At minimum a Peppol Access Point integration (Belgium, France optional, Germany optional) and a KSeF API client (Poland mandatory, no alternatives). Most SaaS vendors will integrate a service provider rather than become a Peppol Access Point themselves; that is fine, but the abstraction needs to handle reception failures, retransmissions and identifier resolution.

How do you store the artefacts? The legal invoice for B2B Belgium is the UBL XML received over Peppol. The legal invoice for B2B Poland is the FA(3) timestamped by KSeF. Storing only the PDF is no longer enough for audit. Plan for parallel storage of the structured XML and the PDF, ideally as a single Factur-X PDF/A-3 file where the format allows.

How do you migrate existing customers? Belgium gives you three months of tolerance, France gives you a year for SMEs, Poland gives you two months between the large-taxpayer and the all-business deadline. None of those is a long runway. The work to do is mostly schema mapping (your accounting flags to EN 16931 codes), party identifier collection (Peppol participant IDs for every B2B customer), and validation (every issued invoice runs through an EN 16931 validator before it leaves the system).

PDF4.dev today renders structured HTML to PDF and can produce PDF/A output suitable as the human-readable side of a Factur-X file. We do not yet generate the EN 16931 XML or perform the embedding step described above; that part of the pipeline is on the roadmap and we will document it explicitly when it ships. Until then, pair PDF4.dev with a dedicated EN 16931 library (Mustang for Java, factur-x for Python, or one of the Node.js packages on npm) for the XML side, and run the embedding step yourself or via a service provider.

The 2026 wall is real, the dates are not moving in any of the three countries, and the format choices are settled. The pragmatic answer is to keep what you have for the human-readable side, add the structured XML as a first-class artefact next to it, and pick the transport per country. The PDF is not going away. It just stops being the legal document.

Start generating PDFs

Build PDF templates with a visual editor. Render them via API from any language in ~300ms.