A developer guide for creating compliant e-invoice PDFs and extracting their embedded XML data using the PDFluent SDK.
XML extraction works with the base crate. The einvoice feature adds profile detection and validation helpers.
# Cargo.toml
[dependencies]
pdfluent = { version = "0.9", features = ["einvoice"] }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.
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(())
}E-invoice support requires the pdfa feature for PDF/A-3 conformance and the einvoice feature for ZUGFeRD/Factur-X metadata.
# Cargo.toml
[dependencies]
pdfluent = { version = "0.9", features = ["pdfa", "einvoice"] }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.
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 ...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.
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();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.
use pdfluent::{EInvoiceProfile, ZugferdAttachment};
doc.attach_einvoice(ZugferdAttachment {
xml,
profile: EInvoiceProfile::EN16931,
filename: "factur-x.xml".to_string(),
})?;ZUGFeRD and Factur-X require PDF/A-3b conformance. convert_to_pdfa3() adds the required XMP metadata and output intent.
doc.convert_to_pdfa3()?;
doc.save("invoice-einvoice.pdf")?;
println!("E-invoice PDF saved.");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.
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(())
}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.
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");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.
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 totalBefore 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.
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());
}
}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).
// 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.");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.
// 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 IDRead the structured invoice XML embedded inside a ZUGFeRD or Factur-X PDF. Parse it for automated accounting import.
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(())
}Use has_einvoice() to quickly check before attempting extraction. This reads only the PDF attachment table.
use pdfluent::PdfDocument;
let doc = PdfDocument::open("invoice.pdf")?;
if doc.has_einvoice() {
println!("E-invoice XML found.");
} else {
println!("No e-invoice embedded.");
}extract_einvoice() returns an EInvoiceData struct. The profile() method identifies ZUGFeRD MINIMUM, BASIC, EN16931, or EXTENDED.
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"),
_ => {}
}The extracted XML is a standard Rust String. Use any XML parser to read the invoice fields.
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);Combine with the batch processing pattern to extract XML from many invoices at once.
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());
}
}
}