Introducing zugpferd: XRechnung and ZUGFeRD e-invoicing for Ruby

Published on Monday, February 16, 2026

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 - BigDecimal amounts, Date fields, simple Ruby objects mapped to EN 16931 Business Terms

Installation

Add it to your Gemfile:

gem "zugpferd"

Then:

bundle install

For 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 ghostscript

Document types

zugpferd supports all common EN 16931 document types, each with a dedicated model class:

ClassType CodeDescription
Model::Invoice380Commercial Invoice
Model::CreditNote381Credit Note
Model::CorrectedInvoice384Corrected Invoice
Model::SelfBilledInvoice389Self-billed Invoice
Model::PartialInvoice326Partial Invoice
Model::PrepaymentInvoice386Prepayment 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 << line3

Tax, 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-schemas

Then 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."
end

Schematron 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.

What are your thoughts about
"Introducing zugpferd: XRechnung and ZUGFeRD e-invoicing for Ruby"?
Drop me a line - I'm looking forward to your feedback!