Automation MCP Server Features Blog Pricing Contact

You Built the XML. Now You Need the PDF/A-3.

Every library that generates Factur-X or ZUGFeRD XML leaves the same 20% unsolved: turning your rendered invoice PDF into a conformant PDF/A-3 with the XML correctly embedded. The embed endpoints close exactly that gap: your PDF plus your CII XML in, a compliant hybrid out, with profile-aware XMP metadata and structural checks that catch the classic mistakes before a recipient does.

There is a specific moment in every Factur-X or ZUGFeRD integration where the easy 80% ends. The CII XML is done: maybe Mustangproject or horstoeko/zugferd built it, maybe drafthorse or node-zugferd or ZUGFeRD-csharp, maybe your own XML writer against the EN 16931 model. Your system also already renders a perfectly good invoice PDF, with your layout, your fonts, your logo. What you do not have is the thing the mandate actually requires: one PDF/A-3 file with the XML inside it, conformant enough that a recipient's archival checks and your customer's accounting import both accept it.

That last mile is not XML work, it is PDF engineering, and it is unsolved or half-solved in every open-source ecosystem. This post covers the two endpoints built for exactly this seam: POST /v1/embed/facturx and POST /v1/embed/zugferd. Your PDF plus your XML in, a compliant hybrid out, one request.

The last-mile problem

The division of labor in most real pipelines looks like this: invoice data lives in your system, an XML layer (library or hand-built) produces the CII document, and a rendering layer (template engine, accounting package, reporting tool) produces the human-readable PDF. Three mature, working components. The hybrid format demands a fourth step that belongs to none of them: merge the PDF and the XML into a single PDF/A-3 container that satisfies an archival ISO standard and the Factur-X/ZUGFeRD packaging rules at the same time.

The XML libraries stop at the XML by design; the ones that attempt the merge hand the conformance problem back to you, because they embed into whatever PDF you provide, and whatever PDF you provide is almost never PDF/A. General-purpose PDF libraries generate ordinary PDFs, not archival ones. The failure mode is quiet and expensive: the file opens fine everywhere, looks perfect, and bounces months later when a trading partner's systems check conformance.


What PDF/A-3 actually requires

The container work the hybrid formats demand, and the part the embed endpoints do for you:

Archival conformance. PDF/A-3 is an ISO profile of PDF: fonts embedded, color behavior pinned down with an output intent, and the document structure constrained so the file renders identically decades from now. An ordinary PDF from a template engine meets almost none of this by default.

The prescribed attachment. The XML must be embedded under the exact filename the specification prescribes, factur-x.xml, with the attachment declarations recipient software looks for. A correctly conformant PDF/A-3 with a wrongly packaged attachment is still a broken hybrid.

Profile-aware XMP metadata. The container itself declares what it carries: an XMP extension schema naming the standard and the conformance level (MINIMUM through EXTENDED). That declaration must agree with what the XML says in BT-24, which is why the embed endpoints read the profile from your document instead of stamping a default.

Each of these is checkable by machines on the receiving side, which is exactly why hand-rolled containers fail there rather than on your desk. The deeper anatomy of the format is covered in the technical overview of Factur-X and ZUGFeRD compliance.


The embed endpoints

Both endpoints take the same two multipart fields and return the finished hybrid as binary application/pdf:

FieldContentNotes
pdfYour rendered invoice PDFAny standard PDF; promoted to PDF/A-3 in place, visual content preserved exactly
xmlYour CII invoice XMLUN/CEFACT CII D16B, root <CrossIndustryInvoice>; BT-24 must declare an official profile URN

Up to 20 MB per file, processed statelessly: both uploads live in memory for the duration of the request and are discarded the moment the embedded PDF is returned.

Calling it: one request

curl -X POST https://api.invoicexml.com/v1/embed/facturx \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -F "[email protected]" \
  -F "[email protected]" \
  --output invoice-facturx.pdf

The same call from any JavaScript runtime, using nothing beyond the standard fetch and FormData:

import { readFile, writeFile } from "node:fs/promises";

const form = new FormData();
form.append("pdf", new Blob([await readFile("invoice.pdf")]), "invoice.pdf");
form.append("xml", new Blob([await readFile("factur-x.xml")]), "factur-x.xml");

const response = await fetch("https://api.invoicexml.com/v1/embed/facturx", {
  method: "POST",
  headers: { Authorization: `Bearer ${process.env.INVOICEXML_API_KEY}` },
  body: form
});
if (!response.ok) throw new Error(await response.text());

await writeFile("invoice-facturx.pdf", Buffer.from(await response.arrayBuffer()));

The response is the complete hybrid: your visual layer untouched, the container promoted to PDF/A-3, the XML embedded as factur-x.xml, and the XMP metadata declaring the profile your document actually carries.

What gets checked before embedding

The endpoint refuses to package a structurally broken document, because a hybrid that embeds cleanly but fails downstream helps nobody. Three checks run before anything is merged:

The profile URN (error code 4017). BT-24 must declare one of the official Factur-X/ZUGFeRD profile URNs, because that value is mirrored into the PDF/A-3 metadata as the conformance level, and the specification defines it as a closed list. The classic near-miss is mixing the stem keywords: EXTENDED uses #conformant# while the narrower profiles use #compliant#. The error response lists every accepted value, and the identifier reference pages unpack each URN, with urn:factur-x.eu:1p0:en16931 as the one most documents should carry.

The profile's own XSD (error code 4001). Your XML must conform to the declared profile's official schema, which is stricter about element placement and order than the generic CII schema. The textbook example: the line object identifier (BT-128) belongs in SpecifiedLineTradeSettlement, not SpecifiedLineTradeAgreement, a placement several hand-built writers get wrong. A violation returns the complete list of schema errors, not just the first.

The business rules (error code 4001). By default the full EN 16931 and profile Schematron rule set runs too, the same rules the validation endpoints apply, so a rule violation surfaces as a structured findings list on your side instead of a rejection at your customer's import. Warnings never block, and the skipValidation=true form field skips the business-rule pass when you deliberately need packaging only; the two structural checks above always apply.

Factur-X or ZUGFeRD branding?

One decision, purely about your recipient. Both endpoints produce a hybrid with the same factur-x.xml attachment and the standard Factur-X XMP metadata; the difference is the packaging conventions. /v1/embed/facturx follows the French (FNFE-MPE) practice French systems expect. /v1/embed/zugferd follows the German (FeRD) practice German B2B and B2G readers expect, including the dedicated xrechnung.xml attachment for the XRechnung reference profile. Same invoice, same XML, endpoint chosen per market:

UBL-first pipelines

The hybrid formats embed CII, so the embed endpoints accept only CII. If your pipeline is UBL-native (a Peppol stack, for instance), the path is two calls: POST /v1/convert/ubl/to/cii maps every EN 16931 business term losslessly into CII syntax, then the result goes to the embedder. The conversion reference covers the mapping.


Validate the output

The embed endpoints already run the roughly 200 EN 16931 and profile rules on your XML by default, so a rule violation never reaches the container. Two extra placements are still useful, and the second belongs in CI either way:

Before embedding: run your XML through POST /v1/validate/cii while developing your generator. Alongside the Schematron verdict, its CII-SR warnings flag exactly the schema-placement issues the embedder would reject, so you catch a 4001 before it happens.

After embedding: upload the finished hybrid to POST /v1/validate/facturx or /v1/validate/zugferd. The validator extracts the embedded XML, detects the profile from BT-24, and runs the full official rule set for it, returning JSON findings with rule ids, plain-language messages, and field paths. One such call on a freshly embedded document in your test suite pins the whole pipeline: XML generation, rendering, and packaging, verified together on every build.


Embed vs create: two on-ramps, one service

The embed endpoints exist because pipelines rarely start from zero. If your XML generation works and your PDF rendering is part of your product's identity, embed completes the integration without asking you to abandon either: the API supplies precisely the piece nobody ships well, the conformant container.

The full on-ramp is POST /v1/create/facturx and /v1/create/zugferd: invoice JSON in, the entire hybrid out, visual layer and XML both generated, totals and VAT breakdown computed, and the complete EN 16931 rule set enforced before delivery. What moves teams from embed to create over time is rarely the endpoints themselves; it is the XML library's maintenance treadmill. Every FNFE-MPE and FeRD release means watching the announcement, waiting for the library update, bumping, retesting, and redeploying, and the specifications move every year. With create, that entire surface disappears: the current artifacts are live server-side on their effective dates, your build carries zero compliance dependencies, and staying current stops being a recurring backlog item.

Either way, the part of the problem you started this post with, the PDF/A-3, is permanently off your plate, backed by a team that does European e-invoicing compliance full time, with professional support behind the integration and missing features integrated on request. That is the difference between a PDF utility and a complete compliance service.


Endpoint reference

OperationEndpointInputOutput
Embed as Factur-XPOST /v1/embed/facturxPDF + CII XML (multipart)PDF/A-3 binary, French packaging
Embed as ZUGFeRDPOST /v1/embed/zugferdPDF + CII XML (multipart)PDF/A-3 binary, German packaging
UBL to CIIPOST /v1/convert/ubl/to/ciiUBL XMLCII XML
Validate CII (pre-embed)POST /v1/validate/ciiCII XMLValidation JSON + CII-SR warnings
Validate the hybridPOST /v1/validate/facturx, /v1/validate/zugferdHybrid PDFValidation JSON
Create the full hybridPOST /v1/create/facturx, /v1/create/zugferdInvoice JSONPDF/A-3 binary

Full OpenAPI 3.1 schema: api.invoicexml.com/v1/openapi  |  Interactive API explorer: api.invoicexml.com/v1/scalar


Get started

If you have a PDF and a CII XML on disk right now, the curl call above is the whole integration; the online embedder does the same thing in the browser for a first look at your output.

Create a free InvoiceXML account → get 100 free credits with the 30-day trial, no credit card required.

Related resources:


InvoiceXML is a REST API for European e-invoice compliance covering Factur-X, ZUGFeRD, XRechnung, Peppol UBL, and CII. Stateless processing, GDPR compliant by architecture, and callable from any stack in minutes.

Start free today

Ready to automate your invoices?

Validate, convert and embed compliant e-invoices through one API. Start your 30-day free trial. No credit card required.

GDPR Compliant No credit card required Setup in minutes
Peppol UBL
Factur-X
EN 16931
142 / 142 passed
Compliant
PDF/A-3 embedded