How to Integrate with the FreshBooks API

Learn how to integrate with the FreshBooks API, including OAuth 2.0 authentication, account and business IDs, key endpoints, pagination, webhooks, and common integration pitfalls.

Kateryna PoryvayKateryna Poryvay

Kateryna Poryvay

13 min read
How to Integrate with the FreshBooks API

FreshBooks is cloud-based accounting software popular with freelancers and small businesses. The FreshBooks API gives you programmatic access to invoices, clients, expenses, time tracking, and reports through a REST interface.

The Apideck FreshBooks connector handles authentication, pagination, and data normalization if you want to skip the manual work. It also lets you support 25+ accounting platforms with a single integration.

This guide covers direct FreshBooks integration: authentication, the identity model, endpoints, pagination, webhooks, and the gotchas that trip people up.

What the FreshBooks API is

The FreshBooks API is JSON-based REST. You can create invoices, manage clients, track expenses, log time, and pull reports.

The API uses standard HTTP methods (GET, POST, PUT, DELETE) with JSON payloads, OAuth 2.0 for authentication, scope-based permissions, and webhooks for real-time notifications.

Base URL: https://api.freshbooks.com

Why Integrate with FreshBooks

SaaS companies integrate with FreshBooks to connect their platform to their customers' accounting workflows. Instead of asking users to export and import data manually, an integration syncs it automatically. For more on accounting integration patterns, see our guide on accounting integration.

Say you have a project management tool that tracks billable hours. Without integration, users manually create invoices in FreshBooks based on their time entries. With integration, your tool generates draft invoices with the right client, line items, and rates.

Other use cases: payment processors marking invoices as paid when payments clear, CRM systems syncing client data bidirectionally, e-commerce platforms creating invoices on orders, expense tools pushing approved expenses into FreshBooks.

Getting Started

You need a FreshBooks account and a registered application before writing any code.

Create a Developer Account

Sign up at freshbooks.com if you don't have an account. Trial accounts work for development.

Register Your Application

Go to the FreshBooks developer page and create a new application. You'll provide an application name (must be unique across all FreshBooks apps, shows on the authorization screen), a redirect URI (receives the authorization code after users grant access, must be HTTPS in production), and the scopes your app needs.

FreshBooks generates a client ID and client secret after you save. Store the secret securely.

Authentication

FreshBooks uses OAuth 2.0. The flow:

  1. User visits your authorization URL, gets redirected to FreshBooks
  2. User logs in and grants permission
  3. FreshBooks redirects back with an authorization code
  4. Your server exchanges the code for access and refresh tokens

Authorization URL

Redirect users here to start the OAuth flow:

https://auth.freshbooks.com/oauth/authorize/
  ?response_type=code
  &client_id=YOUR_CLIENT_ID
  &redirect_uri=YOUR_REDIRECT_URI
  &scope=user:profile:read user:clients:read user:invoices:read

The scope parameter defines what permissions you're requesting. Request only what you need.

Token Exchange

After the user grants access, FreshBooks redirects to your redirect URI with a code parameter. Exchange it:

curl -X POST 'https://api.freshbooks.com/auth/oauth/token' \
  -H 'Content-Type: application/json' \
  -d '{
    "grant_type": "authorization_code",
    "client_id": "YOUR_CLIENT_ID",
    "client_secret": "YOUR_CLIENT_SECRET",
    "code": "AUTHORIZATION_CODE",
    "redirect_uri": "YOUR_REDIRECT_URI"
  }'

Response:

{
  "access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9...",
  "token_type": "Bearer",
  "expires_in": 43200,
  "refresh_token": "abc123...",
  "scope": "user:profile:read user:clients:read user:invoices:read",
  "created_at": 1234567890
}

Token Lifetimes

Access tokens expire after 12 hours. Refresh tokens never expire but are single-use. Every refresh gives you a new access token and a new refresh token. Store the new refresh token immediately because the old one is now invalid.

curl -X POST 'https://api.freshbooks.com/auth/oauth/token' \
  -H 'Content-Type: application/json' \
  -d '{
    "grant_type": "refresh_token",
    "client_id": "YOUR_CLIENT_ID",
    "client_secret": "YOUR_CLIENT_SECRET",
    "refresh_token": "YOUR_REFRESH_TOKEN"
  }'

Making Authenticated Requests

Include the access token in the Authorization header:

curl -X GET 'https://api.freshbooks.com/auth/api/v1/users/me' \
  -H 'Authorization: Bearer YOUR_ACCESS_TOKEN' \
  -H 'Content-Type: application/json'

The Identity Model

This is where FreshBooks gets tricky. The API uses two different identifiers: accountId and businessId. You need to know when to use each.

Accounts vs Businesses

FreshBooks evolved from an account-based architecture to a business-based one. The result is a split:

accountId is for /accounting endpoints (invoices, clients, expenses, payments). businessId is for /timetracking and /projects endpoints.

These are different values. You cannot interchange them. Using the wrong one causes silent failures or incorrect routing.

Getting Your Identifiers

Call the /me endpoint to retrieve the user's identities:

curl -X GET 'https://api.freshbooks.com/auth/api/v1/users/me' \
  -H 'Authorization: Bearer YOUR_ACCESS_TOKEN'

The response includes a business_memberships array:

{
  "response": {
    "business_memberships": [
      {
        "id": 168372,
        "role": "owner",
        "business": {
          "id": 77128,
          "name": "Acme Corp",
          "account_id": "zDmNq"
        }
      }
    ]
  }
}

In this example, businessId is 77128 and accountId is zDmNq.

A single user can have multiple business memberships with different roles (owner, admin, manager, employee, contractor, client). Your integration needs to handle this.

Endpoint Patterns

Accounting endpoints use accountId:

/accounting/account/{accountId}/invoices/invoices
/accounting/account/{accountId}/users/clients
/accounting/account/{accountId}/expenses/expenses

Time tracking and project endpoints use businessId:

/timetracking/business/{businessId}/time_entries
/projects/business/{businessId}/projects

Key Endpoints

FreshBooks organizes its API around business entities.

Clients

MethodEndpointDescription
GET/accounting/account/{accountId}/users/clientsList all clients
POST/accounting/account/{accountId}/users/clientsCreate a client
GET/accounting/account/{accountId}/users/clients/{clientId}Get a single client
PUT/accounting/account/{accountId}/users/clients/{clientId}Update a client

To delete a client, send a PUT request with vis_state set to 1:

curl -X PUT 'https://api.freshbooks.com/accounting/account/zDmNq/users/clients/12345' \
  -H 'Authorization: Bearer YOUR_ACCESS_TOKEN' \
  -H 'Content-Type: application/json' \
  -d '{
    "client": {
      "vis_state": 1
    }
  }'

Creating a client:

curl -X POST 'https://api.freshbooks.com/accounting/account/zDmNq/users/clients' \
  -H 'Authorization: Bearer YOUR_ACCESS_TOKEN' \
  -H 'Content-Type: application/json' \
  -d '{
    "client": {
      "fname": "Jane",
      "lname": "Doe",
      "email": "jane@example.com",
      "organization": "Doe Industries",
      "currency_code": "USD"
    }
  }'

Invoices

MethodEndpointDescription
GET/accounting/account/{accountId}/invoices/invoicesList invoices
POST/accounting/account/{accountId}/invoices/invoicesCreate an invoice
GET/accounting/account/{accountId}/invoices/invoices/{invoiceId}Get an invoice
PUT/accounting/account/{accountId}/invoices/invoices/{invoiceId}Update an invoice

Invoice line items are not returned by default. You must explicitly request them with include[]:

curl -X GET 'https://api.freshbooks.com/accounting/account/zDmNq/invoices/invoices?include[]=lines' \
  -H 'Authorization: Bearer YOUR_ACCESS_TOKEN'

Expenses

MethodEndpointDescription
GET/accounting/account/{accountId}/expenses/expensesList expenses
POST/accounting/account/{accountId}/expenses/expensesCreate an expense
GET/accounting/account/{accountId}/expenses/expenses/{expenseId}Get an expense

Payments

MethodEndpointDescription
GET/accounting/account/{accountId}/payments/paymentsList payments
POST/accounting/account/{accountId}/payments/paymentsRecord a payment

Time Entries

MethodEndpointDescription
GET/timetracking/business/{businessId}/time_entriesList time entries
POST/timetracking/business/{businessId}/time_entriesCreate a time entry

Pagination

FreshBooks uses page-based pagination with page and per_page parameters.

curl -X GET 'https://api.freshbooks.com/accounting/account/zDmNq/invoices/invoices?page=1&per_page=100' \
  -H 'Authorization: Bearer YOUR_ACCESS_TOKEN'

Response includes pagination metadata:

{
  "response": {
    "result": {
      "invoices": [...],
      "page": 1,
      "pages": 14,
      "per_page": 100,
      "total": 1389
    }
  }
}

Watch out: the API silently caps per_page at 100. If you request per_page=2000, you get 100 results with no warning. You need proper pagination to fetch complete datasets.

async function getAllInvoices(accountId, accessToken) {
  const invoices = [];
  let page = 1;
  let totalPages = 1;

  do {
    const response = await fetch(
      `https://api.freshbooks.com/accounting/account/${accountId}/invoices/invoices?page=${page}&per_page=100`,
      { headers: { 'Authorization': `Bearer ${accessToken}` } }
    );
    const data = await response.json();

    invoices.push(...data.response.result.invoices);
    totalPages = data.response.result.pages;
    page++;
  } while (page <= totalPages);

  return invoices;
}

Searching and Filtering

FreshBooks supports search parameters on list endpoints. The syntax is search[field]=value:

# Find invoices for a specific client
curl -X GET 'https://api.freshbooks.com/accounting/account/zDmNq/invoices/invoices?search[customerid]=12345' \
  -H 'Authorization: Bearer YOUR_ACCESS_TOKEN'

# Find unpaid invoices
curl -X GET 'https://api.freshbooks.com/accounting/account/zDmNq/invoices/invoices?search[payment_status]=unpaid' \
  -H 'Authorization: Bearer YOUR_ACCESS_TOKEN'

For "in" searches (matching multiple values):

# Find invoices with status 2 or 4
curl -X GET 'https://api.freshbooks.com/accounting/account/zDmNq/invoices/invoices?search[status][]=2&search[status][]=4' \
  -H 'Authorization: Bearer YOUR_ACCESS_TOKEN'

Available search parameters vary by endpoint. Check the API docs for each resource.

Includes

Related data often requires extra API calls. FreshBooks provides an include[] parameter to fetch related resources inline:

# Get invoices with line items
curl -X GET 'https://api.freshbooks.com/accounting/account/zDmNq/invoices/invoices?include[]=lines' \
  -H 'Authorization: Bearer YOUR_ACCESS_TOKEN'

# Get invoices with lines and allowed payment gateways
curl -X GET 'https://api.freshbooks.com/accounting/account/zDmNq/invoices/invoices?include[]=lines&include[]=allowed_gateways' \
  -H 'Authorization: Bearer YOUR_ACCESS_TOKEN'

Without include[]=lines, invoice responses omit line item details. This trips people up.

Rate Limits

FreshBooks does not publish specific numeric rate limits but will throttle requests if you make too many calls in a short period. The API returns HTTP 429 when rate limited.

Implement exponential backoff on 429 responses, cache responses where appropriate, use includes to reduce call count, and batch operations when possible.

FreshBooks reserves the right to disable apps that hit the API aggressively.

Webhooks

You can subscribe to webhooks for real-time notifications instead of polling.

Registering a Webhook

curl -X POST 'https://api.freshbooks.com/events/account/zDmNq/events/callbacks' \
  -H 'Authorization: Bearer YOUR_ACCESS_TOKEN' \
  -H 'Content-Type: application/json' \
  -d '{
    "callback": {
      "event": "invoice.create",
      "uri": "https://your-server.com/webhooks/freshbooks"
    }
  }'

Verification

FreshBooks sends a verification code to your endpoint when you first register. You must verify ownership by sending the code back:

curl -X PUT 'https://api.freshbooks.com/events/account/zDmNq/events/callbacks/{callbackId}' \
  -H 'Authorization: Bearer YOUR_ACCESS_TOKEN' \
  -H 'Content-Type: application/json' \
  -d '{
    "callback": {
      "verifier": "scADVVi5QuKuj5qTjVkbJNYQe7V7USpGd"
    }
  }'

Save this verification code. You'll need it to verify incoming webhook signatures.

Webhook Payload

When an event occurs, FreshBooks POSTs form-urlencoded data to your URI:

name=invoice.create
&object_id=1234567
&account_id=zDmNq
&business_id=77128
&identity_id=712052
&user_id=1

The object_id is the ID of the affected resource. You'll need to call the API to get full details.

Signature Verification

Each webhook includes an X-FreshBooks-Hmac-SHA256 header. Verify it using the verification code as the secret:

import base64
import hmac
import hashlib
import json

def verify_webhook(verifier, request_data, signature):
    # FreshBooks sends form-urlencoded data, but calculates the signature
    # using a JSON representation. Convert all values to strings and serialize
    # to JSON. Note: Python's json.dumps adds spaces after : and , which matches
    # FreshBooks' format (e.g., {"key": "value", "key2": "value2"})
    msg = {k: str(v) for k, v in request_data.items()}
    payload = json.dumps(msg)

    calculated = hmac.new(
        verifier.encode('utf-8'),
        msg=payload.encode('utf-8'),
        digestmod=hashlib.sha256
    ).digest()

    return base64.b64encode(calculated).decode() == signature

Supported Events

Common webhook events:

EventDescription
client.createClient created
client.updateClient updated
client.deleteClient deleted
invoice.createInvoice created
invoice.updateInvoice updated
invoice.deleteInvoice deleted
invoice.sendByEmailInvoice emailed
estimate.createEstimate created
estimate.updateEstimate updated
estimate.deleteEstimate deleted
payment.createPayment recorded
payment.updatePayment updated
payment.deletePayment deleted
expense.createExpense created
expense.updateExpense updated
expense.deleteExpense deleted
project.createProject created
project.updateProject updated
project.deleteProject deleted
time_entry.createTime entry created
time_entry.updateTime entry updated
time_entry.deleteTime entry deleted

FreshBooks supports many more events including bill.*, bill_vendor.*, category.*, credit_note.*, item.*, recurring.*, service.*, and tax.*. See the FreshBooks webhook documentation for the full list.

You can subscribe to all events for a noun by using just the noun: invoice subscribes to create, update, delete, and sendByEmail.

Error Handling

FreshBooks returns standard HTTP status codes with JSON error bodies: 200 for success, 400 for bad request (invalid parameters), 401 for unauthorized (invalid or expired token), 403 for forbidden (insufficient permissions), 404 for not found, 429 for rate limited, 500 for server error.

Error responses include details:

{
  "response": {
    "errors": [
      {
        "errno": 1012,
        "field": "customerid",
        "message": "Customer not found.",
        "object": "invoice",
        "value": "999999"
      }
    ]
  }
}

Data Mapping Considerations

When mapping your data structures to FreshBooks:

Clients require either a first name/last name or an organization name. Email is optional but recommended. Clients are identified by their userid field in responses.

Invoices need line items with a name and unit_cost. The qty defaults to 1. Tax handling is complex and varies by region.

Monetary values come back as nested objects:

{
  "amount": {
    "amount": "1234.56",
    "code": "USD"
  }
}

Dates use ISO 8601 format (YYYY-MM-DD).

FreshBooks uses vis_state to track whether records are active (0), deleted (1), or archived (2). Records are soft-deleted by sending a PUT request with vis_state: 1. You can restore deleted records by setting vis_state back to 0. If you're updating a record, don't include vis_state unless you intend to change it. FreshBooks processes vis_state changes separately and will ignore other fields in the same request.

Connecting to FreshBooks and 25+ Accounting APIs at Once

Building a direct FreshBooks integration means handling OAuth token rotation, the dual identity model, silent pagination caps, and webhook signature verification. And that's one platform.

If your product needs multiple accounting systems, each has its own quirks. QuickBooks uses a different OAuth flow. Xero has its own pagination. Sage requires managing multiple regional APIs. For more on these challenges, see our guide on ERP integration for fintech and SaaS.

Apideck provides a unified API that handles these differences. You write one integration and get FreshBooks, QuickBooks, Xero, NetSuite, and 25+ other accounting platforms.

import { Apideck } from '@apideck/node';

const apideck = new Apideck({
  apiKey: process.env.APIDECK_API_KEY,
  appId: process.env.APIDECK_APP_ID,
  consumerId: 'user-123'
});

// List invoices from FreshBooks
const { data } = await apideck.accounting.invoicesAll({
  serviceId: 'freshbooks'
});

// Same code works for QuickBooks, Xero, etc.
const qbInvoices = await apideck.accounting.invoicesAll({
  serviceId: 'quickbooks'
});

Apideck handles OAuth flows, token refresh, data normalization, and pagination for each connector. Your team builds features instead of maintaining integrations.

Get started with Apideck to connect FreshBooks and 25+ other platforms through a single Accounting API. For a comparison of FreshBooks against other options, check out our top 15 accounting APIs to integrate with.

Frequently Asked Questions

Does FreshBooks provide a sandbox environment?

No. Use a trial account or a separate development account for testing. Be careful not to send real invoices from test accounts.

How do I handle multiple FreshBooks accounts for a single user?

Users can belong to multiple businesses with different roles. Parse the business_memberships array from the /me endpoint and let users select which business to connect. Store both the accountId and businessId for each connected business.

Does the API support bulk operations?

No dedicated bulk endpoints. You'll need to make individual API calls. Implement rate limiting and error handling to avoid hitting limits.

How do I handle currency conversion?

FreshBooks stores amounts in the currency specified for each client or invoice. Multi-currency transactions are supported, but currency conversion is not handled by the API. Your integration must handle conversion if needed.

What scopes should I request?

Request only the scopes your app needs. user:profile:read is required to get identity information. For client management, add user:clients:read and user:clients:write. For invoices, add user:invoices:read and user:invoices:write. For expenses, add user:expenses:read. For payments, add user:payments:read.

Over-requesting scopes makes users less likely to authorize your app.

Ready to get started?

Scale your integration strategy and deliver the integrations your customers need in record time.

Ready to get started?
Talk to an expert

Trusted by fast-moving product & engineering teams

JobNimbus
Blue Zinc
Drata
Octa
Nmbrs
Apideck Blog

Insights, guides, and updates from Apideck

Discover company news, API insights, and expert blog posts. Explore practical integration guides and tech articles to make the most of Apideck's platform.