What's it all about?
If you're dealing with invoicing in Germany or Europe, chances are you've heard of XRechnung and ZUGFeRD. Starting January 2025, e-invoicing became mandatory for B2B transactions in Germany.
The European standard EN 16931 defines a semantic data model for electronic invoices and supports two XML syntaxes: UBL 2.1 and UN/CEFACT CII. XRechnung is the German compliance profile on top of EN 16931, while ZUGFeRD (also known as Factur-X in France) adds the ability to embed the structured XML data into a PDF/A-3 document - creating a hybrid invoice that is both human-readable and machine-processable.
Most existing libraries for this are written in Java or .NET. I wanted something that feels native to Ruby - plain Ruby objects, BigDecimal for monetary values, Date fields, and a clean API. That's why I built zugpferd.
Introducing zugpferd
zugpferd is a Ruby gem for reading, writing, and converting e-invoices according to EN 16931. It supports both UBL 2.1 and UN/CEFACT CII syntaxes, and optionally handles PDF/A-3 embedding and Schematron validation.
Features
- UBL 2.1 & CII - Full support for both EN 16931 syntaxes: read, write, and roundtrip for any XRechnung or ZUGFeRD document
- Multiple document types - Invoice, Credit Note, Corrected Invoice, Self-billed Invoice, Partial Invoice, and Prepayment Invoice
- PDF/A-3 embedding - Create ZUGFeRD/Factur-X hybrid invoices by embedding XML into PDF/A-3 via Ghostscript
- Validation - Validate invoices against EN 16931 and XRechnung business rules using Schematron (optional Java dependency)
- Format conversion - Read CII, write UBL (or vice versa) through a format-agnostic data model
- Pure Ruby data model -
BigDecimalamounts,Datefields, simple Ruby objects mapped to EN 16931 Business Terms
Installation
Add it to your Gemfile:
gem "zugpferd"Then:
bundle installFor PDF/A-3 embedding, you'll also need Ghostscript installed:
# Debian/Ubuntu
sudo apt-get install ghostscript
# Arch Linux
sudo pacman -S ghostscript
# macOS
brew install ghostscriptDocument types
zugpferd supports all common EN 16931 document types, each with a dedicated model class:
| Class | Type Code | Description |
|---|---|---|
Model::Invoice | 380 | Commercial Invoice |
Model::CreditNote | 381 | Credit Note |
Model::CorrectedInvoice | 384 | Corrected Invoice |
Model::SelfBilledInvoice | 389 | Self-billed Invoice |
Model::PartialInvoice | 326 | Partial Invoice |
Model::PrepaymentInvoice | 386 | Prepayment Invoice |
All document types share the same attributes and work identically with both UBL and CII readers and writers:
credit_note = Zugpferd::Model::CreditNote.new(
number: "CN-001",
issue_date: Date.today,
currency_code: "EUR"
)
corrected = Zugpferd::Model::CorrectedInvoice.new(
number: "C-001",
issue_date: Date.today
)In UBL, a CreditNote produces a <CreditNote> root element while all other types produce <Invoice>. In CII, the structure is identical for all types - only the type code differs.
Building an invoice
Let's walk through creating an XRechnung-compliant invoice, validating it, and embedding it into a ZUGFeRD PDF.
First, require the necessary modules:
require "zugpferd"
require "zugpferd/pdf"
require "zugpferd/validation"
require "bigdecimal"Setting up the invoice header
invoice = Zugpferd::Model::Invoice.new(
number: "RE-2024-0042",
issue_date: Date.new(2024, 6, 15),
currency_code: "EUR"
)
invoice.due_date = Date.new(2024, 7, 15)
invoice.delivery_date = Date.new(2024, 6, 15)
invoice.buyer_reference = "LEITWEG-123-456"
invoice.customization_id = "urn:cen.eu:en16931:2017#compliant#urn:xeinkauf.de:kosit:xrechnung_3.0"
invoice.profile_id = "urn:fdc:peppol.eu:2017:poacc:billing:01:1.0"Adding seller and buyer
invoice.seller = Zugpferd::Model::TradeParty.new(name: "Zugpferd GmbH")
invoice.seller.vat_identifier = "DE123456789"
invoice.seller.electronic_address = "zugpferd@example.com"
invoice.seller.electronic_address_scheme = "EM"
invoice.seller.postal_address = Zugpferd::Model::PostalAddress.new(
country_code: "DE",
city_name: "Frankfurt am Main",
postal_zone: "60311",
street_name: "Kaiserstr. 42"
)
invoice.seller.contact = Zugpferd::Model::Contact.new(
name: "Max Mustermann",
telephone: "+49 69 12345678",
email: "rechnung@zugpferd.example.com"
)
invoice.buyer = Zugpferd::Model::TradeParty.new(name: "Muster AG")
invoice.buyer.electronic_address = "muster@example.com"
invoice.buyer.electronic_address_scheme = "EM"
invoice.buyer.postal_address = Zugpferd::Model::PostalAddress.new(
country_code: "DE",
city_name: "Berlin",
postal_zone: "10115",
street_name: "Unter den Linden 1"
)Adding line items
# Position 1: Software licenses
line1 = Zugpferd::Model::LineItem.new(
id: "1",
invoiced_quantity: "5",
unit_code: "C62",
line_extension_amount: "2500.00"
)
line1.item = Zugpferd::Model::Item.new(
name: "Zugpferd Enterprise License",
tax_category: "S",
tax_percent: BigDecimal("19")
)
line1.price = Zugpferd::Model::Price.new(amount: "500.00")
invoice.line_items << line1
# Position 2: Consulting
line2 = Zugpferd::Model::LineItem.new(
id: "2",
invoiced_quantity: "16",
unit_code: "HUR",
line_extension_amount: "2400.00"
)
line2.item = Zugpferd::Model::Item.new(
name: "Technische Beratung E-Invoicing",
tax_category: "S",
tax_percent: BigDecimal("19")
)
line2.price = Zugpferd::Model::Price.new(amount: "150.00")
invoice.line_items << line2
# Position 3: Workshop
line3 = Zugpferd::Model::LineItem.new(
id: "3",
invoiced_quantity: "1",
unit_code: "C62",
line_extension_amount: "1800.00"
)
line3.item = Zugpferd::Model::Item.new(
name: "Workshop: XRechnung & ZUGFeRD in der Praxis (2 Tage)",
tax_category: "S",
tax_percent: BigDecimal("19")
)
line3.price = Zugpferd::Model::Price.new(amount: "1800.00")
invoice.line_items << line3Tax, totals, and payment
netto = BigDecimal("6700.00")
steuer = BigDecimal("1273.00")
brutto = BigDecimal("7973.00")
invoice.tax_breakdown = Zugpferd::Model::TaxBreakdown.new(
tax_amount: steuer.to_s("F"),
currency_code: "EUR"
)
invoice.tax_breakdown.subtotals << Zugpferd::Model::TaxSubtotal.new(
taxable_amount: netto.to_s("F"),
tax_amount: steuer.to_s("F"),
category_code: "S",
currency_code: "EUR",
percent: BigDecimal("19")
)
invoice.monetary_totals = Zugpferd::Model::MonetaryTotals.new(
line_extension_amount: netto.to_s("F"),
tax_exclusive_amount: netto.to_s("F"),
tax_inclusive_amount: brutto.to_s("F"),
payable_amount: brutto.to_s("F")
)
invoice.payment_instructions = Zugpferd::Model::PaymentInstructions.new(
payment_means_code: "58",
account_id: "DE89370400440532013000"
)Writing XML
Now generate the CII XML:
xml = Zugpferd::CII::Writer.new.write(invoice)
File.write("invoice.xml", xml)Need UBL instead? Just swap the writer:
xml = Zugpferd::UBL::Writer.new.write(invoice)Format conversion
Since the data model is format-agnostic, converting between UBL and CII is straightforward:
# Read a CII invoice, write it as UBL
cii_xml = File.read("invoice_cii.xml")
invoice = Zugpferd::CII::Reader.new.read(cii_xml)
ubl_xml = Zugpferd::UBL::Writer.new.write(invoice)Validation
Before sending an invoice, you probably want to make sure it's valid. zugpferd supports Schematron validation against EN 16931 and XRechnung business rules.
First, download the required schemas:
bin/setup-schemasThen validate:
validator = Zugpferd::Validation::SchematronValidator.new(
schemas_path: "vendor/schemas"
)
errors = validator.validate_all(xml, rule_sets: [:cen_cii, :xrechnung_cii])
fatals = errors.select { |e| e.flag == "fatal" }
if fatals.any?
fatals.each { |e| puts " [#{e.id}] #{e.text}" }
else
puts "Invoice is valid."
endSchematron validation requires Java and Saxon HE. If you don't need it, just skip the require "zugpferd/validation" - the core library works without Java.
PDF/A-3 embedding
This is where ZUGFeRD comes in: embedding the structured XML into a PDF, creating a hybrid document that works for both humans and machines.
embedder = Zugpferd::PDF::Embedder.new
embedder.embed(
pdf_path: "invoice.pdf",
xml: xml,
output_path: "invoice_zugferd.pdf",
version: "2p1",
conformance_level: "XRECHNUNG"
)The result is a PDF/A-3 compliant file with the XML attached as factur-x.xml. This works with ZUGFeRD 1.0, 2.0, and 2.1, and supports all conformance levels from MINIMUM to XRECHNUNG.
Reading invoices
Of course, zugpferd also reads existing invoices:
# UBL
xml = File.read("invoice.xml")
invoice = Zugpferd::UBL::Reader.new.read(xml)
puts invoice.number # => "RE-2024-0042"
puts invoice.seller.name # => "Zugpferd GmbH"
puts invoice.monetary_totals.payable_amount # => 7973.0
# CII
invoice = Zugpferd::CII::Reader.new.read(cii_xml)The reader auto-detects document types: Credit Notes, Corrected Invoices, and all other EN 16931 type codes are mapped to their respective model classes.
Wrapping up
zugpferd aims to make e-invoicing in Ruby feel natural - no XML wrangling, no Java dependencies for core functionality, just plain Ruby objects.
For the full documentation, check out the docs.
The source code is available on GitHub.
As this is a fairly new project, feedback and contributions are welcome.