DEV Community

PDF4me
PDF4me

Posted on

A PDF a Human Reads and a Machine Parses at the Same Time: How PDF4me Builds ZUGFeRD E-Invoices

Picture the scenario: your invoicing pipeline generates a clean, branded PDF for a German B2B customer. It looks right. It would print fine, email fine, and satisfy anyone who opens it by hand. Then it bounces, because since January 1, 2025, that customer is legally required to receive invoices in a format their software can parse without a human retyping the totals. A pretty PDF isn't enough anymore, and honestly, for a machine, it never really was the point.

The part that surprises people who haven't dealt with this yet: the mandate doesn't force you to give up the human-readable PDF. It just requires that PDF to carry a second, structured version of itself, riding along inside it. That format is called ZUGFeRD, with an internationally aligned sibling called Factur-X.

If you've never had to build one, it's worth understanding the mechanics before the code, because it's a genuinely clever piece of engineering, not just a compliance checkbox. So how does a single file manage to be both a human-readable invoice and a machine-parseable one at once?

What a ZUGFeRD invoice actually is

Open a ZUGFeRD invoice in Adobe Acrobat or any PDF viewer and you see a normal invoice: logo, line items, totals, payment terms, nothing unusual. But embedded inside that same file, in its attachments, sits an XML document carrying the exact same invoice data in structured, typed form: invoice number, line items, tax rates, totals, every field an accounting system needs, tagged rather than buried in a paragraph a parser has to guess at.

The container format making this possible is PDF/A-3, the only PDF/A variant that permits arbitrary file attachments while still meeting the archival standard's long-term readability requirements. PDF/A-1 and PDF/A-2 explicitly forbid embedded attachments; PDF/A-3 was built for exactly this use case, which is why every ZUGFeRD file you'll open is, underneath, a PDF/A-3b document with an XML file riding inside it.

The embedded XML follows EN 16931, the EU's semantic data model for electronic invoices, with Germany's own XRechnung profile layered on top for domestic traffic. ZUGFeRD has shipped several versions (the 1.0 line through the current 2.x releases), each defining conformance levels, typically BASIC, COMFORT, EXTENDED, and the EN16931-aligned and XRECHNUNG profiles, trading structural strictness for how much invoice detail gets exposed to the machine-readable layer.

This matters most directly for two kinds of teams: anyone generating outbound invoices for German business customers, and anyone receiving invoices who needs to parse the embedded XML straight into an ERP or accounting system instead of manually retyping totals off a PDF.

The two REST primitives underneath it

Strip the standard's name away and ZUGFeRD generation is two REST calls chained together. First, Create PDF/A converts the visible invoice into PDF/A-3b, one of eight conformance levels the endpoint supports (PDF/A-1b, PDF/A-1a, PDF/A-2b, PDF/A-2u, PDF/A-2a, PDF/A-3b, PDF/A-3u, PDF/A-3a). Second, Add Attachment to PDF embeds the invoice XML inside that PDF/A-3 shell as a file attachment, the same mechanism you'd use to attach a spreadsheet to a report, just pointed at an XML payload instead. The endpoint's own schema makes this concrete: alongside the top-level docContent and docName for the base PDF, an attachments array holds one object per file to embed, each carrying its own docName (the attachment's filename as it appears inside the PDF, typically invoice-data.xml) and docContent (that file's own Base64 content).

Here's what that looks like live-verified against docs.pdf4me.com, chained end to end:

import requests
import base64

API_KEY = "YOUR_API_KEY"  # Base64-encoded, per PDF4me's Basic auth convention
headers = {
    "Authorization": f"Basic {API_KEY}",
    "Content-Type": "application/json",
}

# Step 1: convert the invoice PDF to PDF/A-3b
with open("invoice.pdf", "rb") as f:
    pdf_b64 = base64.b64encode(f.read()).decode()

pdfa_payload = {
    "docContent": pdf_b64,
    "docName": "invoice",
    "compliance": "PdfA3b",
    "allowUpgrade": True,
    "allowDowngrade": True,
}
pdfa_resp = requests.post(
    "https://api.pdf4me.com/api/v2/PdfA", json=pdfa_payload, headers=headers
).json()

# Step 2: embed the invoice XML inside the PDF/A-3b shell
with open("invoice-data.xml", "rb") as f:
    xml_b64 = base64.b64encode(f.read()).decode()

attach_payload = {
    "docContent": pdfa_resp["docContent"],
    "docName": "invoice.pdf",
    "attachments": [
        {"docName": "invoice-data.xml", "docContent": xml_b64}
    ],
}
zugferd_resp = requests.post(
    "https://api.pdf4me.com/api/v2/AddAttachmentToPdf", json=attach_payload, headers=headers
).json()

# zugferd_resp["docContent"] is now the finished ZUGFeRD PDF/A-3b file, Base64-encoded
Enter fullscreen mode Exit fullscreen mode

One honest flag: PDF4me's own Create PDF/A docs page renders its response example inconsistently with every other endpoint here, so double check the exact response key your account actually returns before wiring pdfa_resp["docContent"] into production. Every other call in this chain uses consistent docContent/docName naming, and it's a safe bet Create PDF/A does too, but verify against a real response rather than trusting a docs page's formatting on this one.

Going the other direction, Extract Attachment from PDF does the reverse: hand it a ZUGFeRD PDF, get back the embedded XML as structured data, live-verified as an outputDocuments array of {fileName, streamFile} objects, one per embedded file, exactly what a receiving ERP system needs to actually consume the invoice instead of just archiving it unread.

Knowing this matters even if you never touch the REST API directly. It explains what PDF4me's no-code action is doing under the hood, and it tells you where to look if a generated file doesn't validate the way you expect.

Which approach to reach for depends on how much control the job needs. Calling Create PDF/A and Add Attachment to PDF yourself makes sense if you're already generating the invoice PDF through your own template engine and just need the final embedding step, or if you want to validate the XML against your own schema before it goes anywhere near the PDF. Reaching for Create ZUGFeRD Invoice directly makes more sense the moment you're already living inside Make, Zapier, Power Automate, or n8n for the rest of the invoicing flow, since it collapses both REST calls, plus the conformance-level bookkeeping, into a single configured step you're not maintaining yourself.

The one-action version

Chaining two REST calls yourself is a reasonable way to build this, but PDF4me also ships it as a single purpose-built action, Create ZUGFeRD Invoice, across all four automation platforms this cluster covers. Feed it invoice data as XML, JSON, or CSV, pick a conformance level, and get back a hybrid PDF/A-3 file with the XML embedded and aligned to EN 16931. No manual REST chaining required.

In Make, the action accepts invoice data in any of those three formats and returns the ready-to-send hybrid file. A documented four-module Dropbox workflow walks through the whole thing: pull a base PDF and a ZUGFeRD 2.0+ XML file from Dropbox, run them through Create ZUGFeRD Invoice at EN16931 conformance, push the finished hybrid invoice back to Dropbox.

Zapier's version supports ZUGFeRD 1.0 and the full 2.x line, across BASIC, COMFORT, EXTENDED, EN16931, and XRECHNUNG conformance. Its Dropbox + XML walkthrough triggers on a new file landing in Dropbox, fetches the base PDF and XML payload, runs Create ZUGFeRD Invoice with XmlWithPdf output, and saves the result back to Dropbox at BASIC conformance in the documented example.

Power Automate's action is built explicitly around the German B2B mandate, and pulls source data from SharePoint, OneDrive, Outlook, or Dataverse in addition to XML, JSON, or CSV. Two documented walkthroughs cover the two source-format paths you're most likely to hit: a Dropbox + JSON flow for teams generating invoice data programmatically, and a Dropbox + XML flow for teams that already have ZUGFeRD-formatted XML sitting in a system somewhere and just need it embedded.

n8n's action generates ZUGFeRD 2.0 through 2.4, with EN16931, XRECHNUNG, BASIC, and EXTENDED conformance, from XML, JSON, or CSV. Its Dropbox + JSON walkthrough uploads invoice JSON, runs it through the ZUGFeRD action, and lands the finished file back in Dropbox, with real screenshots of every mapping.

What this doesn't do for you

A structurally valid ZUGFeRD file isn't automatically an invoice your counterparty's accounts-payable system will accept without complaint. Conformance levels exist precisely because different trading partners and national profiles expect different amounts of structured detail, and whether your invoice data actually satisfies EN 16931's business rules is a data-quality question the generation step alone doesn't answer for you. PDF4me's Validate PDF/A action can confirm the PDF/A-3 container itself conforms to ISO 19005, a useful gate before anything ships, though it checks the container, not ZUGFeRD's own business-rule validation of the embedded XML. Build that check into your pipeline before this touches production traffic, not after the first rejected invoice comes back.

Which conformance level to pick isn't really a technical question, it's a contractual one. Ask the receiving business, or their invoicing software vendor, which profile they expect. EN16931 and XRECHNUNG are the two most commonly required for the German mandate specifically, so match what's asked for rather than defaulting to EXTENDED because it sounds more thorough.

Getting started

Every action above sits behind the same PDF4me V2 REST API, so the authentication and base URL story is identical whether you're calling Create PDF/A and Add Attachment to PDF yourself, or letting Create ZUGFeRD Invoice do both in one step through whichever automation platform your team already runs on.

Germany's mandate is the concrete deadline in front of anyone shipping invoices there today. Building the pipeline once, with a tool that already understands PDF/A-3's attachment mechanics, beats re-explaining to a new hire every few months why an invoice PDF has a file attached to it.

Website: pdf4me.com
Documentation: docs.pdf4me.com
Developer portal: dev.pdf4me.com

Top comments (1)

Collapse
 
shubham_samanta_8a86bc0d6 profile image
Shubham Samanta

NICE INITIATIVE