If you’re an engineer who just got told “the invoices need to be PDF/A-3 with Factur-X by next quarter”, and your only context is that someone in legal said the words, this post is for you.
We’re going to cut through the standards-document tone and explain what these profiles actually constrain, why governments started mandating them, and the smallest practical pipeline for emitting a compliant PDF from a structured-data renderer.
PDF/A in two paragraphs
PDF is a flexible format. Too flexible — the same PDF spec lets you embed JavaScript, link to external resources that may not exist in 50 years, encrypt content with reversible cryptography, reference external fonts, and a hundred other things that make a document non-self-contained.
PDF/A (“A” for Archival) is a profile of PDF that bans the parts which would prevent the document from rendering identically in 50 years. The high-level rules:
- All fonts must be embedded.
- No JavaScript, no external links, no audio/video.
- No encryption.
- All transparency must be flattened or supported by the profile version.
- Colours must be device-independent (ICC profile required).
- All content must be inside the file — no references that depend on the network.
There are several versions, each adding tolerance for newer features:
| Profile | Year | What it adds |
|---|---|---|
| PDF/A-1b | 2005 | Original baseline — strictest |
| PDF/A-2b | 2011 | Allows JPEG2000, transparency, layers |
| PDF/A-3b | 2012 | Allows arbitrary file attachments (the foundation of Factur-X) |
| PDF/A-4 | 2020 | ISO 32000-2 (PDF 2.0) base, simplified conformance levels |
The “b” suffix means “basic” conformance (visual fidelity). There are also “u” (unicode-mapped) and “a” (accessibility-tagged) variants — for most invoice/receipt workflows, “b” is what you want, because tax archival cares about visual reproducibility, not screen-reader semantics.
Practical takeaway: if your renderer says it supports PDF/A-3b, it should be a single config flag ({ profile: "PDF/A-3b" } or equivalent). If you have to run a second tool (Ghostscript, qpdf, Acrobat) to convert afterwards, that’s a workflow gap to factor into your ops.
Why PDF/A-3 specifically matters: it’s the carrier for e-invoices
PDF/A-3 added one capability that turned out to be world-changing: arbitrary file attachments inside the PDF.
This sounds boring. It isn’t. It’s the entire technical foundation for the e-invoice mandates rolling out across Europe right now.
The architecture: a single PDF file that is both
- A human-readable invoice (visual layout, totals, branding) — the part the human reads.
- A machine-readable XML invoice — the part the tax authority’s software parses.
Both inside one file, both representing the same invoice, and the PDF/A-3 wrapper guarantees the file will still be parseable in decades.
The two main XML formats:
- Factur-X (France) — XML profile based on UN/CEFACT Cross Industry Invoice
- ZUGFeRD (Germany) — substantively identical to Factur-X (the two standards merged technically in 2018)
- EN 16931 — the European norm both implementations conform to
For most workflows, “Factur-X” and “ZUGFeRD” are interchangeable terms — they share a schema, share the embedding mechanism, and a single PDF that’s compliant with one is generally compliant with the other.
What’s mandatory, where, when
A non-exhaustive snapshot for engineers planning Q2/Q3 2026 rollouts:
| Country | Status | Required format |
|---|---|---|
| Germany | B2B mandatory from 2025-01-01 for invoice receipt; from 2027 also for issuing | EN 16931 (Factur-X / ZUGFeRD / XRechnung) |
| France | Mandatory issuing for large enterprises 2026-09; SMEs 2027-09 | Factur-X via Chorus Pro |
| Italy | B2B mandatory since 2019 | FatturaPA via SDI |
| Poland | Mandatory since 2024-07 | KSeF |
| Spain | Mandatory from 2026 (B2B) | Facturae via FACe |
| Belgium | Mandatory from 2026-01 | Peppol BIS 3 |
The pattern: every EU member state is implementing some flavour of EN 16931–compliant e-invoicing on a 2024–2027 timeline. If your customers operate in any of these markets, your PDF generator will need to emit attached XML alongside the visual invoice.
The smallest practical pipeline
Forget what the standards documents prescribe. Here’s the engineering view:
┌─────────────────────┐
│ Your invoice data │ (already a JSON object somewhere)
└─────────┬───────────┘
│
▼
┌─────────────────────┐
│ Build EN 16931 XML │ (deterministic mapping; well-tested libs exist)
└─────────┬───────────┘
│
▼
┌─────────────────────┐
│ Render PDF/A-3b + │
│ attach the XML │ (single API call to gPdf — or two-step elsewhere)
└─────────┬───────────┘
│
▼
┌─────────────────────┐
│ Hand off to │
│ Chorus Pro / SDI / │
│ Peppol / etc │
└─────────────────────┘
The two non-trivial steps:
Step 1: build the XML
This is annoying but mechanical. You map your invoice data (lines, taxes, totals, parties) to EN 16931 XML field names. Several Java/Node/Python libraries do this for you — search for “factur-x library” in your language. Don’t write it from scratch unless you genuinely enjoy XML schema specs.
Step 2: render PDF/A-3 and attach the XML
This is where renderer choice matters.
Without built-in support: you render an ordinary PDF, then post-process with a tool that converts to PDF/A-3 and attaches the XML as an embedded file. Common stacks: Ghostscript + qpdf, or a paid tool like Aspose. Two extra steps, two extra failure points, and you have to ensure the post-processing doesn’t drift the visual layout.
With built-in support (gPdf’s approach): one call.
curl -X POST https://api.gpdf.com/api/v1/e-invoice/render \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
--data '{
"settings": {
"profile": "pdfa-3b",
"e_invoice": {
"standard": "factur_x",
"profile": "en16931",
"document_type": "invoice",
"xml": {
"format": "cii",
"encoding": "utf8",
"content": "<?xml version=\"1.0\"?><rsm:CrossIndustryInvoice>...</rsm:CrossIndustryInvoice>"
}
}
},
"pages": [{ "size": "a4", "elements": [/* invoice layout */] }]
}' \
--output invoice-with-einvoice.pdf
That’s the whole pipeline. The renderer emits PDF/A-3b, attaches your XML as factur-x.xml (or zugferd-invoice.xml, both are recognised by every consumer), and returns the bytes.
Common gotchas
A few things people learn the hard way:
“PDF/A” and “with PDF/A-compliant fonts” aren’t the same
A PDF/A-3 file requires all fonts to be embedded with full character coverage of glyphs used. If your invoice has a Japanese customer name and the renderer falls back to a font that isn’t fully embeddable, validation tools will reject it. Check that your renderer embeds CJK fonts in PDF/A mode — many don’t, by default.
Visual + XML must agree
The XML invoice and the visual invoice are supposed to represent the same invoice. Tax auditors will diff them. If your code emits XML with total: 119.00 and the visual PDF shows Total: 120.00 (because of a rounding bug or a stale template), you have a tax discrepancy on file. Generate both from the same source-of-truth, ideally in the same code path.
”Profile” levels in EN 16931
Factur-X has profiles: MINIMUM, BASIC, EN 16931, EXTENDED. They differ in how much data is in the XML. Use BASIC unless your customer specifically requires more — it covers tax codes, line items, parties, totals, which is enough for ~95% of B2B invoicing. EN 16931 profile adds further detail for edge cases.
Validation before submission
Always validate the generated PDF against a PDF/A validator (veraPDF is the open-source standard) and validate the XML against the EN 16931 schema before you ship to the tax authority. Failed submissions to Chorus Pro / SDI count against your reliability metrics with the regulator.
TL;DR
PDF/A is a self-contained-document profile. PDF/A-3 lets you attach files. Factur-X / ZUGFeRD is “an EN 16931 XML attached inside a PDF/A-3”. E-invoice mandates across the EU make this combination the de facto B2B invoice format from 2025–2027.
If your renderer treats PDF/A-3 + Factur-X as a single config flag, the migration is mechanical. If it doesn’t, you’re building a multi-step ops pipeline. gPdf’s /api/v1/e-invoice/render is the single-flag version — the API reference has the full schema, or try a sample render in the Playground.