How to Handle PDF/A Archival Compliance in Your Document Pipeline
If your application generates contracts, invoices, medical records, or any document that needs to survive a decade or two of regulatory scrutiny, regular PDFs are not enough. You need PDF/A — an ISO-standardized subset of PDF designed for long-term preservation. The catch: getting PDF/A right is harder than it looks, and a "looks fine in Acrobat" PDF will fail compliance the moment an auditor runs it through veraPDF.
This post walks through what PDF/A actually requires, where most document pipelines break, and how to bake compliance into your generation flow.
What PDF/A actually requires
PDF/A is not a single format. The flavors that matter in practice:
- PDF/A-1b — Visual reproducibility. The minimum bar. Based on PDF 1.4.
- PDF/A-2b — Adds JPEG2000, transparency, layers. Based on PDF 1.7.
- PDF/A-3b — Same as 2b, but allows embedded files (useful for ZUGFeRD/Factur-X e-invoicing).
- The
avariants (1a, 2a, 3a) additionally require tagged structure for accessibility.
Across all of them, you must satisfy roughly the same hard rules:
- All fonts must be embedded — no font substitution at viewing time.
- No encryption — the document must be readable forever without keys.
- Device-independent color — either use an output intent (ICC profile) or stick to calibrated color spaces.
- No external dependencies — no JavaScript, no audio/video, no references to external resources.
- Valid XMP metadata — including a
pdfaid:partandpdfaid:conformanceclaim. - No transparency (PDF/A-1 only) — alpha channels are forbidden in the 1.x line.
Skip any of these and a validator will reject the file.
Where pipelines typically break
Most teams generate PDFs from HTML using a headless browser or from a templating library. Both paths have predictable failure modes:
- Web fonts loaded via
@font-facefrom a CDN — the browser rasterizes them fine, but the resulting PDF may reference subsetted fonts without embedding the full encoding tables PDF/A requires. - CSS
rgba()colors with alpha — produces transparency objects that violate PDF/A-1. - System fonts (Arial, Helvetica) — often referenced by name without being embedded.
- Missing XMP packet — even a perfectly rendered PDF will fail without the
<rdf:Description>block declaring conformance. - sRGB color used without an output intent — flags color management warnings.
A working approach
Here is the pattern I recommend regardless of which library you use:
Step 1: Lock down fonts. Self-host your fonts and explicitly embed them. If you are using Puppeteer or Playwright, preload the font files locally and reference them by file:// URLs.
tsawait page.addStyleTag({ content: ` @font-face { font-family: "Inter"; src: url("file:///app/fonts/Inter-Regular.ttf") format("truetype"); font-weight: 400; } body { font-family: "Inter", sans-serif; } `, });
Step 2: Strip transparency. Audit your CSS for rgba(), opacity, and box-shadow. Convert them to flat equivalents if you are targeting PDF/A-1.
Step 3: Inject an output intent and XMP metadata. This is the step nearly every homegrown solution gets wrong. You need to post-process the generated PDF to attach an ICC profile (typically sRGB IEC61966-2.1) and write the XMP packet.
Using pdf-lib plus a separate tool like Ghostscript:
bashgs -dPDFA=2 \ -dBATCH \ -dNOPAUSE \ -sColorConversionStrategy=UseDeviceIndependentColor \ -sDEVICE=pdfwrite \ -sPDFACompatibilityPolicy=1 \ -sOutputFile=output-pdfa.pdf \ PDFA_def.ps input.pdf
You'll need a PDFA_def.ps file that references your ICC profile. Ghostscript ships an example you can adapt.
Step 4: Validate. Never trust your own pipeline. Run every output through veraPDF before considering the file compliant.
bashverapdf --flavour 2b --format text output-pdfa.pdf
A passing run looks like this:
txtPASS output-pdfa.pdf
Anything else, including warnings, should fail your CI build.
Doing this with Kamy
If you'd rather not maintain a Ghostscript install, font catalog, and ICC profile in your Docker image, Kamy can render directly to PDF/A. You pass a conformance option and the API handles font embedding, output intent, and metadata injection:
tsimport { kamy } from "@kamydev/sdk"; const pdf = await kamy.documents.render({ template: "patient-record", data: { patientId: "P-9281", visitDate: "2025-03-14" }, output: { format: "pdf", conformance: "PDF/A-2b", }, }); console.log(pdf.url);
The returned file is already validated against veraPDF on our side, but you should still run validation as part of your own pipeline if you have hard regulatory requirements.
Archive-side concerns
Compliance does not end at generation. A few things to plan for:
- **Storage imm