ZUGFeRD & Factur-X

Generate e-invoices that comply with the ZUGFeRD / Factur-X standard by combining PDF/A-3B output, an embedded CII XML attachment, and Factur-X XMP metadata.

What is ZUGFeRD / Factur-X?

ZUGFeRD (Zentraler User Guide des Forums elektronische Rechnung Deutschland) and Factur-X are technically the same e-invoicing standard under two names — ZUGFeRD for Germany/Austria/Switzerland, Factur-X for France and the broader EU. The standard defines a hybrid invoice format:

  • A human-readable PDF that looks like a normal invoice
  • A machine-readable XML file (Cross-Industry Invoice, CII) embedded inside the PDF as an attachment
  • XMP metadata declaring the Factur-X profile and version
  • The PDF must be PDF/A-3B (or higher) — the "3" allows arbitrary file attachments, and the "B" ensures visual fidelity

This lets accounting software parse the XML for automated processing while humans can still open the PDF and read the invoice visually.

Factur-X Profiles

The standard defines five conformance profiles, from least to most data:

ProfileXML FilenameDescription
MINIMUM factur-x.xml Bare minimum: invoice number, date, total amount, VAT, buyer/seller. Sufficient for archiving.
BASIC WL factur-x.xml Basic Without Lines: header-level data only (no line items). Good for simple invoices.
BASIC factur-x.xml Adds line-item details (quantity, unit price, descriptions). Most common profile.
EN 16931 factur-x.xml Full European standard EN 16931 compliance. Required for B2G (business-to-government) invoicing in many EU countries.
EXTENDED factur-x.xml Maximum data: payment terms, delivery details, allowances/charges, additional references. Used for complex B2B invoicing.

Requirements

A valid ZUGFeRD / Factur-X invoice requires three things from FolioPDF:

  1. PDF/A-3B base document — generated via PdfAConformance.PdfA_3B
  2. Embedded CII XML — attached with Relationship = Alternative
  3. Factur-X XMP metadata — injected via ExtendMetadata or ExtendXmpMetadata

Step-by-Step Workflow

Step 1: Generate the PDF/A-3B invoice

Create the visual invoice using the layout engine with PDF/A-3B conformance:

using FolioPDF;
using FolioPDF.Fluent;
using FolioPDF.Helpers;
using FolioPDF.Infrastructure;

// Generate the base invoice PDF
Document.Create(doc =>
{
    doc.Page(page =>
    {
        page.Size(PageSizes.A4);
        page.Margin(50);

        page.Header().Row(row =>
        {
            row.RelativeItem().Text("ACME Corporation").FontSize(20).Bold();
            row.ConstantItem(150).AlignRight().Text("INVOICE").FontSize(20).Bold()
                .FontColor(Colors.Blue.Medium);
        });

        page.Content().Column(col =>
        {
            col.Spacing(10);

            col.Item().Text("Invoice #2026-0042").FontSize(14).SemiBold();
            col.Item().Text("Date: 2026-04-11");
            col.Item().Text("Due: 2026-05-11");

            col.Item().PaddingVertical(10).LineHorizontal(1).LineColor(Colors.Grey.Lighten2);

            col.Item().Table(table =>
            {
                table.ColumnsDefinition(c =>
                {
                    c.RelativeColumn(3);
                    c.ConstantColumn(60);
                    c.ConstantColumn(80);
                    c.ConstantColumn(80);
                });

                table.Cell().Text("Description").Bold();
                table.Cell().Text("Qty").Bold();
                table.Cell().Text("Unit Price").Bold();
                table.Cell().Text("Amount").Bold();

                table.Cell().Text("Widget Pro Annual License");
                table.Cell().Text("10");
                table.Cell().Text("EUR 49.00");
                table.Cell().Text("EUR 490.00");

                table.Cell().Text("Premium Support");
                table.Cell().Text("1");
                table.Cell().Text("EUR 200.00");
                table.Cell().Text("EUR 200.00");
            });

            col.Item().PaddingVertical(10).LineHorizontal(1).LineColor(Colors.Grey.Lighten2);
            col.Item().AlignRight().Text("Subtotal: EUR 690.00");
            col.Item().AlignRight().Text("VAT (19%): EUR 131.10");
            col.Item().AlignRight().Text("Total: EUR 821.10").Bold().FontSize(14);
        });

        page.Footer().AlignCenter().Text(t =>
        {
            t.Span("Page ");
            t.CurrentPageNumber();
        });
    });
})
.WithMetadata(new DocumentMetadata
{
    Title = "Invoice 2026-0042",
    Author = "ACME Corporation",
    Creator = "ACME Billing System",
    Language = "en"
})
.WithSettings(new DocumentSettings
{
    PdfAConformance = PdfAConformance.PdfA_3B
})
.GeneratePdf("invoice-base.pdf");

Step 2: Attach the CII XML and inject XMP metadata

Use DocumentOperation to embed the XML and set the Factur-X XMP. This is a post-processing step that runs the generated PDF through the qpdf engine:

// Factur-X XMP metadata — must match the CII XML profile
var facturxXmp = "
    <fx:ConformanceLevel
        xmlns:fx=""urn:factur-x:pdfa:CrossIndustryDocument:invoice:1p0#"">
        BASIC
    </fx:ConformanceLevel>
    <pdfaExtension:schemas
        xmlns:pdfaExtension=""http://www.aiim.org/pdfa/ns/extension/""
        xmlns:pdfaSchema=""http://www.aiim.org/pdfa/ns/schema#""
        xmlns:pdfaProperty=""http://www.aiim.org/pdfa/ns/property#"">
        <rdf:Bag>
            <rdf:li rdf:parseType=""Resource"">
                <pdfaSchema:schema>Factur-X PDFA Extension Schema</pdfaSchema:schema>
                <pdfaSchema:namespaceURI>urn:factur-x:pdfa:CrossIndustryDocument:invoice:1p0#</pdfaSchema:namespaceURI>
                <pdfaSchema:prefix>fx</pdfaSchema:prefix>
                <pdfaSchema:property>
                    <rdf:Seq>
                        <rdf:li rdf:parseType=""Resource"">
                            <pdfaProperty:name>DocumentFileName</pdfaProperty:name>
                            <pdfaProperty:valueType>Text</pdfaProperty:valueType>
                            <pdfaProperty:category>external</pdfaProperty:category>
                            <pdfaProperty:description>Name of the embedded XML invoice file</pdfaProperty:description>
                        </rdf:li>
                        <rdf:li rdf:parseType=""Resource"">
                            <pdfaProperty:name>DocumentType</pdfaProperty:name>
                            <pdfaProperty:valueType>Text</pdfaProperty:valueType>
                            <pdfaProperty:category>external</pdfaProperty:category>
                            <pdfaProperty:description>INVOICE</pdfaProperty:description>
                        </rdf:li>
                        <rdf:li rdf:parseType=""Resource"">
                            <pdfaProperty:name>Version</pdfaProperty:name>
                            <pdfaProperty:valueType>Text</pdfaProperty:valueType>
                            <pdfaProperty:category>external</pdfaProperty:category>
                            <pdfaProperty:description>Version of the Factur-X XML schema</pdfaProperty:description>
                        </rdf:li>
                        <rdf:li rdf:parseType=""Resource"">
                            <pdfaProperty:name>ConformanceLevel</pdfaProperty:name>
                            <pdfaProperty:valueType>Text</pdfaProperty:valueType>
                            <pdfaProperty:category>external</pdfaProperty:category>
                            <pdfaProperty:description>Factur-X conformance level</pdfaProperty:description>
                        </rdf:li>
                    </rdf:Seq>
                </pdfaSchema:property>
            </rdf:li>
        </rdf:Bag>
    </pdfaExtension:schemas>
    <fx:DocumentFileName
        xmlns:fx=""urn:factur-x:pdfa:CrossIndustryDocument:invoice:1p0#"">
        factur-x.xml
    </fx:DocumentFileName>
    <fx:DocumentType
        xmlns:fx=""urn:factur-x:pdfa:CrossIndustryDocument:invoice:1p0#"">
        INVOICE
    </fx:DocumentType>
    <fx:Version
        xmlns:fx=""urn:factur-x:pdfa:CrossIndustryDocument:invoice:1p0#"">
        1.0
    </fx:Version>";

// Post-process: attach XML + inject XMP
DocumentOperation.LoadFile("invoice-base.pdf")
    .AddAttachment(new DocumentAttachment
    {
        FilePath = "factur-x.xml",
        Key = "factur-x.xml",
        AttachmentName = "factur-x.xml",
        MimeType = "text/xml",
        Description = "Factur-X XML invoice data",
        Relationship = DocumentAttachmentRelationship.Alternative
    })
    .ExtendMetadata(facturxXmp)
    .Save("invoice-zugferd.pdf");

Relationship = Alternative is critical. PDF/A-3 requires every embedded file to declare its relationship to the document. ZUGFeRD / Factur-X mandates /Alternative — the XML is an alternative machine-readable representation of the PDF's visual content. Using any other relationship value will fail validation.

Using PdfEditor (Single-Chain Approach)

If you prefer a single fluent chain from generation through post-processing, use PdfEditor.Create with ExtendXmpMetadata and AddAttachment:

PdfEditor.Create(doc =>
{
    doc.Page(page =>
    {
        page.Size(PageSizes.A4);
        page.Margin(50);
        page.Content().Text("Invoice content here...").FontSize(12);
    });
})
.SetTitle("Invoice 2026-0042")
.ExtendXmpMetadata(facturxXmp)
.AddAttachment(new DocumentAttachment
{
    FilePath = "factur-x.xml",
    Key = "factur-x.xml",
    AttachmentName = "factur-x.xml",
    MimeType = "text/xml",
    Description = "Factur-X XML invoice data",
    Relationship = DocumentAttachmentRelationship.Alternative
})
.Save("invoice-zugferd.pdf");

Note: When using PdfEditor.Create, set the PdfAConformance via .WithSettings() on the inner Document.Create call, or generate the base document separately. The PdfEditor post-processing pipeline does not re-encode PDF/A conformance — it preserves whatever conformance the base PDF already has.

The CII XML File

FolioPDF handles the PDF side (embedding, metadata, conformance) but does not generate the CII XML — that is your application's responsibility. A minimal BASIC-profile XML looks like this:

<?xml version="1.0" encoding="UTF-8"?>
<rsm:CrossIndustryInvoice
    xmlns:rsm="urn:un:unece:uncefact:data:standard:CrossIndustryInvoice:100"
    xmlns:ram="urn:un:unece:uncefact:data:standard:ReusableAggregateBusinessInformationEntity:100"
    xmlns:udt="urn:un:unece:uncefact:data:standard:UnqualifiedDataType:100">
    <rsm:ExchangedDocumentContext>
        <ram:GuidelineSpecifiedDocumentContextParameter>
            <ram:ID>urn:cen.eu:en16931:2017#compliant#urn:factur-x.eu:1p0:basic</ram:ID>
        </ram:GuidelineSpecifiedDocumentContextParameter>
    </rsm:ExchangedDocumentContext>
    <rsm:ExchangedDocument>
        <ram:ID>2026-0042</ram:ID>
        <ram:TypeCode>380</ram:TypeCode>
        <ram:IssueDateTime>
            <udt:DateTimeString format="102">20260411</udt:DateTimeString>
        </ram:IssueDateTime>
    </rsm:ExchangedDocument>
    <!-- ... trade transaction, line items, totals ... -->
</rsm:CrossIndustryInvoice>

Consider using a dedicated CII library (e.g. ZUGFeRD-csharp or building from the UN/CEFACT schema) to generate the XML, then embed it with FolioPDF.

XMP Metadata Template

The XMP metadata injected via ExtendMetadata / ExtendXmpMetadata is inserted inside the existing <rdf:Description> tag of the document's XMP stream. The content must include:

ElementValuePurpose
fx:DocumentFileNamefactur-x.xmlName of the embedded XML file
fx:DocumentTypeINVOICEDocument type identifier
fx:Version1.0Factur-X schema version
fx:ConformanceLevelMINIMUM | BASIC WL | BASIC | EN 16931 | EXTENDEDMust match the profile of your CII XML
pdfaExtension:schemas(schema definition)PDF/A extension schema declaring the fx: namespace properties

ConformanceLevel must match your XML. If the XML is a BASIC profile but the XMP says EN 16931, validators will report a mismatch. Always keep the two in sync.

Validation

Validate the final PDF with veraPDF for PDF/A-3B conformance and the Mustang validator for ZUGFeRD/Factur-X compliance:

# PDF/A-3B validation
verapdf --format mrr --flavour 3b invoice-zugferd.pdf

# ZUGFeRD / Factur-X validation (Mustang)
java -jar Mustang-CLI.jar --action validate invoice-zugferd.pdf

Common validation failures:

FailureCauseFix
Missing /AFRelationship Relationship not set on attachment Set Relationship = DocumentAttachmentRelationship.Alternative
XMP metadata stream compressed qpdf recompressed the XMP stream FolioPDF handles this automatically — when ExtendMetadata is used, stream encoding is preserved
Missing Factur-X XMP extension schema The pdfaExtension:schemas block is missing Include the full schema definition in the XMP (see template above)
PDF/A-3B stream EOL violation Missing newline before endstream FolioPDF emits newline-before-endstream automatically on save