Automation MCP Server Features Blog Pricing Contact
Integration Factur-X Format

Create and Validate Factur-X Invoices in Ruby: Complete Guide

A working Factur-X pipeline in Ruby without a single gem to maintain: generating hybrid PDF/A-3 invoices from a plain hash, validating against the official EN 16931 rules, reading incoming documents into your app, and parsing legacy supplier PDFs with AI. Net::HTTP from the standard library, plus Rails patterns with Faraday, ActiveJob, and ActiveStorage.

From September 1, 2026, every business subject to French VAT must be able to receive structured e-invoices, and large companies plus ETIs must start issuing them. SMEs follow a year later. Factur-X sits at the center of the reform for most issuers because it is the one core format that stays readable by a human while carrying machine-readable data. If your billing runs on Ruby, a Rails monolith, a Sinatra service, or a plain script in a cron job, the question is how to produce and check compliant Factur-X without adopting a stack of XML, PDF, and validation tooling that the Ruby ecosystem does not actually offer.

That last part is worth saying plainly, because it shapes the whole integration: there is no maintained Factur-X Ruby gem. Java has Mustangproject; Ruby has Nokogiri, some PDF libraries, and a hard stop at Schematron validation. This guide covers why that is, and then shows the practical alternative: the full lifecycle (create, validate, extract, parse) as HTTP calls against the Factur-X API, using nothing but Net::HTTP from the standard library, with a Rails section for teams on Faraday, ActiveJob, and ActiveStorage.

One naming note up front: Factur-X and Germany's ZUGFeRD 2.x are the same technical standard, CII XML embedded in a PDF/A-3, under a French and a German brand. This guide takes the French perspective. For German customers, both B2B hybrid PDFs and XRechnung for the public sector, see the companion guide to ZUGFeRD and XRechnung in Ruby.

Ruby and the French e-invoicing reform

The reform changes both the document and the delivery route, and both affect your Ruby code:

Deadlines. September 1, 2026: reception becomes mandatory for all French businesses, issuance for grandes entreprises and ETIs. September 1, 2027: issuance extends to PME and micro-entreprises. An e-reporting obligation for B2C and cross-border transactions runs on the same calendar.

Invoices flow through PDPs, not email. Domestic B2B invoices are exchanged via registered Plateformes de Dématérialisation Partenaires, which validate documents on acceptance, route them through the central directory, and report tax data to the administration. A PDP will reject a malformed file, which is why the validate call belongs in your pipeline before submission, not in a support ticket after it.

B2G continues on Chorus Pro. Invoices to the French public sector have gone through Chorus Pro for years, and Factur-X is an accepted format there, so one generation pipeline serves both audiences.

The core format set. The reform's socle is Factur-X, UBL, and CII. Factur-X wins by default whenever your customers still expect a PDF they can open, which for most Ruby shops sending invoices to real businesses is nearly always.


Factur-X profiles in two minutes

Factur-X is published by FNFE-MPE together with the German FeRD (Factur-X 1.0.7 corresponds to ZUGFeRD 2.3; same artifacts, two version numbers). The standard defines five profiles that declare how much structured data the embedded XML carries:

ProfileStructured contentFrench reform status
MINIMUMHeader amounts onlyBooking aid, not a full e-invoice
BASIC WLHeader level, no linesBooking aid, not a full e-invoice
BASICFull invoice with simple linesQualifies as a structured e-invoice
EN 16931 (Comfort)Complete EN 16931 semantic modelQualifies; the recommended default
EXTENDEDEN 16931 plus industry extensionsQualifies, for complex scenarios

Generate EN 16931 unless a trading partner demands otherwise. That is what /v1/create/facturx emits, and whatever profile a document declares in BT-24 is the rule set it gets validated against.


Is there a Factur-X Ruby gem?

Short answer: no maintained one. Search RubyGems and you will find experiments from the ZUGFeRD 1.0 era that predate the current standard and have been dormant for years. Python has factur-x, Java has Mustangproject; Ruby has a gap. To understand why the gap has not been filled, look at what a complete implementation needs:

CII XML generation. Building the UN/CEFACT CII document with Nokogiri is feasible but unforgiving: the EN 16931 semantic model spans roughly 160 business terms across a deeply nested namespace structure, and around 200 business rules constrain how they combine (totals that must reconcile, VAT category codes that dictate which fields become mandatory, and so on). Getting the XML well-formed is a week; getting it rule-compliant is the real project.

PDF/A-3 with embedded files. Prawn writes ordinary PDFs, not archival PDF/A-3. HexaPDF is the strongest Ruby option here and can embed file attachments and target PDF/A conformance, with the caveat that it is AGPL-licensed (commercial licenses available) and that assembling the exact XMP metadata Factur-X requires, profile declaration included, is still on you. Converting an arbitrary existing PDF into conformant PDF/A-3, with fonts embedded and color spaces fixed, is unsolved in Ruby.

Schematron validation, the hard stop. The official EN 16931 and Factur-X rule sets are distributed as Schematron, and the reference pipelines compile them to XSLT 2.0. Ruby's XML stack, Nokogiri wrapping libxslt, implements XSLT 1.0 only. There is no Saxon for Ruby. In practice, teams that want local validation end up shelling out to a Java process, at which point the "pure Ruby" goal has already been abandoned.

None of this makes Ruby a bad choice for invoicing. It means the compliance layer belongs behind an API rather than inside your Gemfile, and the rest of this guide shows exactly that: every operation as one HTTP call, with the official rule sets maintained server-side.


Client setup with Net::HTTP

The standard library covers everything, including multipart uploads. No Gemfile changes are needed for any snippet in this guide; Rails users can swap in Faraday later without changing the shape of the calls.

Create a free InvoiceXML account to get an API key with 100 free credits, no credit card needed. Keep the key in the environment or your credential store, never in source:

require "net/http"
require "json"
require "uri"

BASE_URL = "https://api.invoicexml.com"
API_KEY  = ENV.fetch("INVOICEXML_API_KEY")

def api_http(uri)
  http = Net::HTTP.new(uri.host, uri.port)
  http.use_ssl = true
  http.open_timeout = 10
  http.read_timeout = 120
  http
end

Create Factur-X invoices in Ruby

The payload is a plain Ruby hash, serialized with JSON.generate. Note the French specifics: the intra-community VAT number (N° TVA, FR + 2-digit key + SIREN), the SIREN in legalRegistration with the SIRENE scheme code 0002, and mixed VAT rates across the lines (20% standard, 5.5% reduced). You send net prices and quantities; totals and the per-rate VAT breakdown are computed for you.

invoice = {
  invoice: {
    invoiceNumber: "FA-2026-0198",
    issueDate: "2026-09-15",
    currency: "EUR",
    seller: {
      name: "Studio Horizon SARL",
      vatIdentifier: "FR46987654321",
      legalRegistration: { identifier: "987654321", schemeId: "0002" },
      postalAddress: {
        line1: "42 Quai des Chartrons",
        city: "Bordeaux",
        postCode: "33000",
        country: "FR"
      }
    },
    buyer: {
      name: "Maison Delacroix SAS",
      postalAddress: {
        line1: "12 Rue Crebillon",
        city: "Nantes",
        postCode: "44000",
        country: "FR"
      }
    },
    paymentDetails: {
      paymentAccountIdentifier: "FR7612345987650123456789014"
    },
    lines: [
      {
        quantity: 3,
        item: { name: "Refonte identite visuelle" },
        priceDetails: { netPrice: 1200.00 },
        vatInformation: { rate: 20 }
      },
      {
        quantity: 150,
        item: { name: "Guide de marque imprime" },
        priceDetails: { netPrice: 9.80 },
        vatInformation: { rate: 5.5 }
      }
    ]
  }
}

uri = URI("#{BASE_URL}/v1/create/facturx")
request = Net::HTTP::Post.new(uri)
request["Authorization"] = "Bearer #{API_KEY}"
request["Content-Type"]  = "application/json"
request.body = JSON.generate(invoice)

response = api_http(uri).request(request)
raise response.body unless response.is_a?(Net::HTTPSuccess)

File.binwrite("facture-facturx.pdf", response.body)

The response body is a finished Factur-X EN 16931 document: PDF/A-3 container, embedded factur-x.xml, XMP metadata declaring the profile, already validated against the official Schematron. If your data would produce a non-compliant invoice (a VAT breakdown that does not reconcile, a missing mandatory field), the API refuses with HTTP 400 and lists the violated rules as structured findings, so nothing invalid ever reaches a customer or a PDP.


Validate Factur-X invoices in Ruby

Validation earns its keep twice: as a gate before outgoing invoices meet a PDP's acceptance checks, and on incoming supplier documents that claim to be Factur-X. The endpoint pulls the embedded XML out of the PDF, detects the declared profile from BT-24, and applies that profile's official schema and business rules, including the French BR-FR constraints.

Here Ruby has a small advantage over most languages: Net::HTTP does multipart natively through set_form, so where the Java and C# guides hand-roll a multipart body builder, Ruby needs three lines:

def validate_facturx(path)
  uri = URI("#{BASE_URL}/v1/validate/facturx")
  request = Net::HTTP::Post.new(uri)
  request["Authorization"] = "Bearer #{API_KEY}"

  File.open(path, "rb") do |file|
    request.set_form([["file", file]], "multipart/form-data")
    JSON.parse(api_http(uri).request(request).body)
  end
end

report = validate_facturx("facture-facturx.pdf")

if report["valid"]
  puts "Compliant Factur-X, profile #{report.dig("data", "profile")}"
else
  report["errors"].each do |finding|
    puts "[#{finding["rule"]}] #{finding["message"]}"
    puts "  fields: #{Array(finding["fields"]).join(", ")}"
  end
end

A finished validation always answers HTTP 200: the pass or fail verdict lives in valid, and non-2xx statuses are reserved for transport problems such as a bad API key or an unreadable upload. Each finding carries the rule id, a plain-language message, and the field paths into the document, so an AP workflow can point a supplier at the exact value that failed. The same call belongs in your RSpec suite: validate a freshly generated invoice on every build and regressions in your payload mapping surface long before a PDP finds them.


Extract Factur-X data as JSON

On the receiving side, incoming Factur-X files already carry structured data, and the extract endpoint returns it parsed and normalized, ready for your models. This is pure XML parsing, no AI: what the supplier declared is exactly what you import. It accepts a hybrid PDF (the embedded XML is pulled out automatically) or a standalone CII / UBL XML file:

def extract_invoice(path)
  uri = URI("#{BASE_URL}/v1/extract/json")
  request = Net::HTTP::Post.new(uri)
  request["Authorization"] = "Bearer #{API_KEY}"

  File.open(path, "rb") do |file|
    request.set_form([["file", file]], "multipart/form-data")
    JSON.parse(api_http(uri).request(request).body)
  end
end

facture = extract_invoice("fournisseur-facturx.pdf")
puts "Facture #{facture["invoiceNumber"]} de #{facture.dig("seller", "name")}"
puts "Total TTC : #{facture["totalAmount"]}"

Prefer the raw embedded XML? /v1/extract/xml returns the CII document as application/xml for Nokogiri to work on directly.


Parse legacy invoice PDFs with AI

Not every supplier will send Factur-X on day one of the reform, and your AP inbox will keep receiving ordinary PDFs, scans, and photographed paper invoices for years. For those, POST /v1/parse/json reads the document with AI and returns the same normalized JSON shape as the extract endpoint, plus a confidence object that makes the non-determinism explicit: an overall score and four area scores (seller identification, buyer identification, tax calculation, line items), each from 0.0 to 1.0. Automate the confident reads, route the shaky ones to a human:

def parse_invoice_pdf(path)
  uri = URI("#{BASE_URL}/v1/parse/json")
  request = Net::HTTP::Post.new(uri)
  request["Authorization"] = "Bearer #{API_KEY}"

  File.open(path, "rb") do |file|
    request.set_form([["file", file]], "multipart/form-data")
    JSON.parse(api_http(uri).request(request).body)
  end
end

result     = parse_invoice_pdf("fournisseur-scan.pdf")
facture    = result["invoice"]
confidence = result.dig("confidence", "overall")

if confidence < 0.7
  queue_for_human_review(facture, result["confidence"])
else
  import_into_erp(facture)
end

The two endpoints compose into one intake path: try /v1/extract/json first, and when it answers 400 with error code 4006 (no embedded XML, so not a Factur-X hybrid), hand the file to the AI parser instead. The JSON envelope matches the /v1/create request body, so a parsed supplier invoice can even be piped back into invoice creation.

One more receiving-side endpoint earns its keep in French AP workflows: POST /v1/extract/attachments collects the supporting documents a sender embedded inside the e-invoice (BG-24: delivery notes, timesheets, the original order) and returns the original files as one ZIP. An invoice without embedded attachments answers 404 rather than an empty archive. See the attachment extraction reference.


Rails integration

In a Rails app, promote the calls into a service object. Faraday with faraday-multipart is the idiomatic client, and Rails credentials keep the key out of the environment-variable sprawl:

# Gemfile
gem "faraday"
gem "faraday-multipart"

# app/services/facturx_service.rb
class FacturxService
  BASE_URL = "https://api.invoicexml.com"

  def create(invoice_attributes)
    response = connection.post("/v1/create/facturx") do |req|
      req.headers["Content-Type"] = "application/json"
      req.body = JSON.generate(invoice: invoice_attributes)
    end
    raise FacturxError, response.body unless response.success?
    response.body
  end

  def validate(pdf_bytes)
    part = Faraday::Multipart::FilePart.new(
      StringIO.new(pdf_bytes), "application/pdf", "facture.pdf"
    )
    response = connection.post("/v1/validate/facturx", { file: part })
    JSON.parse(response.body)
  end

  private

  def connection
    @connection ||= Faraday.new(url: BASE_URL) do |f|
      f.request :multipart
      f.headers["Authorization"] =
        "Bearer #{Rails.application.credentials.dig(:invoicexml, :api_key)}"
    end
  end
end

class FacturxError < StandardError; end

Generation belongs in a background job, not a request cycle, and ActiveStorage is the natural home for the returned PDF:

# app/jobs/generate_facturx_job.rb
class GenerateFacturxJob < ApplicationJob
  queue_as :invoices
  retry_on FacturxError, wait: :polynomially_longer, attempts: 3

  def perform(invoice_id)
    invoice = Invoice.find(invoice_id)
    pdf = FacturxService.new.create(invoice.to_facturx_payload)

    invoice.document.attach(
      io: StringIO.new(pdf),
      filename: "#{invoice.number}-facturx.pdf",
      content_type: "application/pdf"
    )
  end
end

One caution on the retry policy: retry transport failures and 5xx responses, but treat HTTP 400 as terminal. A 400 carries validation findings about your data, and resending the same payload produces the same findings. Log them, surface them, fix the mapping.

The other Rails-native win is the test suite. A single request spec that generates and validates an invoice pins down your whole payload mapping:

RSpec.describe FacturxService do
  it "produces compliant invoices from our billing data" do
    service = described_class.new
    pdf = service.create(build(:invoice, :with_mixed_vat_rates).to_facturx_payload)
    report = service.validate(pdf)

    expect(report["valid"]).to be(true), report["errors"].inspect
  end
end

Building it yourself vs the API

The honest framing for Ruby differs from the Java version of this decision, because there is no Mustangproject to weigh:

Building locally means assembling, not installing. Nokogiri for the CII XML, HexaPDF (mind the AGPL) for the PDF/A-3 container and attachments, hand-built XMP metadata, and either a Java sidecar or no answer at all for Schematron validation. Every FNFE-MPE release becomes your migration project (monitor the announcement, rebuild the artifacts, retest, redeploy), and PDF/A conformance failures surface as PDP rejections in production. It is a permanent tax on a solved problem, paid in engineering time that never touches your actual product.

The API keeps the compliance surface out of your codebase. Current FNFE-MPE artifacts applied server-side the day they take effect, PDF/A-3 output that holds up under PDP acceptance checks, deterministic extraction for incoming Factur-X plus AI parsing with confidence scores for the plain PDFs your suppliers still send, findings with field paths your support team can forward verbatim, and the same integration extending to ZUGFeRD and XRechnung or Peppol UBL by swapping the endpoint.

And the moving target stays ours. The reform's rules will keep evolving after September 2026, on the government's calendar rather than yours. With the API there is nothing to monitor, no artifact to rebuild, and no redeploy when they do: your integration is finished the day it works, backed by a team that does European e-invoicing compliance all day and professional support when you need it. That is the difference between a toolkit and a complete compliance service.

Data handling is strict throughout: requests are processed in memory and discarded when the response ships. No storage, no logging of document content, no training on your data. SIREN numbers, IBANs, and customer relationships exist server-side only for the duration of one HTTP exchange.


Endpoint reference

OperationEndpointInputOutput
Create Factur-XPOST /v1/create/facturxJSONPDF/A-3 binary
Validate Factur-XPOST /v1/validate/facturxFactur-X PDFValidation JSON
Extract as JSONPOST /v1/extract/jsonFactur-X PDF or CII/UBL XMLStructured JSON
Parse PDF with AIPOST /v1/parse/jsonAny invoice PDF (typed, scanned, photo)Structured JSON + confidence
Extract attachmentsPOST /v1/extract/attachmentsFactur-X PDF or CII/UBL XMLZIP archive
Extract CII XMLPOST /v1/extract/xmlFactur-X PDFCII XML
Render CII as PDFPOST /v1/render/cii/to/pdfCII XMLPDF binary

Full OpenAPI 3.1 schema: api.invoicexml.com/v1/openapi  |  Interactive API explorer: api.invoicexml.com/v1/scalar


Get started

Every snippet in this guide is complete and self-contained: paste it into a file, set INVOICEXML_API_KEY, and run it with a stock Ruby 3.x, no Gemfile required. Runnable versions of these snippets, alongside Python, PHP, Java, C#, and Node.js examples, live in the Ruby folder of the examples repository.

Create a free InvoiceXML account → get 100 credits for free, no credit card required.

Related resources:


InvoiceXML is a REST API for European e-invoice compliance covering Factur-X, ZUGFeRD, XRechnung, Peppol UBL, and CII. Stateless processing, GDPR compliant by architecture, and callable from any Ruby application: Rails, Sinatra, Hanami, or a plain script.

Start free today

Ready to automate your invoices?

Validate, convert and embed compliant e-invoices through one API. Start your 30-day free trial. No credit card required.

GDPR Compliant No credit card required Setup in minutes
Peppol UBL
Factur-X
EN 16931
142 / 142 passed
Compliant
PDF/A-3 embedded