Creating ZUGFeRD invoices with PDFEdit


Requirements: Secure PDF and IPWorks (Optional)

Introduction

ZUGFeRD is a standardized electronic invoicing format developed to simplify the exchange of invoices between businesses and public administrations. It combines a human-readable PDF/A-3 invoice with a machine-readable XML file embedded within the PDF, enabling seamless automation, validation, and archiving of invoice data. This hybrid approach ensures compliance with EU standards while maintaining visual compatibility with traditional invoice processing.

Using the PDFEdit component of Secure PDF, this integration of XML data into a PDF to create a ZUGFeRD-compliant invoice can be achieved efficiently and programmatically.

ZUGFeRD and Factur-X

ZUGFeRD and Factur-X are both hybrid invoice formats that combine a human-readable PDF with embedded machine-readable XML data. While ZUGFeRD originated in Germany and Factur-X in France, both standards now share a common specification and structure. Starting from ZUGFeRD version 2.0, the two formats are mostly compatible, and with the version 2.1 of ZUGFeRD the standard was made fully compatible with Factur-X, meaning any invoice created according to the ZUGFeRD 2.1 (or later) specification is also compliant with Factur-X requirements.

ZUGFeRD Format Requirements

  1. PDF/A-3 Format
    Must use PDF/A-3 (a, b, or u). Serves as container for the XML. Additional attachments (e.g., images, spreadsheets) are allowed.

  2. Single XML Attachment
    Only one XML invoice is allowed, associated at document level using the /AFRelationship key in the Filespec dictionary. In the latest versions of ZUGFeRD the file name must be factur-x.xml

  3. XMP Metadata
    The PDF must include XMP metadata containing key invoice details as well as PDF/A information, which we will cover in more detail in the next section.

XMP Metadata

To comply with the ZUGFeRD specification, the PDF must include XMP metadata that describes the invoice. This metadata contains details such as the document type, embedded XML filename, schema version, and profile (e.g., BASIC, COMFORT, EN 16931).

We use a predefined XMP template and extract the necessary values from the XML invoice to populate this metadata. Below you can find the template for ZUGFeRD 2.1 (or later) and PDF/A-3b.

<?xpacket begin="?" id="W5M0MpCehiHzreSzNTczkc9d"?><x:xmpmeta xmlns:x="adobe:ns:meta/"> <rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"> <rdf:Description xmlns:pdf="http://ns.adobe.com/pdf/1.3/" rdf:about=""> <pdf:Producer>ProducerName</pdf:Producer> </rdf:Description> <rdf:Description xmlns:pdfaid="http://www.aiim.org/pdfa/ns/id/" rdf:about=""> <pdfaid:conformance>B</pdfaid:conformance> <pdfaid:part>3</pdfaid:part> </rdf:Description> <rdf:Description xmlns:dc="http://purl.org/dc/elements/1.1/" rdf:about=""> <dc:format>application/pdf</dc:format> <dc:creator> <rdf:Seq> <rdf:li></rdf:li> </rdf:Seq> </dc:creator> <dc:date> <rdf:Seq> <rdf:li></rdf:li> </rdf:Seq> </dc:date> </rdf:Description> <!-- The actual Factur-X properties --> <rdf:Description xmlns:fx="urn:factur-x:pdfa:CrossIndustryDocument:invoice:1p0#" rdf:about=""> <fx:ConformanceLevel></fx:ConformanceLevel> <fx:DocumentType>INVOICE</fx:DocumentType> <fx:DocumentFileName></fx:DocumentFileName> <fx:Version>1.0</fx:Version> </rdf:Description> <!-- PDF/A extension schema description for the Factur-X schema. --> <rdf:Description 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:about=""> <pdfaExtension:schemas> <rdf:Bag> <rdf:li rdf:parseType="Resource"> <pdfaSchema:schema>Factur-X PDF/A 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>The actual 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>The conformance level of the embedded Factur-X data</pdfaProperty:description> </rdf:li> </rdf:Seq> </pdfaSchema:property> </rdf:li> </rdf:Bag> </pdfaExtension:schemas> </rdf:Description> </rdf:RDF> </x:xmpmeta><?xpacket end="w"?>

The following method shows how to dynamically generate XMP metadata using the XML component from IPWorks, based on the template provided above.

private string CreateMetadata(string XMLInvoice) { XML xmlparser = new XML(); string XMPTemplate = "XMP-Template.xml"; string output = "XMP-Output.xml";
// Parse the invoice XML xmlparser.InputFile = XMLInvoice; xmlparser.Parse();
// Extracting creators name (the sellers company name) xmlparser.XPath = "/rsm:CrossIndustryInvoice/rsm:SupplyChainTradeTransaction/ram:ApplicableHeaderTradeAgreement/ram:SellerTradeParty/ram:Name"; string creator = xmlparser.XText;
// Current time in the right format DateTimeOffset currentTime = DateTimeOffset.UtcNow; string formattedTime = currentTime.ToString("yyyy-MM-ddTHH:mm:sszzz");
// Extracting the conformance level xmlparser.XPath = "/rsm:CrossIndustryInvoice/rsm:ExchangedDocumentContext/ram:GuidelineSpecifiedDocumentContextParameter/ram:ID"; int lastColonIndex = xmlparser.XText.LastIndexOf(':'); string conformanceLevel = xmlparser.XText.Substring(lastColonIndex + 1).ToUpper(); conformanceLevel = conformanceLevel == "XRECHNUNG_3.0" ? "XRECHNUNG" : conformanceLevel == "2017" ? "EN 16931" : conformanceLevel == "BASICWL" ? "BASIC WL" : conformanceLevel
xmlparser.Reset();
xmlparser.Config("ProcessNamespace = false"); xmlparser.Config("IncludeXMLDeclaration = false"); xmlparser.Config("PreserveAll = true");
xmlparser.InputFile = XMPTemplate; xmlparser.OutputFile = output; xmlparser.Overwrite = true;
xmlparser.Parse();
// Add the creator xmlparser.XPath = "/x:xmpmeta/rdf:RDF/rdf:Description[3]/dc:creator/rdf:Seq/rdf:li"; xmlparser.XText = creator;
// Add current time xmlparser.XPath = "/x:xmpmeta/rdf:RDF/rdf:Description[3]/dc:date/rdf:Seq/rdf:li"; xmlparser.XText = formattedTime;
// Add conformance level xmlparser.XPath = "/x:xmpmeta/rdf:RDF/rdf:Description[4]/fx:ConformanceLevel"; xmlparser.XText = conformanceLevel;
// Add XML file name xmlparser.XPath = "/x:xmpmeta/rdf:RDF/rdf:Description[4]/fx:DocumentFileName"; xmlparser.XText = Path.GetFileName(XMLInvoice);
xmlparser.Save();
string metadata = File.ReadAllText(output); File.Delete(output);
return metadata; }

Generating ZUGFeRD invoice

The snippet above demonstrates how to create a fully compliant ZUGFeRD invoice using the PDFEdit component. First, it attaches the XML invoice as an embedded file and configures the appropriate /AFRelationship to define the relationship between the XML and the PDF content. The XMP metadata, which is generated dynamically from the invoice data as shown in the previous section, is then embedded into the PDF using the SetDocumentProperty() method. Finally, it saves the result as a valid PDF/A-3 file that combines both the human-readable PDF and machine-readable XML, fulfilling the core requirement of the ZUGFeRD and Factur-X specifications.

PDFEdit pdfedit = new PDFEdit(); string inputFile = "pdfa3.pdf"; string outputFile = "ZUGFeRD_invoice.pdf"; string invoice = "factur-x.xml";
pdfedit.InputFile = inputFile; pdfedit.OutputFile = outputFile; pdfedit.Overwrite = true;
pdfedit.Open(); // Attaching the XML invoice PDFAttachment zugferdInvoice = new PDFAttachment(); string desc = "Invoice metadata conforming to ZUGFeRD standard"; string contentType = "text/xml"; // In the latest version of ZUGFeRD (2.3 +) the content type should be text/xml zugferdInvoice.FileName = XMLInvoice; zugferdInvoice.Description = desc; zugferdInvoice.ContentType = contentType; pdfedit.Attachments.Add(zugferdInvoice); pdfedit.Config($"AFRelationship[{XMLInvoice}]=Data"); // Setting the AFRelationship key depending on the profile
pdfedit.SetDocumentProperty("Xmp", CreateMetadata(XMLInvoice));
pdfedit.Close();

Validation

Once the XML invoice is embedded and the XMP metadata is injected, we can verify that the resulting PDF conforms to the ZUGFeRD specification.

ZUGFeRD Validation

Conclusion

Creating ZUGFeRD invoices is a common requirement for many companies and is becoming increasingly popular as electronic invoicing standards continue to evolve across Europe. Using the PDFEdit and XML components, this process can be fully automated, ensuring your invoices are both human-readable and machine-processable. By following the steps outlined in this article, you can generate valid ZUGFeRD or Factur-X invoices that meet modern e-invoicing standards and are ready for exchange and archiving.

We appreciate your feedback. If you have any questions, comments, or suggestions about this article please contact our support team at support@nsoftware.com.