0.0
No release in over a year
Ruby library for the FreeAgent v2 API
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
2026
 Dependencies

Runtime

~> 0.6.0
~> 2.11
 Project Readme

FreeAgentRB

FreeAgentRB is the easiest and most complete Ruby library for the FreeAgent v2 API.

Installation

Add this line to your application's Gemfile:

gem "freeagentrb"

Usage

Set Client Details

Firstly you'll need to set an Access Token, which would be created from OAuth. See this page for more info.

@client = FreeAgent::Client.new(access_token: "", sandbox: true)

API Versions

FreeAgent versions breaking changes by date. Without a version, each endpoint behaves as it did before versioning was introduced. To opt in to a newer version, pass api_version, which is sent as the X-Api-Version header. See the FreeAgent docs for more info.

@client = FreeAgent::Client.new(access_token: "", api_version: "2026-09-01")

Rate Limiting

The library automatically tracks rate limiting based on the FreeAgent API's Retry-After header when you receive a 429 (Too Many Requests) response. You can access rate limit information through the client's rate limiter:

# Initialize with optional logger
require "logger"
logger = Logger.new($stdout)
@client = FreeAgent::Client.new(access_token: "", sandbox: true, logger: logger)

# Check rate limit status
@client.rate_limiter.status
# => "Not rate limited" or "Rate limited. Retry in 60 seconds"

# Check if currently rate limited
@client.rate_limiter.rate_limited?
# => true/false

# Get seconds until rate limit resets
@client.rate_limiter.reset_in
# => 60 (seconds remaining)

# Manually wait if rate limited
@client.rate_limiter.wait_if_rate_limited

For testing rate limiting in the sandbox, enable the X-RateLimit-Test header:

# This artificially lowers sandbox API calls to 5 requests/minute
@client = FreeAgent::Client.new(
  access_token: "",
  sandbox: true,
  enable_rate_limit_test: true
)

OAuth

This library includes the ability to create, refresh and revoke OAuth tokens.

# Firstly, set the client details
@oauth = FreeAgent::OAuth.new(sandbox: true, client_id: "", client_secret: "")

# Generate an authorisation URL
# state can be nil
@oauth.authorise_url(redirect: "https://mysite.com/auth", state: "something")

# Create a Token from the authorisation code
@oauth.create(token: "abc123", redirect: "https://mysite.com/auth")

# Refresh a Token
@oauth.refresh(refresh_token: "abc123")

Resources

Methods that return a list give you a FreeAgent::Collection, which responds to each, count, first and last, along with total (from the X-Total-Count header) and pagination (from the Link header).

Any extra keyword arguments are passed through to the API as query parameters on list methods, or as attributes on create and update, so anything documented by FreeAgent that isn't named below can still be sent.

Attributes that reference another record are given as full URLs, e.g. contact: "https://api.freeagent.com/v2/contacts/1".

Company

@client.company.retrieve

# The business categories a company can choose from, as strings
@client.company.business_categories

# Upcoming tax deadlines and payments
@client.company.tax_timeline

Contacts

@client.contacts.list
@client.contacts.list(view: "active")
@client.contacts.retrieve(id: "12345")
@client.contacts.create first_name: "Dwight", last_name: "Schrute"
@client.contacts.update id: "12345", first_name: "Dwight Kurt"
@client.contacts.delete id: "12345"

Users

@client.users.me
@client.users.update_me first_name: "Dwight"

@client.users.list
@client.users.retrieve(id: "12345")
@client.users.create email: "dwight@example.com", first_name: "Dwight", last_name: "Schrute", role: "Director"
@client.users.update id: "12345", role: "Employee"
@client.users.delete id: "12345"

Bank Accounts

@client.bank_accounts.list
@client.bank_accounts.list(view: "paypal_accounts")
@client.bank_accounts.retrieve(id: "12345")
@client.bank_accounts.create type: "StandardBankAccount", name: "My Account", opening_balance: "10"
@client.bank_accounts.update id: "12345", name: "My Other Account"
@client.bank_accounts.delete id: "12345"

Bank Transactions

A bank account is required when listing.

@client.bank_transactions.list bank_account: "https://api.freeagent.com/v2/bank_accounts/1"
@client.bank_transactions.list bank_account: "...", view: "unexplained"
@client.bank_transactions.retrieve(id: "12345")
@client.bank_transactions.delete id: "12345"

# Upload a statement as an array of transactions
@client.bank_transactions.create bank_account: "...", statement: [
  { dated_on: "2026-08-15", amount: "100.0", description: "Payment" }
]

# Or upload a statement file (OFX, QIF, CSV)
@client.bank_transactions.upload bank_account: "...", statement: "statement.csv"

Bank Transaction Explanations

@client.bank_transaction_explanations.list bank_account: "https://api.freeagent.com/v2/bank_accounts/1"
@client.bank_transaction_explanations.retrieve(id: "12345")
@client.bank_transaction_explanations.create bank_transaction: "...", dated_on: "2026-08-15", gross_value: "-100.0", category: "..."
@client.bank_transaction_explanations.update id: "12345", description: "Hosting"
@client.bank_transaction_explanations.delete id: "12345"

# File attachments, up to 50 per explanation. These endpoints always send
# X-Api-Version 2026-09-01 or later.
@client.bank_transaction_explanations.attachments id: "12345"
@client.bank_transaction_explanations.add_attachments id: "12345", attachments: [
  { data: Base64.strict_encode64(File.binread("receipt.png")), file_name: "receipt.png", content_type: "image/png" }
]
@client.bank_transaction_explanations.update_attachments id: "12345", attachments: [
  { url: "https://api.freeagent.com/v2/attachments/1", description: "Printer receipt" },
  { url: "https://api.freeagent.com/v2/attachments/2", _destroy: true }
]
@client.bank_transaction_explanations.delete_attachments id: "12345"

From 1 December 2026, explanations return an attachments array instead of a single attachment, and attachments can only be changed through the endpoints above.

Projects

@client.projects.list
@client.projects.list(view: "active")
@client.projects.list_for_contact contact: "https://api.freeagent.com/v2/contacts/1"
@client.projects.retrieve(id: "12345")
@client.projects.create contact: "...", name: "My Project", status: "Active", currency: "GBP", budget_units: "Hours"
@client.projects.update id: "12345", name: "Renamed Project"
@client.projects.delete id: "12345"

Tasks

@client.tasks.list
@client.tasks.list_for_project project: "https://api.freeagent.com/v2/projects/1"
@client.tasks.retrieve(id: "12345")
@client.tasks.create project: "...", name: "My Task", currency: "GBP", is_billable: true, status: "Active"
@client.tasks.update id: "12345", name: "Renamed Task"
@client.tasks.delete id: "12345"

Timeslips

@client.timeslips.list
@client.timeslips.list(from_date: "2026-01-01", to_date: "2026-01-31")
@client.timeslips.list_for_user user: "https://api.freeagent.com/v2/users/1"
@client.timeslips.list_for_task task: "https://api.freeagent.com/v2/tasks/1"
@client.timeslips.list_for_project project: "https://api.freeagent.com/v2/projects/1"
@client.timeslips.retrieve(id: "12345")
@client.timeslips.create task: "...", user: "...", project: "...", dated_on: "2026-08-15", hours: "7.5"

# Create several at once
@client.timeslips.create_many timeslips: [
  { task: "...", user: "...", project: "...", dated_on: "2026-08-15", hours: "7.5" },
  { task: "...", user: "...", project: "...", dated_on: "2026-08-16", hours: "6.0" }
]

@client.timeslips.update id: "12345", hours: "8.0"
@client.timeslips.delete id: "12345"

@client.timeslips.start_timer id: "12345"
@client.timeslips.stop_timer id: "12345"

Invoices

@client.invoices.list
@client.invoices.list(view: "open", nested_invoice_items: true)
@client.invoices.list_for_contact contact: "https://api.freeagent.com/v2/contacts/1"
@client.invoices.list_for_project project: "https://api.freeagent.com/v2/projects/1"
@client.invoices.retrieve(id: "12345")

@client.invoices.create contact: "...", dated_on: "2026-08-15", payment_terms_in_days: 30, invoice_items: [
  { description: "Consultancy", item_type: "Hours", price: "100.0", quantity: "10.0" }
]

@client.invoices.update id: "12345", reference: "002"
@client.invoices.delete id: "12345"
@client.invoices.duplicate id: "12345"

# Returns a Base64-encoded PDF
@client.invoices.retrieve_pdf id: "12345"

@client.invoices.email id: "12345", to: "someone@example.com", subject: "Your invoice"

@client.invoices.mark_as_sent id: "12345"
@client.invoices.mark_as_scheduled id: "12345"
@client.invoices.mark_as_draft id: "12345"
@client.invoices.mark_as_cancelled id: "12345"
@client.invoices.convert_to_credit_note id: "12345"
@client.invoices.direct_debit id: "12345"

# Recent activity across all invoices
@client.invoices.timeline

# The text added to the bottom of every new invoice
@client.invoices.default_additional_text
@client.invoices.update_default_additional_text "Thanks for your business"
@client.invoices.delete_default_additional_text

net_value, total_value, paid_value, due_value and sales_tax_value are returned as floats rather than the strings the API sends. The same applies to credit notes, bills, estimates and expenses.

Estimates

@client.estimates.list
@client.estimates.list_for_contact contact: "https://api.freeagent.com/v2/contacts/1"
@client.estimates.list_for_project project: "https://api.freeagent.com/v2/projects/1"
@client.estimates.list_for_invoice invoice: "https://api.freeagent.com/v2/invoices/1"
@client.estimates.retrieve(id: "12345")
@client.estimates.create contact: "...", dated_on: "2026-08-15", currency: "GBP", reference: "001"
@client.estimates.update id: "12345", reference: "002"
@client.estimates.delete id: "12345"

# The copy is a draft dated today, with the next reference
@client.estimates.duplicate id: "12345"

# Returns a Base64-encoded PDF
@client.estimates.retrieve_pdf id: "12345"

@client.estimates.email id: "12345", to: "someone@example.com"

@client.estimates.mark_as_sent id: "12345"
@client.estimates.mark_as_draft id: "12345"
@client.estimates.mark_as_approved id: "12345"
@client.estimates.mark_as_rejected id: "12345"
@client.estimates.convert_to_invoice id: "12345"

# The text added to the bottom of every new estimate
@client.estimates.default_additional_text
@client.estimates.update_default_additional_text "Please respond within 21 days"
@client.estimates.delete_default_additional_text

Estimate Items

@client.estimate_items.create estimate: "https://api.freeagent.com/v2/estimates/1",
  item_type: "Hours", quantity: "10.0", price: "100.0", description: "Consultancy"
@client.estimate_items.update id: "12345", description: "Updated"
@client.estimate_items.delete id: "12345"

Recurring Invoices

Read-only.

@client.recurring_invoices.list
@client.recurring_invoices.list_for_contact contact: "https://api.freeagent.com/v2/contacts/1"
@client.recurring_invoices.list_for_project project: "https://api.freeagent.com/v2/projects/1"
@client.recurring_invoices.retrieve(id: "12345")

Price List Items

@client.price_list_items.list
@client.price_list_items.retrieve(id: "12345")
@client.price_list_items.create code: "CONS", quantity: "1.0", item_type: "Hours",
  description: "Consultancy", price: "100.0"
@client.price_list_items.update id: "12345", price: "120.0"
@client.price_list_items.delete id: "12345"

Stock Items

Read-only.

@client.stock_items.list
@client.stock_items.retrieve(id: "12345")

Credit Notes

@client.credit_notes.list
@client.credit_notes.list_for_contact contact: "https://api.freeagent.com/v2/contacts/1"
@client.credit_notes.list_for_project project: "https://api.freeagent.com/v2/projects/1"
@client.credit_notes.retrieve(id: "12345")
@client.credit_notes.create contact: "...", dated_on: "2026-08-15", payment_terms_in_days: 30
@client.credit_notes.update id: "12345", reference: "002"
@client.credit_notes.delete id: "12345"

# Returns a Base64-encoded PDF
@client.credit_notes.retrieve_pdf id: "12345"

@client.credit_notes.email id: "12345", to: "someone@example.com"

@client.credit_notes.mark_as_sent id: "12345"
@client.credit_notes.mark_as_draft id: "12345"

Credit Note Reconciliations

@client.credit_note_reconciliations.list
@client.credit_note_reconciliations.retrieve(id: "12345")
@client.credit_note_reconciliations.create credit_note: "https://api.freeagent.com/v2/credit_notes/1",
  invoice: "https://api.freeagent.com/v2/invoices/2", gross_value: "50.0"
@client.credit_note_reconciliations.update id: "12345", gross_value: "75.0"
@client.credit_note_reconciliations.delete id: "12345"

Bills

@client.bills.list
@client.bills.list_for_contact contact: "https://api.freeagent.com/v2/contacts/1"
@client.bills.list_for_project project: "https://api.freeagent.com/v2/projects/1"
@client.bills.retrieve(id: "12345")

@client.bills.create contact: "...", dated_on: "2026-08-15", due_on: "2026-09-15", reference: "Bill-001", bill_items: [
  { description: "Stationery", category: "https://api.freeagent.com/v2/categories/285", total_value: "100.0" }
]

@client.bills.update id: "12345", reference: "Bill-002"
@client.bills.delete id: "12345"

Expenses

@client.expenses.list
@client.expenses.list(from_date: "2026-01-01", to_date: "2026-01-31")
@client.expenses.list_for_user user: "https://api.freeagent.com/v2/users/1"
@client.expenses.list_for_project project: "https://api.freeagent.com/v2/projects/1"
@client.expenses.retrieve(id: "12345")

@client.expenses.create user: "...", category: "https://api.freeagent.com/v2/categories/285",
  dated_on: "2026-08-15", gross_value: "-12.0", description: "Train fare"

# Mileage expenses use the Mileage category
@client.expenses.create user: "...", category: "Mileage", dated_on: "2026-08-15",
  mileage: 100, vehicle_type: "Car"

@client.expenses.update id: "12345", description: "Updated"
@client.expenses.delete id: "12345"

@client.expenses.mileage_settings

Journal Sets

@client.journal_sets.list
@client.journal_sets.opening_balances
@client.journal_sets.retrieve(id: "12345")

@client.journal_sets.create dated_on: "2026-08-15", description: "Adjustment", journal_entries: [
  { category: "https://api.freeagent.com/v2/categories/285", debit_value: "100.0", description: "Stationery" },
  { category: "https://api.freeagent.com/v2/categories/750", debit_value: "-100.0", description: "Stationery" }
]

@client.journal_sets.update id: "12345", description: "Updated"
@client.journal_sets.delete id: "12345"

Transactions

The general ledger, as opposed to bank transactions. Read-only, and date ranges must span 12 months or less.

@client.transactions.list
@client.transactions.list from_date: "2026-01-01", to_date: "2026-06-30"
@client.transactions.retrieve(id: "12345")

Notes

The parent record goes in the query string rather than the body, so pass either contact: or project:.

@client.notes.list_for_contact contact: "https://api.freeagent.com/v2/contacts/1"
@client.notes.list_for_project project: "https://api.freeagent.com/v2/projects/1"
@client.notes.retrieve(id: "12345")
@client.notes.create note: "Called them", contact: "https://api.freeagent.com/v2/contacts/1"
@client.notes.update id: "12345", note: "Updated"
@client.notes.delete id: "12345"

Bank Feeds

Read-only.

@client.bank_feeds.list
@client.bank_feeds.retrieve(id: "12345")

Email Addresses

Verified sender addresses, returned as a plain Array of strings.

@client.email_addresses.list
# => ["me@example.com"]

Account Locks

# The locks, plus earliest_lock_date and latest_lock_date
@client.account_locks.retrieve
@client.account_locks.update locked_to_date: "2026-03-31"
@client.account_locks.delete

Categories

Unlike other resources this returns a plain Array, with each category tagged with the category_type it was listed under.

@client.categories.list
# => [#<FreeAgent::Category description="Accommodation and Travel",
#      nominal_code="250", category_type="admin_expenses_categories">, ...]

# Include bank account, user and other sub accounts
@client.categories.list sub_accounts: true

@client.categories.retrieve nominal_code: "001"

# category_group is one of income, cost_of_sales, admin_expenses,
# current_assets, liabilities or equities
@client.categories.create description: "Consulting", nominal_code: "047", category_group: "income"
@client.categories.update nominal_code: "047", description: "Consultancy"
@client.categories.delete nominal_code: "047"

Capital Assets

Read-only.

@client.capital_assets.list
@client.capital_assets.list include_history: true
@client.capital_assets.retrieve(id: "12345")

Capital Asset Types

@client.capital_asset_types.list
@client.capital_asset_types.retrieve(id: "12345")
@client.capital_asset_types.create name: "Computer Equipment"
@client.capital_asset_types.update id: "12345", name: "Vehicles"
@client.capital_asset_types.delete id: "12345"

Hire Purchases

Read-only.

@client.hire_purchases.list
@client.hire_purchases.retrieve(id: "12345")

Reports

All read-only.

# Returns a FreeAgent::BalanceSheet. Defaults to today
@client.balance_sheet.retrieve
@client.balance_sheet.retrieve as_at_date: "2026-03-31"
@client.balance_sheet.opening_balances

# Returns a FreeAgent::ProfitAndLoss. Defaults to the current accounting year to date
@client.profit_and_loss.summary from_date: "2026-01-01", to_date: "2026-03-31"
@client.profit_and_loss.summary accounting_period: "2025/26"

# Return a collection of FreeAgent::TrialBalanceItem
@client.trial_balance.summary
@client.trial_balance.summary to_date: "2026-03-31"
@client.trial_balance.opening_balances

# Returns a FreeAgent::Cashflow
@client.cashflow.retrieve from_date: "2026-01-01", to_date: "2026-03-31"

Final Accounts Reports

Reports are identified by the date their accounting period ends, not by an id.

@client.final_accounts_reports.list
@client.final_accounts_reports.retrieve period_ends_on: "2026-03-31"
@client.final_accounts_reports.mark_as_filed period_ends_on: "2026-03-31"
@client.final_accounts_reports.mark_as_unfiled period_ends_on: "2026-03-31"

VAT Returns

Identified by the date their period ends. Each return includes a breakdown of the boxes on the return.

@client.vat_returns.list
@client.vat_returns.retrieve period_ends_on: "2026-03-31"
@client.vat_returns.mark_as_filed period_ends_on: "2026-03-31"
@client.vat_returns.mark_as_unfiled period_ends_on: "2026-03-31"

# Payments are identified by their due_on date
@client.vat_returns.mark_payment_as_paid period_ends_on: "2026-03-31", payment_date: "2026-05-07"
@client.vat_returns.mark_payment_as_unpaid period_ends_on: "2026-03-31", payment_date: "2026-05-07"

Corporation Tax Returns

@client.corporation_tax_returns.list
@client.corporation_tax_returns.retrieve period_ends_on: "2026-03-31"
@client.corporation_tax_returns.mark_as_filed period_ends_on: "2026-03-31"
@client.corporation_tax_returns.mark_as_unfiled period_ends_on: "2026-03-31"
@client.corporation_tax_returns.mark_as_paid period_ends_on: "2026-03-31"
@client.corporation_tax_returns.mark_as_unpaid period_ends_on: "2026-03-31"

Self Assessment Returns

Nested under a user, and identified by the date the period ends. FreeAgent's docs call these Income Tax Returns.

@client.self_assessment_returns.list user_id: "12345"
@client.self_assessment_returns.retrieve user_id: "12345", period_ends_on: "2026-04-05"
@client.self_assessment_returns.mark_as_filed user_id: "12345", period_ends_on: "2026-04-05"
@client.self_assessment_returns.mark_as_unfiled user_id: "12345", period_ends_on: "2026-04-05"

# Payments are identified by their due_on date
@client.self_assessment_returns.mark_payment_as_paid user_id: "12345", period_ends_on: "2026-04-05", payment_date: "2027-01-31"
@client.self_assessment_returns.mark_payment_as_unpaid user_id: "12345", period_ends_on: "2026-04-05", payment_date: "2027-01-31"

Sales Tax Periods

US and Universal companies only.

@client.sales_tax_periods.list
@client.sales_tax_periods.retrieve(id: "12345")
@client.sales_tax_periods.create sales_tax_name: "Sales Tax", sales_tax_registration_status: "Registered",
  sales_tax_rate_1: "8.0", sales_tax_is_value_added: false, effective_date: "2026-01-01"
@client.sales_tax_periods.update id: "12345", sales_tax_rate_1: "8.5"
@client.sales_tax_periods.delete id: "12345"

CIS Bands

@client.cis_bands.list

CIS Settings

@client.cis_settings.retrieve
@client.cis_settings.update contractor_details: { reporting_starts_on: "2024-05-06", paye_ni_period: "Monthly" }

# Deregister as a contractor
@client.cis_settings.update contractor_details: nil

Payroll

Organised by tax year, then by period within that year. Read-only apart from the payment transitions.

@client.payroll.list year: 2026

# A period, with its payslips
@client.payroll.retrieve year: 2026, period: 1

# PAYE payments due to HMRC, identified by their due_on date
@client.payroll.payments year: 2026
@client.payroll.mark_payment_as_paid year: 2026, payment_date: "2025-05-22"
@client.payroll.mark_payment_as_unpaid year: 2026, payment_date: "2025-05-22"

Payroll Profiles

@client.payroll_profiles.list year: 2026
@client.payroll_profiles.list year: 2026, user: "https://api.freeagent.com/v2/users/1"

Properties

UkUnincorporatedLandlord companies only.

@client.properties.list
@client.properties.retrieve(id: "12345")
@client.properties.create name: "12 High Street"
@client.properties.update id: "12345", name: "14 High Street"
@client.properties.delete id: "12345"

Attachments

@client.attachments.retrieve(id: "12345")
@client.attachments.delete id: "12345"

Accountancy Practice API

If you've authenticated as an accountancy practice, you can list the clients your practice has access to and make requests on their behalf. See the FreeAgent docs for more info.

These endpoints are only available to practice-level access tokens. If each of your users connected their own FreeAgent account via OAuth, you don't need any of this — their token is already scoped to their company.

Practice details

@client.practice.retrieve
# => #<FreeAgent::Practice name="My Practice", subdomain="mypracticesubdomain">

Clients

@client.clients.list
@client.clients.list(view: "active")
@client.clients.list(sort: "-created_at")
@client.clients.list(updated_since: "2026-01-01T00:00:00Z")

# Fetch only the id, name and subdomain, up to 500 per page
@client.clients.list(minimal_data: true, per_page: 500)

view accepts all, active, inactive, closed, practice, linked, copilot and demo. sort accepts created_at and updated_at, prefixed with - for descending order. from_date and to_date are also supported.

Account managers

@client.account_managers.list
@client.account_managers.retrieve(id: "123")

# The account manager the access token belongs to
@client.account_managers.me

Making requests on behalf of a client

Requests are scoped to one of your clients by sending its subdomain in the X-Subdomain header. Set it when building the client:

@client = FreeAgent::Client.new(access_token: "", subdomain: "testcompany")

# Every request is now made against that client's account
@client.invoices.list
@client.contacts.list

Or derive a scoped client from your practice-level one, which is handy when iterating over several clients:

@practice = FreeAgent::Client.new(access_token: "")

@practice.clients.list(minimal_data: true).each do |client|
  @practice.on_behalf_of(client.subdomain).invoices.list
end

on_behalf_of returns a new client and leaves the original untouched, so the practice-level client can still be used for clients, account_managers and practice.

Development

Tests stub every request with WebMock, using the example responses from the FreeAgent API docs saved in test/fixtures. To refresh them when the docs change:

bin/fixtures                  # every page
bin/fixtures contacts bills   # just these pages

Then run the tests with bundle exec rake test.