Moloni

A modern Ruby wrapper for the Moloni API. Works with Ruby 3.2+ and Rails 8.

Installation

Add this line to your application's Gemfile:

gem 'moloni'

And then execute:

$ bundle

Or install it yourself as:

$ gem install moloni

Setup

In a Rails app

Create config/initializers/moloni.rb:

Moloni.configure do |config|
  config.developer_id = ENV.fetch('MOLONI_DEVELOPER_ID')
  config.redirect_uri = ENV.fetch('MOLONI_REDIRECT_URI')
  config.client_secret = ENV.fetch('MOLONI_CLIENT_SECRET')
  config.access_token = ENV.fetch('MOLONI_ACCESS_TOKEN')
  config.refresh_token = ENV.fetch('MOLONI_REFRESH_TOKEN')
  config.company_id = ENV.fetch('MOLONI_COMPANY_ID', 0)
end

Environment Variables

Variable Purpose
MOLONI_DEVELOPER_ID OAuth client_id from Moloni developer area
MOLONI_REDIRECT_URI OAuth callback URL
MOLONI_CLIENT_SECRET OAuth client_secret
MOLONI_ACCESS_TOKEN Short-lived API token (1h)
MOLONI_REFRESH_TOKEN Long-lived refresh token (14 days)
MOLONI_COMPANY_ID Default company for API calls

Authentication

The gem supports OAuth 2.0. Use the built-in CLI tool to obtain tokens:

bin/auth

This starts a local callback server and opens the browser for Moloni authorization. After approval, tokens are printed to the console.

Token auto-refresh: The gem automatically refreshes expired access_tokens using the refresh_token before each API call (with a 5-minute safety margin). If the refresh token also expires, a Moloni::TokenExpiredError is raised.

Usage

Basic API calls

All API calls use POST under the hood, including reads. The gem automatically injects company_id, access_token, json=true, and human_errors=true.

# List all products
products = Moloni::Product.getAll

# Find one product
product = Moloni::Product.getOne(product_id: 123)

# Search by EAN (or any future method)
product = Moloni::Product.getByEAN(ean: '5601234567890')

# Snake_case also works
product = Moloni::Product.get_by_ean(ean: '5601234567890')

Dynamic dispatch

Any method name not explicitly defined on a model is automatically translated to a Moloni API endpoint. This means the gem supports new Moloni methods as soon as they are released, without code changes.

# These all work immediately, even if not explicitly defined:
Moloni::Product.getByReference(reference: 'ABC-123')
Moloni::Product.get_by_reference(reference: 'ABC-123')
Moloni::Invoice.getByNumber(number: 'FT 2025/001')

Customers

# Find by VAT, email, or customer number
customer = Moloni::Customer.getByVat(vat: '123456789')
customer = Moloni::Customer.find_by_vat(vat: '123456789')

# Create a customer
customer = Moloni::Customer.insert(
  name: 'Acme Corp',
  vat: '123456789',
  number: 'C001',
  language_id: 1,
  address: 'Main St 1',
  city: 'Lisbon',
  country_id: 1,
  maturity_date_id: Moloni::MaturityDate.pronto_pagamento,
  payment_method_id: Moloni::PaymentMethod.numerario
)

Invoices and Documents

# Create an invoice
invoice = Moloni::Invoice.insert(
  document_set_id: 332_429,
  date: Date.today,
  expiration_date: Date.today + 30,
  customer_id: 38_096_359,
  products: [
    {
      product_id: 70_241_498,
      name: 'Consulting',
      qty: 1,
      price: 100.00,
      taxes: [{ tax_id: Moloni::Tax.iva_normal_id }]
    }
  ]
)

# Get PDF link
pdf_url = Moloni::Document.getPDFLink(document_id: invoice[:document_id])

Taxes and Settings

# Get hardcoded tax IDs
normal_iva = Moloni::Tax.iva_normal_id      # 2072734
intermedio = Moloni::Tax.iva_intermedio_id    # 2072748
reduzido   = Moloni::Tax.iva_reduzido_id      # 2072741

# Get full tax objects
normal_tax = Moloni::Tax.iva_normal

Error Handling

The gem raises specific exceptions for different failure modes:

begin
  Moloni::Product.getOne(product_id: 999_999)
rescue Moloni::APIKeyError => e
  # HTTP 401 — access token invalid or expired
rescue Moloni::APIError => e
  # Validation errors (missing fields, invalid format, etc.)
  e.errors.each do |err|
    puts "#{err[:field]}: #{err[:message]}"
  end
rescue Moloni::TokenExpiredError => e
  # Both access and refresh tokens expired — re-authentication needed
end

Breaking Changes (0.4 → 0.5)

  • Ruby requirement: Now requires Ruby 3.2 or later.
  • HTTP method: All API calls now use POST (previously some reads used GET).
  • Removed methods: Redundant explicit methods (all, count, find, create) were removed from models where they offered no custom logic. Use dynamic dispatch (getAll, getOne, insert) instead.
  • Auth URL fix: Moloni::Auth.auth_url now correctly points to https://www.moloni.pt/ac/root/oauth/.
  • Multi-json removed: Replaced multi_json with standard library JSON.

Development

After checking out the repo, run bin/setup to install dependencies. Then, run rake spec to run the tests. You can also run bin/console for an interactive prompt.

To install this gem onto your local machine, run bundle exec rake install. To release a new version, update the version number in version.rb, and then run bundle exec rake release.

License

The gem is available as open source under the terms of the MIT License.