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:
- User visits your authorization URL, gets redirected to FreshBooks
- User logs in and grants permission
- FreshBooks redirects back with an authorization code
- 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
| 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:
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
| 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[]:
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.
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:
| 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 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.








