billrb

A lightweight Ruby client for the BILL (bill.com) v3 API, focused on the Accounts Receivable modules: customers (including bank accounts and charge authorization), invoices, recurring invoices, credit memos, and received payments.

Installation

Add this line to your application's Gemfile:

gem "billrb"

And then execute:

$ bundle install

Or install it yourself as:

$ gem install billrb

Configuration

Billrb.configure do |config|
  config.username        = ENV["BILL_USERNAME"]
  config.password        = ENV["BILL_PASSWORD"]
  config.organization_id = ENV["BILL_ORGANIZATION_ID"]
  config.dev_key         = ENV["BILL_DEV_KEY"]
  config.environment     = :production # defaults to :sandbox
end

The client logs in lazily on the first request and transparently re-authenticates when the API session expires (BILL sessions expire after 35 minutes of inactivity).

All configuration options

Option Default Description
username, password, organization_id, dev_key BILL credentials (required)
environment :sandbox :sandbox or :production
timeout, open_timeout 30, 5 Faraday request/connect timeouts (seconds)
logger nil A Logger; request lines are logged with headers/bodies excluded
max_retries 2 Retry attempts for transient failures (see below)
retry_backoff 0.5 Base seconds for exponential backoff
adapter Faraday default Faraday adapter, e.g. :net_http_persistent
faraday nil A proc given the Faraday builder to add custom middleware

Resilience

Transient failures are retried automatically with exponential backoff, honoring a Retry-After header when present. Rate-limit responses (429) are retried for every method since the request was rejected before processing; 5xx and network errors are retried only for idempotent methods (GET/HEAD/PUT/DELETE), so a POST create is never silently repeated. After max_retries the mapped error (e.g. RateLimitError, ServerError) is raised.

Thread safety

A client may be shared across threads. Sign-in is mutex-guarded, so concurrent requests trigger at most one login, and an expired session is refreshed only once even when many requests race on it.

Custom Faraday middleware

Billrb.configure do |config|
  config.adapter = :net_http_persistent
  config.faraday = ->(builder) { builder.use MyInstrumentationMiddleware }
end

For multi-tenant applications, build clients explicitly instead of using the global configuration:

client = Billrb::Client.new(
  username: "...", password: "...", organization_id: "...", dev_key: "..."
)
Billrb::Customer.list({ max: 50 }, client: client)
Billrb::Customer.retrieve("customer_id", client: client)

Usage

# List with filters and pagination (BILL's native filter syntax)
page = Billrb::Customer.list(max: 50, filters: "archived:eq:false")
page.each { |customer| puts customer.name }
page = page.fetch_next_page if page.next_page?

# Or iterate across all pages
Billrb::Invoice.list(max: 100).auto_paging_each do |invoice|
  puts "#{invoice.invoice_number}: #{invoice.due_date}"
end

# CRUD — params are written snake_case and converted to the API's camelCase
customer = Billrb::Customer.create(name: "Acme Inc.", email: "billing@acme.test")
customer = Billrb::Customer.update(customer.id, name: "Acme Incorporated")
Billrb::Customer.archive(customer.id)

invoice = Billrb::Invoice.retrieve("inv_id")
invoice.due_date              # reads "dueDate" from the API response
invoice.billing_address.city  # nested objects are wrapped too
invoice.line_items.first.amount
invoice.to_h                  # raw API attributes (nested values stay hashes)

Invoicing actions

# Email the invoice (PDF attached) to the customer
Billrb::Invoice.send_email(invoice.id, recipient: { to: ["billing@acme.test"] })

# Or get a payment link to deliver yourself
link = Billrb::Invoice.payment_link(invoice.id, customer_id: customer.id, email: "billing@acme.test")
link.payment_link

# Record a payment received outside BILL (cash, check, ...)
Billrb::Invoice.record_payment(
  amount: 50.0, payment_date: "2026-06-12", payment_type: "CASH",
  invoices: [{ invoice_id: invoice.id, amount: 50.0 }]
)

# Recurring invoices and credit memos follow the same CRUD shape
Billrb::RecurringInvoice.create(customer_id: customer.id, ...)
Billrb::CreditMemo.create(customer_id: customer.id, ...)

Charging customers

Charging requires a one-time authorization and a customer bank account:

Billrb::Customer.authorize_charge(customer.id)
Billrb::CustomerBankAccount.create(customer.id, routing_number: "...", account_number: "...")

payment = Billrb::ReceivablePayment.charge(
  customer_id: customer.id,
  invoices: [{ invoice_id: invoice.id, amount: 100.0 }]
)

# Received payments are queryable like any other resource
Billrb::ReceivablePayment.list(max: 50)
Billrb::ReceivablePayment.retrieve(payment.id)

Errors

All errors inherit from Billrb::Error:

Error Raised on
Billrb::ConfigurationError missing/invalid configuration
Billrb::ConnectionError network failures and timeouts
Billrb::AuthenticationError 401 (after one automatic re-login attempt)
Billrb::BadRequestError / ForbiddenError / NotFoundError 400 / 403 / 404
Billrb::RateLimitError 429
Billrb::ServerError 5xx

API errors expose error.status and error.body (the parsed response) for access to BILL error codes.

Architecture & extending

The gem is intentionally small and layered so new BILL modules can be added without touching existing code:

  • Billrb::Client — transport only: session auth, JSON, error mapping. Knows nothing about resources.
  • Billrb::Resource — wraps API JSON, exposing camelCase attributes as snake_case readers.
  • Billrb::Operations — CRUD mixins (List, Retrieve, Create, Update, Replace, Archive).

A new resource is one file declaring its path and supported operations:

module Billrb
  class CreditMemo < Resource
    self.resource_path = "/credit-memos"

    extend Operations::List
    extend Operations::Retrieve
    extend Operations::Create
  end
end

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 that will allow you to experiment.

Linting & formatting

Formatting is handled by rufo and linting by RuboCop; rufo owns formatting, so RuboCop's layout cops are disabled to keep the two from fighting. They are wired up as pre-commit hooks:

pre-commit run --all-files   # run on the whole repo
pre-commit install           # optional: run automatically on every commit

You can also run them directly: bundle exec rufo . and bundle exec rubocop.

Integration specs

spec/integration runs end-to-end against the real BILL sandbox. It needs all four credentials; copy the template and fill it in:

cp .env.example .env   # .env is gitignored
bundle exec rspec spec/integration

Without the credentials these specs are skipped automatically, so rake spec stays green for everyone else. In CI they run when the matching repository secrets are configured.

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, which will create a git tag for the version, push git commits and tags, and push the .gem file to rubygems.org.

Contributing

Bug reports and pull requests are welcome on GitHub at https://github.com/TECMANIC/billrb. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the code of conduct.

License

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

Code of Conduct

Everyone interacting in the Billrb project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the code of conduct.