---
title: "How to Integrate with the FreshBooks API"
description: "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."
author: "Kateryna Poryvay"
published: "2026-03-23T15:00+00:00"
updated: "2026-03-24T10:32:03.552Z"
url: "https://www.apideck.com/blog/how-to-integrate-with-the-freshbooks-api"
tags: ["Unified API", "Accounting", "Guides & Tutorials"]
---

# 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](https://www.apideck.com/integrations/freshbooks) 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](https://www.apideck.com/blog/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](https://www.freshbooks.com) if you don't have an account. Trial accounts work for development.

### Register Your Application

Go to the [FreshBooks developer page](https://my.freshbooks.com/#/developer) 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:

```bash
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:

```json
{
  "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.

```bash
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:

```bash
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:

```bash
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:

```json
{
  "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

| Method | Endpoint | Description |
|--------|----------|-------------|
| GET | `/accounting/account/{accountId}/users/clients` | List all clients |
| POST | `/accounting/account/{accountId}/users/clients` | Create 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`:

```bash
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:

```bash
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

| Method | Endpoint | Description |
|--------|----------|-------------|
| GET | `/accounting/account/{accountId}/invoices/invoices` | List invoices |
| POST | `/accounting/account/{accountId}/invoices/invoices` | Create 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[]`:

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

### Expenses

| Method | Endpoint | Description |
|--------|----------|-------------|
| GET | `/accounting/account/{accountId}/expenses/expenses` | List expenses |
| POST | `/accounting/account/{accountId}/expenses/expenses` | Create an expense |
| GET | `/accounting/account/{accountId}/expenses/expenses/{expenseId}` | Get an expense |

### Payments

| Method | Endpoint | Description |
|--------|----------|-------------|
| GET | `/accounting/account/{accountId}/payments/payments` | List payments |
| POST | `/accounting/account/{accountId}/payments/payments` | Record a payment |

### Time Entries

| Method | Endpoint | Description |
|--------|----------|-------------|
| GET | `/timetracking/business/{businessId}/time_entries` | List time entries |
| POST | `/timetracking/business/{businessId}/time_entries` | Create a time entry |

## Pagination

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

```bash
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:

```json
{
  "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.

```javascript
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`:

```bash
# 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):

```bash
# 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:

```bash
# 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

```bash
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:

```bash
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:

```python
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:

| Event | Description |
|-------|-------------|
| client.create | Client created |
| client.update | Client updated |
| client.delete | Client deleted |
| invoice.create | Invoice created |
| invoice.update | Invoice updated |
| invoice.delete | Invoice deleted |
| invoice.sendByEmail | Invoice emailed |
| estimate.create | Estimate created |
| estimate.update | Estimate updated |
| estimate.delete | Estimate deleted |
| payment.create | Payment recorded |
| payment.update | Payment updated |
| payment.delete | Payment deleted |
| expense.create | Expense created |
| expense.update | Expense updated |
| expense.delete | Expense deleted |
| project.create | Project created |
| project.update | Project updated |
| project.delete | Project deleted |
| time_entry.create | Time entry created |
| time_entry.update | Time entry updated |
| time_entry.delete | Time entry deleted |

FreshBooks supports many more events including `bill.*`, `bill_vendor.*`, `category.*`, `credit_note.*`, `item.*`, `recurring.*`, `service.*`, and `tax.*`. See the [FreshBooks webhook documentation](https://www.freshbooks.com/api/webhooks) 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:

```json
{
  "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:

```json
{
  "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](https://www.apideck.com/blog/how-to-integrate-your-app-with-quickbooks-online). 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](https://www.apideck.com/blog/erp-integration-for-fintech-and-saas-connecting-quickbooks-netsuite-and-sage).

[Apideck](https://www.apideck.com) provides a unified API that handles these differences. You write one integration and get FreshBooks, QuickBooks, Xero, NetSuite, and 25+ other accounting platforms.

```javascript
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](https://www.apideck.com/signup) 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](https://www.apideck.com/blog/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.