Skip to content

Ruby SDK

Official Ruby SDK for thelawin.dev.

Requires Ruby 3.0+

Installation

Add to your Gemfile:

ruby
gem 'thelawin', '~> 0.2'

Then:

bash
bundle install

Or install directly:

bash
gem install thelawin

Quick Start

ruby
require 'thelawin'

client = Thelawin::Client.new(api_key: 'env_sandbox_xxx')

result = client.invoice
  .number('2026-001')
  .date('2026-01-15')
  .seller(name: 'Acme GmbH', vat_id: 'DE123456789', city: 'Berlin', country: 'DE')
  .buyer(name: 'Customer AG', city: 'München', country: 'DE')
  .add_item(description: 'Consulting Services', quantity: 8, unit: 'HUR', unit_price: 150, vat_rate: 19)
  .template('minimal')
  .generate

if result.success?
  result.save_pdf('./invoice.pdf')
  puts "Generated: #{result.filename}"
  puts "Format: #{result.format.format_used}"
else
  result.errors.each { |e| puts "#{e[:path]}: #{e[:message]}" }
end

Client Options

ruby
client = Thelawin::Client.new(
  api_key: 'env_sandbox_xxx',
  environment: :production,  # :production or :preview
  timeout: 30                # optional, seconds
)

Environments

EnvironmentURLDescription
:productionhttps://api.thelawin.devProduction API (default)
:previewhttps://api.preview.thelawin.dev:3080Preview/staging API
ruby
# Global configuration
Thelawin.configure do |config|
  config.api_key = 'env_sandbox_xxx'
  config.environment = :preview
end

# Or per-client
client = Thelawin::Client.new(api_key: 'env_sandbox_xxx', environment: :preview)

# Check environment
client.preview?     # => true
client.production?  # => false

Supported Formats

FormatDescriptionOutput
autoAuto-detect based on countries (default)PDF or XML
zugferdZUGFeRD 2.3 (Germany/EU)PDF/A-3 + CII XML
facturxFactur-X 1.0 (France)PDF/A-3 + CII XML
xrechnungXRechnung 3.0 (German B2G)PDF/A-3 + UBL XML
pdfPlain PDF without XMLPDF
ublUBL 2.1 InvoiceXML only
ciiUN/CEFACT CIIXML only
peppolPeppol BIS Billing 3.0XML only
fatturapaFatturaPA 1.2.1 (Italy)XML only

Builder API

Invoice Details

ruby
client.invoice
  .number('2026-001')           # Required
  .date('2026-01-15')           # Required, String or Date
  .due_date('2026-02-15')       # Optional
  .currency('EUR')              # Default: 'EUR'
  .notes('Thank you!')          # Optional

Format & Profile

ruby
.format('zugferd')              # 'auto', 'zugferd', 'facturx', 'xrechnung', 'pdf', 'ubl', 'cii', 'peppol', 'fatturapa'
.profile('en16931')             # 'minimum', 'basic_wl', 'basic', 'en16931', 'extended'

Parties

ruby
.seller(
  name: 'Acme GmbH',           # Required
  vat_id: 'DE123456789',       # Required for ZUGFeRD
  street: 'Hauptstraße 1',
  city: 'Berlin',              # Required
  postal_code: '10115',
  country: 'DE',               # Required
  email: 'invoice@acme.de',    # Required for XRechnung
  peppol_id: '0088:123...',    # For Peppol
  codice_fiscale: '...',       # For FatturaPA
)
.buyer(
  name: 'Customer AG',         # Required
  vat_id: 'DE987654321',
  city: 'München',             # Required
  country: 'DE',               # Required
  codice_destinatario: '...',  # For FatturaPA
  pec: 'cliente@pec.it'        # For FatturaPA
)

Line Items

ruby
.add_item(
  description: 'Consulting',   # Required
  quantity: 8,                 # Required
  unit: 'HUR',                 # Required (C62=piece, HUR=hour, DAY=day)
  unit_price: 150.00,          # Required
  vat_rate: 19.0,              # Required
  natura: 'N2.2'               # For FatturaPA (VAT exemption)
)

Format-Specific Fields

ruby
.leitweg_id('04011000-12345-67')   # XRechnung: German B2G routing
.buyer_reference('PO-12345')        # Peppol: Purchase order reference
.tipo_documento('TD01')             # FatturaPA: Document type (TD01=invoice)

Customization

ruby
.template('minimal')           # 'minimal', 'classic', 'compact'
.locale('de')                  # 'en', 'de', 'fr', 'es', 'it'
.accent_color('#8b5cf6')
.footer_text('Thank you!')
ruby
# From file (auto Base64)
.logo_file('./logo.png')
.logo_file('./logo.png', width_mm: 30)

# From Base64
.logo_base64('iVBORw0KGgo...', width_mm: 30)

Result Handling

ruby
result = builder.generate

if result.success?
  puts result.filename           # 'invoice-2026-001.pdf' or '.xml'
  puts result.format.format_used # 'zugferd', 'fatturapa', etc.
  puts result.format.profile     # 'EN16931'
  puts result.format.version     # '2.3'

  # Save to file
  result.save_pdf('./invoice.pdf')  # or result.save('./invoice.xml')

  # Get bytes
  pdf_bytes = result.to_bytes

  # Get data URL
  data_url = result.to_data_url

  # Check if XML-only output
  if result.xml_only?
    # Handle XML-only formats (UBL, Peppol, FatturaPA, etc.)
  end

  # Legal warnings
  result.warnings.each do |warning|
    puts "#{warning.code}: #{warning.message}"
  end
else
  result.errors.each do |error|
    puts "#{error[:path]}: #{error[:message]}"
  end
end

Pre-Validation (Dry-Run)

Validate invoice data without generating PDF:

ruby
result = client.invoice
  .number('2026-001')
  .date('2026-01-15')
  .seller(name: 'Acme', country: 'DE')
  .buyer(name: 'Customer', country: 'IT')
  .add_item(description: 'Service', quantity: 1, unit_price: 100)
  .format('fatturapa')
  .validate  # Returns DryRunResult

if result.valid?
  puts "Valid! Would generate: #{result.format.format_used}"
else
  result.errors.each { |e| puts e }
end

Error Handling

ruby
begin
  result = client.invoice.generate
rescue Thelawin::QuotaExceededError
  puts 'Quota exceeded, upgrade your plan'
rescue Thelawin::NetworkError => e
  puts "Network error: #{e.message}"
rescue Thelawin::ApiError => e
  puts "API error #{e.status_code}: #{e.message}"
end

Format-Specific Examples

XRechnung (German B2G)

ruby
result = client.invoice
  .format('xrechnung')
  .leitweg_id('04011000-12345-67')
  .seller(
    name: 'Acme GmbH',
    vat_id: 'DE123456789',
    email: 'invoice@acme.de',  # Required
    street: 'Hauptstraße 1',
    city: 'Berlin',
    postal_code: '10115',
    country: 'DE'
  )
  # ... rest of invoice
  .generate

Peppol

ruby
result = client.invoice
  .format('peppol')
  .buyer_reference('PO-12345')
  .seller(
    name: 'Acme Ltd',
    vat_id: 'GB123456789',
    peppol_id: '0088:1234567890123',
    city: 'London', country: 'GB'
  )
  .buyer(
    name: 'Customer BV',
    peppol_id: '0106:NL123456789B01',
    city: 'Amsterdam', country: 'NL'
  )
  .generate

FatturaPA (Italy)

ruby
result = client.invoice
  .format('fatturapa')
  .tipo_documento('TD01')
  .seller(
    name: 'Acme S.r.l.',
    vat_id: 'IT12345678901',
    codice_fiscale: '12345678901',
    city: 'Milano', country: 'IT'
  )
  .buyer(
    name: 'Cliente S.p.A.',
    vat_id: 'IT98765432109',
    codice_destinatario: 'ABCDEFG',  # or pec: 'cliente@pec.it'
    city: 'Roma', country: 'IT'
  )
  .add_item(description: 'Consulenza', quantity: 10, unit_price: 100, vat_rate: 22.0)
  .generate

# FatturaPA returns XML only
result.save('./fattura.xml')

Rails Integration

ruby
# config/initializers/thelawin.rb
Thelawin.configure do |config|
  config.api_key = Rails.application.credentials.thelawin_api_key
  config.environment = Rails.env.production? ? :production : :preview
end

# In your controller/service
class InvoiceService
  def generate_invoice(order)
    Thelawin.client.invoice
      .number(order.invoice_number)
      .date(order.created_at.to_date)
      .seller(company_details)
      .buyer(customer_party(order.customer))
      .items(order.line_items.map { |li| line_item_attrs(li) })
      .generate
  end
end

Source Code

github.com/steviee/thelawin-clients/tree/main/ruby

ZUGFeRD 2.3 & Factur-X 1.0 compliant