Create ZUGFeRD, Factur-X, XRechnung and extract XML

A developer guide for creating compliant e-invoice PDFs and extracting their embedded XML data using the PDFluent SDK.

Add PDFluent to Cargo.toml

XML extraction works with the base crate. The einvoice feature adds profile detection and validation helpers.

toml
# Cargo.toml
[dependencies]
pdfluent = { version = "0.9", features = ["einvoice"] }

Create a ZUGFeRD or Factur-X e-invoice PDF in Rust

ZUGFeRD and Factur-X are hybrid e-invoice formats. The PDF is human-readable and machine-readable at the same time. The XML payload is embedded as an attachment.

rust
use pdfluent::{PdfDocument, EInvoiceProfile, ZugferdAttachment};

fn main() -> pdfluent::Result<()> {
    let mut doc = PdfDocument::open("invoice-template.pdf")?;

    let xml = std::fs::read_to_string("invoice.xml")?;

    doc.attach_einvoice(ZugferdAttachment {
        xml,
        profile: EInvoiceProfile::EN16931,
        filename: "factur-x.xml".to_string(),
    })?;

    doc.convert_to_pdfa3()?;
    doc.save("invoice-einvoice.pdf")?;
    Ok(())
}
  1. Add PDFluent with the einvoice and pdfa features

    E-invoice support requires the pdfa feature for PDF/A-3 conformance and the einvoice feature for ZUGFeRD/Factur-X metadata.

    rust
    # Cargo.toml
    [dependencies]
    pdfluent = { version = "0.9", features = ["pdfa", "einvoice"] }
  2. Create or load the visual PDF

    Start from a rendered invoice PDF. This is the human-readable part. PDFluent will attach the XML and set the required PDF/A-3 metadata.

    rust
    use pdfluent::PdfDocument;
    
    // Load an existing rendered invoice
    let mut doc = PdfDocument::open("invoice-template.pdf")?;
    
    // Or build one from scratch
    // let mut doc = PdfDocument::new();
    // ... add content, text, tables ...
  3. Prepare the Factur-X XML payload

    The XML must conform to the UN/CEFACT CII schema. EN16931 is the profile required for public sector e-invoicing in the EU. MINIMUM and BASIC_WL are simpler profiles.

    rust
    let xml = r#"<?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:en16931</ram:ID>
        </ram:GuidelineSpecifiedDocumentContextParameter>
      </rsm:ExchangedDocumentContext>
      <!-- ... invoice content ... -->
    </rsm:CrossIndustryInvoice>"#.to_string();
  4. Attach the XML to the PDF

    attach_einvoice() embeds the XML as a PDF file attachment with the correct MIME type, AFRelationship, and XMP metadata required by the Factur-X spec.

    rust
    use pdfluent::{EInvoiceProfile, ZugferdAttachment};
    
    doc.attach_einvoice(ZugferdAttachment {
        xml,
        profile: EInvoiceProfile::EN16931,
        filename: "factur-x.xml".to_string(),
    })?;
  5. Convert to PDF/A-3 and save

    ZUGFeRD and Factur-X require PDF/A-3b conformance. convert_to_pdfa3() adds the required XMP metadata and output intent.

    rust
    doc.convert_to_pdfa3()?;
    doc.save("invoice-einvoice.pdf")?;
    println!("E-invoice PDF saved.");
  • PDF/A-3 requires all fonts to be embedded. convert_to_pdfa3() checks and embeds missing fonts automatically.
  • The XML attachment filename must be exactly factur-x.xml for Factur-X or ZUGFeRD-invoice.xml for ZUGFeRD. Some validators reject other filenames.
  • EN16931 is the baseline profile for mandatory EU e-invoicing (Directive 2014/55/EU). Check your national requirements for the correct profile.
  • Validate the output with the Mustang library (Java) or an online Factur-X validator before sending to customers.

Create an XRechnung 3.0 e-invoice in Rust

Generate EN 16931–compliant XML invoices for German B2B and B2G e-invoicing. PDFluent validates all 344 business rules before export so your invoices reach recipients without rejection.

rust
use pdfluent::einvoice::{XRechnung, InvoiceData, TaxCategory};

fn main() -> pdfluent::Result<()> {
    let invoice = XRechnung::builder()
        .invoice_number("RE-2027-001")
        .issue_date(2027, 1, 15)
        .due_date(2027, 2, 15)
        .seller("Muster GmbH", "DE123456789")   // name + VAT ID
        .buyer("Kunde AG", "DE987654321")
        .line_item("Consulting services", 10.0, 150.00, TaxCategory::S, 19.0)
        .build()?;

    // Validate against all 344 EN 16931 business rules before sending
    invoice.validate_en16931()?;

    // Export as XRechnung 3.0 XML
    invoice.save_xml("invoice_RE-2027-001.xml")?;
    Ok(())
}
  1. Collect all mandatory invoice fields

    EN 16931 defines a strict set of mandatory fields. Missing any of them will cause the invoice to fail KoSIT validation. At minimum you need: invoice number, issue date, due date, seller name and VAT ID, buyer name and VAT ID, at least one line item with quantity, unit price, VAT category, and VAT rate.

    rust
    use pdfluent::einvoice::{XRechnung, TaxCategory};
    
    let invoice = XRechnung::builder()
        // Mandatory identification
        .invoice_number("RE-2027-001")
        .invoice_type_code("380")          // 380 = commercial invoice
        .issue_date(2027, 1, 15)
        .due_date(2027, 2, 15)
    
        // Seller (supplier)
        .seller("Muster GmbH", "DE123456789")
        .seller_address("Musterstraße 1", "10115", "Berlin", "DE")
    
        // Buyer (customer)
        .buyer("Kunde AG", "DE987654321")
        .buyer_address("Hauptstraße 5", "80331", "München", "DE")
    
        // Payment
        .payment_iban("DE89370400440532013000")
        .payment_reference("RE-2027-001");
  2. Add line items with VAT categories

    Each line item requires a description, quantity, unit price, VAT category code, and VAT rate. EN 16931 defines standardised VAT category codes: S = standard rate, Z = zero rate, E = exempt. The totals are calculated automatically.

    rust
    use pdfluent::einvoice::TaxCategory;
    
    let invoice = invoice
        // description, quantity, unit_price_eur, vat_category, vat_rate_pct
        .line_item("Consulting services Q1 2027", 10.0, 150.00, TaxCategory::S, 19.0)
        .line_item("Software licence (annual)", 1.0, 2400.00, TaxCategory::S, 19.0)
        .line_item("Travel expenses (reimbursement)", 1.0, 340.00, TaxCategory::Z, 0.0)
        .build()?;
    
    // Totals are derived automatically:
    // net total, VAT amount per rate, gross total
  3. Validate against EN 16931 business rules

    Before submitting or storing the invoice, run the built-in EN 16931 validator. It checks all 344 business rules defined in the standard, including mathematical consistency rules, mandatory field presence, and code list constraints. Fix any reported violations before proceeding.

    rust
    match invoice.validate_en16931() {
        Ok(()) => println!("Validation passed — all 344 EN 16931 rules satisfied"),
        Err(violations) => {
            for v in &violations {
                eprintln!("Rule {}: {}", v.rule_id, v.message);
            }
            return Err(violations.into());
        }
    }
  4. Export as XRechnung 3.0 XML

    XRechnung uses the UN/CEFACT Cross Industry Invoice (CII) XML syntax. The output is pure XML — there is no embedded PDF. For invoices that also need to be human-readable, use ZUGFeRD/Factur-X instead (a hybrid PDF/A-3 with embedded XML).

    rust
    // Export as XRechnung 3.0 CII XML
    invoice.save_xml("invoice_RE-2027-001.xml")?;
    
    println!("XRechnung written to invoice_RE-2027-001.xml");
    println!("Submit via Peppol or deliver directly to the recipient.");
  5. Submit via Peppol or deliver directly

    For B2G (federal procurement), submit via the central Peppol access point or the OZG-RE (Onlinezugangsgesetz-Rechnungseingang) portal. For B2B, agree with your trading partner on the delivery channel. Peppol over AS4 is the recommended standard channel in Germany.

    rust
    // Optional: submit directly to a Peppol access point
    // (requires a registered Peppol participant ID)
    use pdfluent::einvoice::PeppolClient;
    
    let client = PeppolClient::new("https://your-access-point.example.com")?;
    client.send(&invoice, "0088:4012345678901")?; // recipient Peppol ID
  • XRechnung is pure XML — it has no embedded PDF or human-readable layer. For invoices that must also be printable or readable by humans without specialist software, use ZUGFeRD (Factur-X) instead.
  • The KoSIT reference validator (open source, Java) is the authoritative tool for XRechnung validation in Germany. PDFluent's built-in validator implements the same EN 16931 rule set.
  • Mandatory for B2G (federal procurement) in Germany since 2020. For B2B: businesses with turnover above €800K must send e-invoices from January 2027; all businesses from January 2028.
  • EN 16931 is a European standard — the same rule set applies to other national formats including Italy's FatturaPA, France's Factur-X, and the Netherlands' NLCIUS.
  • Retain XRechnung XML files for the full statutory retention period (10 years in Germany under §147 AO). The XML itself is the legally relevant document.

Extract ZUGFeRD or Factur-X XML from a PDF in Rust

Read the structured invoice XML embedded inside a ZUGFeRD or Factur-X PDF. Parse it for automated accounting import.

rust
use pdfluent::PdfDocument;

fn main() -> pdfluent::Result<()> {
    let doc = PdfDocument::open("invoice.pdf")?;

    if let Some(einvoice) = doc.extract_einvoice()? {
        println!("Profile: {:?}", einvoice.profile());
        println!("XML length: {} bytes", einvoice.xml().len());
        std::fs::write("extracted-invoice.xml", einvoice.xml())?;
    } else {
        println!("No e-invoice XML found in this PDF.");
    }

    Ok(())
}
  1. Check whether the PDF contains an e-invoice

    Use has_einvoice() to quickly check before attempting extraction. This reads only the PDF attachment table.

    rust
    use pdfluent::PdfDocument;
    
    let doc = PdfDocument::open("invoice.pdf")?;
    
    if doc.has_einvoice() {
        println!("E-invoice XML found.");
    } else {
        println!("No e-invoice embedded.");
    }
  2. Extract the XML and detect the profile

    extract_einvoice() returns an EInvoiceData struct. The profile() method identifies ZUGFeRD MINIMUM, BASIC, EN16931, or EXTENDED.

    rust
    use pdfluent::EInvoiceProfile;
    
    let einvoice = doc.extract_einvoice()?.unwrap();
    
    println!("Profile: {:?}", einvoice.profile());
    
    match einvoice.profile() {
        EInvoiceProfile::Minimum      => println!("Basic routing data only"),
        EInvoiceProfile::BasicWl      => println!("Line items without account data"),
        EInvoiceProfile::EN16931      => println!("Full invoice, EU compliant"),
        EInvoiceProfile::Extended     => println!("Extended German profile"),
        EInvoiceProfile::XRechnung    => println!("German public sector"),
        _ => {}
    }
  3. Parse the XML with quick-xml or roxmltree

    The extracted XML is a standard Rust String. Use any XML parser to read the invoice fields.

    rust
    use roxmltree::Document as XmlDoc;
    
    let xml_str = einvoice.xml();
    let xml = XmlDoc::parse(&xml_str)?;
    
    // Read invoice number
    let invoice_id = xml
        .descendants()
        .find(|n| n.has_tag_name("ID") && n.parent().map_or(false, |p| p.has_tag_name("ExchangedDocument")))
        .and_then(|n| n.text());
    
    println!("Invoice ID: {:?}", invoice_id);
  4. Batch extract XML from a folder of PDFs

    Combine with the batch processing pattern to extract XML from many invoices at once.

    rust
    use std::fs;
    use pdfluent::PdfDocument;
    
    let dir = fs::read_dir("./invoices")?;
    
    for entry in dir.filter_map(|e| e.ok()) {
        let path = entry.path();
        if path.extension().map_or(false, |e| e == "pdf") {
            let doc = PdfDocument::open(&path)?;
            if let Some(inv) = doc.extract_einvoice()? {
                let xml_path = path.with_extension("xml");
                fs::write(&xml_path, inv.xml())?;
                println!("Extracted: {}", xml_path.display());
            }
        }
    }
  • The XML attachment in Factur-X PDFs is always named factur-x.xml. In ZUGFeRD 1.0 PDFs, it may be named ZUGFeRD-invoice.xml. PDFluent checks both names.
  • PDF/A-3 attachments have an AFRelationship entry set to Alternative. PDFluent searches for this to locate the invoice XML.
  • If the PDF has multiple XML attachments, extract_einvoice() returns the first one matching a known e-invoice profile.