Guides

ZUGFeRD PDF generation: embedding XML in PDF/A-3 step by step

By Mohamed Waleed

ZUGFeRD PDF generation: embedding XML in PDF/A-3 step by step

ZUGFeRD gives you one file for two audiences. A person opens the PDF and reads it like a normal invoice. A machine extracts the embedded CII XML and processes it automatically. This walkthrough shows you how to generate a ZUGFeRD PDF/A-3 with the InvoiceSpec API, choose the right profile, and validate the result before you send it.

What a ZUGFeRD file actually is

A ZUGFeRD invoice is a PDF/A-3 document with a CII XML file embedded as an attachment. The visible PDF page is the human-readable invoice. The embedded XML carries the same data in a machine-readable form. Both layers come from the same EN 16931 data model, so they never disagree.

The XML inside a ZUGFeRD PDF is always CII. There is no UBL variant of ZUGFeRD. If you need UBL, you generate a standalone XML file instead. The current ZUGFeRD version is 2.5.2, and the format is also known as Factur-X in France.

What you need before you start

  • An InvoiceSpec API key. You can get one from the dashboard in seconds.
  • The API base URL. For production, use https://api.invoicespec.com.
  • A basic understanding of the invoice JSON schema. You do not need to know the CII XML structure or how to author a PDF. The engine maps the JSON to CII XML and packages it into the PDF for you.

Choose a ZUGFeRD profile

ZUGFeRD defines profiles that trade detail for simplicity. You select one in the config.profile field. The profiles the API supports are:

  • MINIMUM: a booking aid with the bare minimum of fields. Line items are suppressed from the embedded XML, though the visible PDF still shows them.
  • BASICWL: basic without lines. Enough to identify the invoice and its totals, but no line-item detail in the XML.
  • BASIC: basic with line items. Covers the common small-business invoice.
  • EN16931: the full EN 16931 data set. This is the default and the profile most businesses need for compliance. It is also called COMFORT.
  • XRECHNUNG: the EN16931 profile with the XRechnung customization layered on top. Use this when your recipient wants a PDF but also needs the German public-sector rules satisfied.
  • EXTENDED: a superset of EN 16931 that adds fields for more complex scenarios like downstream settlement. The engine stamps the profile URN but does not model the extension-only fields.

Most B2B invoices that need ZUGFeRD use the EN16931 profile. If your recipient is a German public authority that accepts ZUGFeRD, the XRECHNUNG profile gives you the PDF surface plus the XRechnung rule compliance in one file.

The request structure

Every InvoiceSpec request has two top-level objects: invoice and config. The invoice object carries the business data. The config object selects the output format, syntax, and profile. For ZUGFeRD, the config sets spec to zugferd, syntax to cii, and profile to the profile you chose.

json
{
"invoice": {
"invoiceNumber": "INV-DEMO-001",
"issueDate": "2025-07-01",
"invoiceTypeCode": "380",
"currencyCode": "EUR",
"buyerReference": "991-12345-67",
"seller": {
"name": "Acme GmbH",
"legalRegistrationIdentifier": "HRB98765",
"vatIdentifier": "DE123456789",
"electronicAddress": "DE123456789",
"electronicAddressSchemeId": "9930",
"postalAddress": {
"addressLine1": "Musterstr. 1",
"city": "Berlin",
"postcode": "10115",
"countryCode": "DE"
},
"contact": {
"contactPoint": "Max Mustermann",
"telephone": "+49 30 12345678",
"email": "billing@acme.de"
}
},
"buyer": {
"name": "Example Corp",
"electronicAddress": "FR12345678901",
"electronicAddressSchemeId": "9930",
"postalAddress": {
"addressLine1": "12 Rue de Rivoli",
"city": "Paris",
"postcode": "75001",
"countryCode": "FR"
}
},
"deliveryInformation": { "actualDeliveryDate": "2025-07-01" },
"paymentInstructions": {
"meansCode": "30",
"creditTransfer": { "accountIdentifier": "DE89370400440532013000" }
},
"documentTotals": {},
"vatBreakdowns": [{ "vatCategoryCode": "S", "vatCategoryRate": 19 }],
"invoiceLines": [
{
"lineIdentifier": "1",
"quantity": 10,
"unitCode": "C62",
"priceDetails": { "netPrice": 150.0 },
"lineVatInformation": { "vatCategoryCode": "S", "vatRate": 19 },
"itemInformation": { "name": "Consulting Services" }
}
]
},
"config": { "spec": "zugferd", "syntax": "cii", "profile": "EN16931" }
}

A few things to notice:

  • buyerReference is included because the XRECHNUNG profile enforces the BR-DE-15 rule. If you might switch to that profile, include it from the start. The EN16931 profile does not require it.
  • The seller contact group is present because the XRECHNUNG profile requires it under rules BR-DE-5, BR-DE-6, and BR-DE-7.
  • documentTotals is empty. In AUTO mode the engine calculates line net amounts, VAT, and the payable total from the line data you provide.
  • config.version is omitted, so the engine targets the latest active ZUGFeRD release (currently 2.5.2). Pin an explicit version only when you need to lock the output to a specific release.

Send the request

The ZUGFeRD generation endpoint is POST /v1/invoice/zugferd. Pass your API key in the Authorization header as a bearer token and send the JSON body.

bash
curl -X POST https://api.invoicespec.com/v1/invoice/zugferd \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d @invoice.json \
-o invoice.pdf

The response is a validated ZUGFeRD PDF/A-3 document. The engine runs the official FeRD per-profile Schematron rules on the embedded CII XML before packaging, then checks the final PDF for PDF/A-3 conformance and ZUGFeRD XMP metadata. The PDF you get back passes the same validation the recipient's system will run.

Choose the PDF language

The PDF page the engine renders has labels and a date format in a language you control. Set config.metadataLanguage in the config object:

  • "en" (the default) produces English labels and an ISO date format.
  • "de" produces German labels and a German date format.

The engine does not translate your invoice content. It only controls the chrome the engine authors around your data, such as column headers and the word "Invoice" versus "Rechnung".

json
{ "spec": "zugferd", "syntax": "cii", "profile": "EN16931", "metadataLanguage": "de" }

Validate the PDF before delivery

If you already have a ZUGFeRD PDF and want to check it, use the validate endpoint: POST /v1/invoice/zugferd/validate. Send the PDF in the request body with Content-Type set to application/pdf.

bash
curl -X POST https://api.invoicespec.com/v1/invoice/zugferd/validate \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/pdf" \
-d @invoice.pdf

The endpoint always returns 200 OK with a JSON body. Validation failures are reported as valid: false with a structured issue list, not as an HTTP error. The validator extracts the embedded CII XML, checks PDF/A-3 conformance, and runs the ZUGFeRD profile rules.

json
{
"valid": true,
"version": "2.5.2",
"profile": "EN16931",
"summary": { "errors": 0, "warnings": 0, "information": 0 },
"issues": [],
"requestId": "req_01HXYZ..."
}

An invalid document returns the same shape with valid: false and a populated issues array. Each issue carries a severity, the rule code, and a message that names the field or element at fault.

Handle errors

When the engine rejects a request, it returns a structured error with a machine-readable code and a human-readable message. The most common errors when generating ZUGFeRD are:

  • INVALID_INPUT: a required field is missing or a value violates the JSON schema. The error response names the field and the constraint.
  • UNSUPPORTED_SPEC: the version, syntax, or profile is not resolvable. This happens when a profile is inactive, not yet effective, or past its sunset date. Omit config.version to target the latest resolvable version, or check the specs-list endpoint for the schedule.
  • VALIDATION_FAILED: the generated XML fails a FeRD Schematron rule. The response includes the rule identifier, the XPath of the offending element, and a description so you can trace the failure back to the source field in your JSON.

Read the errors reference for the full list of error codes and recovery guidance.

ZUGFeRD or XRechnung?

If you are unsure whether to send a ZUGFeRD PDF or a XRechnung XML file, the deciding factor is what your recipient expects. German public authorities that require structured XML want XRechnung. German B2B recipients often prefer ZUGFeRD because their accounting team can read the PDF while their software extracts the XML. Both formats share the EN 16931 data model, so you generate either from the same invoice data.

For a deeper comparison, read XRechnung vs ZUGFeRD: which e-invoicing standard do you need?

Next steps

  • Read the full API reference for the ZUGFeRD generate and validate endpoints, including every request field and response shape.
  • Browse the invoice JSON schema explorer to see every field, its type, and its EN 16931 business term mapping.
  • Try the free ZUGFeRD Generator to generate a real ZUGFeRD PDF from a form without writing code.
ZUGFeRDPDF/A-3CIIAPIdeveloperstutorial

Mohamed Waleed

Founder, InvoiceSpec

Mohamed is the founder of InvoiceSpec, where he builds the e-invoicing infrastructure software companies use to ship compliant XRechnung, ZUGFeRD, and EN 16931 documents without maintaining the logic in-house. He writes about German e-invoicing compliance in plain terms for the finance and ops teams who have to meet it.

Related posts

Guides

Generate and validate XRechnung, EN 16931, and ZUGFeRD with one API — endpoint-specific schemas, examples, and recovery guidance.

Read the API reference