---
name: freshbooks-oauth-app
description: Creates or signs in to a FreshBooks account and registers a FreshBooks OAuth2 app to obtain a client ID and client secret — with redirect URIs, the portal-side `user:<object>:<action>` scope picker, the account-id / business-id / business-UUID indirection behind `/auth/api/v1/users/me`, one-time-use rotating refresh tokens, and a safe credential handoff. Use when asked to get FreshBooks OAuth credentials, set up a FreshBooks developer app, rotate a FreshBooks client secret, add a redirect URI, or fix a FreshBooks error like a 403 naming missing scopes, a 401 on a token that worked an hour ago, a refresh that suddenly fails, or 404s on every accounting call. For any other vendor's developer portal, use that vendor's skill instead.
---

# FreshBooks OAuth2 App Registration

Get a working FreshBooks OAuth2 client — a FreshBooks account, an app on the developer page, redirect URIs, scopes,
a client ID and secret — for a platform that connects many customers' FreshBooks businesses.

Three things decide whether this works, and only one of them is on the registration form.

**A FreshBooks token does not name an account.** After the flow you call the identity endpoint to discover which
businesses the user belongs to — and each one carries *three* different identifiers. Invoices, expenses, taxes and
payments are addressed by a short alphanumeric **account id**; projects, services and time tracking by a numeric
**business id**; the chart of accounts, journal entries and the ledger reports by a **business UUID**. Swap two of
them and FreshBooks returns 404, which reads like "this customer has no data". That is §7.

**Scopes are ticked on the app, not sent on the authorize URL.** FreshBooks' authorize URL takes no `scope`
parameter. Whatever is checked on the app is what every user consents to, and adding a scope later means every
existing user must re-authorize. That is §5.

**Refresh tokens are single-use and there is only ever one alive.** Every token response invalidates the previous
refresh token immediately. A client that keeps reusing the original one works once and then strands the customer.
That is §9.

## Inputs to collect before you start

Ask in one batch. Never invent.

| Input | Notes |
| --- | --- |
| **App name** and description | Shown to the user on the FreshBooks consent screen (§3) |
| **FreshBooks account email** | Any FreshBooks account — trial or paid — can own apps (§2) |
| **Redirect URIs** | Every callback host your platform serves; HTTPS only, no query strings (§4) |
| **Scope set** | Exactly the `user:<object>:<action>` strings the connector calls (§5) |
| **New app or an edit to an existing one?** | New credentials orphan every existing customer connection (§1) |
| **Will this be listed on the FreshBooks AppStore?** | Listing is a review process with its own requirements (§11) |
| **Does the client persist the rotated refresh token?** | Blocking — a client that does not will fail on the second refresh (§9) |
| **Which objects the connector reads/writes** | Decides scopes, and decides whether you need account-scoped, business-scoped or UUID-scoped paths (§7, §8) |

## Quick Start

1. Confirm a **new app** is actually needed — existing connections are bound to the current client ID (§1).
2. Sign in to, or create, a FreshBooks account; a free trial doubles as the test account (§2).
3. Create the app on the developer page and fill in name and redirect URI (§3).
4. Add **every** redirect URI, one per line, HTTPS, no query strings (§4).
5. Tick **every** scope the connector needs — they cannot be sent at authorize time (§5).
6. Capture the client ID and client secret (§6).
7. Work through the account-id / business-id / business-UUID indirection before writing client code (§7).
8. Establish which API generation each endpoint belongs to (§8).
9. Confirm the client handles ~12-hour access tokens and single-use rotating refresh tokens (§9).
10. Verify a real authorize → callback → token → refresh → read round trip (§12).
11. Hand the credentials over — never commit them (§13).

## Platform state (verified 2026-09-20 — re-verify before trusting)

- **The developer portal is self-serve and there is no approval gate on connecting other accounts.** You create an
  app from the developer page inside any FreshBooks account and get a client ID and secret immediately. FreshBooks
  documents an **AppStore review** with statuses *Development, Pending, In Review, Approved, Suspended, Rejected* —
  that review governs **being listed in the AppStore**, not whether your OAuth app functions. FreshBooks does not
  publish a connection cap for unlisted apps, and does not publish a statement that an unreviewed app is limited to
  the developer's own account; if that matters commercially, ask FreshBooks rather than assuming either way.
- **Scopes live on the app.** The documented authorize URL is
  `https://auth.freshbooks.com/oauth/authorize/?response_type=code&redirect_uri=<REDIRECT_URL>&client_id=<CLIENT_ID>`
  — no `scope` parameter. FreshBooks' own scope guide tells you to *edit your application to include the scopes*,
  and warns that afterwards "you'll need your app's users to authorize your app once again".
- **`user:profile:read` is added to every new app by default**, because basic calls need it.
- **Scopes cannot be narrowed on a live token.** "It's not possible to remove scopes from an existing access token.
  The only way to reduce or add consented scopes is to revoke the token and start with the app authorization flow
  again."
- **Access tokens are JWTs.** FreshBooks replaced fixed-length bearer tokens with variable-length JWTs for new apps
  in April 2021, with a 1 May 2023 deadline for legacy apps. Store them in a `TEXT`-sized column — FreshBooks
  explicitly warns about 64-character assumptions.
- **`client_credentials` is not supported.** The token page states it inline: authorization code grant only. There
  is no machine-to-machine path and no way to connect an account without a browser round trip.
- **Two API generations coexist, and both are current.** The Classic XML API (`/api/2.1/xml-in`, API token or
  OAuth 1.0) is the dead one; everything at `api.freshbooks.com` is the live one. But *inside* the live API there is
  a second, subtler split between account-scoped legacy accounting paths and newer business-UUID-scoped ledger
  paths. See §8 — this is the generation question that actually costs time.
- **Rate limits are real but unpublished.** FreshBooks states there is no daily request cap but that requests "will
  be rate-limited if too many calls are made within a short period of time", and its AppStore requirements name the
  status: **HTTP 429 `HTTP_RATE_LIMITED`**, with the right to disable an app that "is hitting us aggressively". No
  numeric threshold and no `Retry-After` contract is documented.
- **The API changelog is one page.** The only entry FreshBooks publishes under "API Changelog" is the bearer-token →
  JWT migration. Do not expect a dated feed; treat the individual endpoint pages as the source of truth.

If the portal does not look like this, stop and report what you actually see rather than clicking on.

## 1. Decide: reuse the existing app, or register a new one

A new app means a **new client ID, and every existing customer connection is bound to the old one** — every customer
would have to re-authorize, and each re-authorization is an action only they can take.

Reuse the existing app for: adding a redirect URI, adding a scope, rotating a compromised secret, or diagnosing a
failure.

Register a **new** app only when the user explicitly wants one: a replacement for a compromised app, a separate app
for a different product, or a deliberate test app. FreshBooks itself recommends the last one — its scope guide tells
you to **duplicate your app from the developer portal** and hunt missing scopes against the copy before touching the
production app. That is the right move here too (§5). Say which path you are taking before you touch the portal.

## 2. Account: sign in or sign up

- **Any FreshBooks account owns apps.** There is no separate developer registration and no separate developer
  account tier. Sign in at `https://www.freshbooks.com/` and go to the developer page.
- **Developer page**: `https://my.freshbooks.com/#/developer`. It is an authenticated single-page app, so it will
  not render for an unauthenticated fetch — that is expected, not an outage.
- **Test accounts**: FreshBooks' migration guide says to "create test accounts (sandboxes) by signing up for a free
  trial account". There is **no separate sandbox host and no second key pair** — production and test run against the
  same endpoints with the same credentials. Whichever account authorizes is the account you touch, so be careful
  which one you are signed into.
- **Who may authorize**: FreshBooks states that accessing data in an account "will require an owner, admin, or
  manager to allow access to your application within a web browser". Lower-privileged roles exist in the identity
  model (§7) and matter to what your connector can resolve afterwards.

Hand control back to the user for anything only a human can do: signup, email verification, CAPTCHA, MFA, accepting
terms, and clicking Allow on the consent screen. Do not retry a blocked step in a loop.

**If this session has no browser automation** (the usual case for a CLI or cloud run), do not pretend to click. Hand
the user an exact, ordered click path with the literal values to paste — the redirect URIs from §4 and the scope
strings from §5 — then continue once they report back with the client ID.

## 3. Create the app

On the developer page, create an application. The form asks for the **name of the application and a redirect URI**.
Then open the app and work through **App Settings**, where the portal also shows you a ready-made, clickable
authorization URL for your client ID — useful for testing the flow by hand before any code exists.

What carries more weight than it looks:

- The **name and description are what the consent screen shows the customer.** Use the go-to-market name.
- The **scope list is part of the app record, not the request** (§5). Getting it wrong is the most expensive mistake
  on this form, because fixing it later forces every user to re-consent.
- Duplicating an app is a first-class action in this portal and is the documented way to test scopes safely (§5).

## 4. Redirect URIs

Add **every** callback host your platform serves. FreshBooks allows multiple redirect URIs per application,
"specified on separate lines of the Redirect URI field". For Unified.to these are one per data center; confirm the
current list with the platform owner rather than assuming:

```
https://api.unified.to/oauth/code          # us (default)
https://api-eu.unified.to/oauth/code       # eu
https://api-au.unified.to/oauth/code       # au
https://api-dev.unified.to/oauth/code      # dev
```

Rules FreshBooks states explicitly:

- **HTTPS only.** The one concession: while developing, if you cannot set up a certificate, you may manually change
  the URL to HTTP in your browser to complete the connection. That is a hand-testing escape hatch, not a registered
  value.
- **No query string parameters in a redirect URI.** If you need to carry data through the flow, url-encode it into
  the `state` parameter; FreshBooks inserts `state` into the redirect URI when it sends the user back. All four URIs
  above are query-free, which is why they are legal.
- The `redirect_uri` you send at the **token exchange must be the same one** used to obtain the code — FreshBooks'
  own exchange and refresh examples both carry it.

## 5. Scopes — chosen on the app, never on the authorize URL

This is the part people get wrong because every other vendor works the other way.

FreshBooks scopes read `user:<object>:<action>`. Two actions only: **`read`** (reading the full information about a
resource) and **`write`** (creating, editing, archiving or deleting). The objects FreshBooks lists:

```
bill_payments   bill_vendors   billable_items   bills       business
clients         credit_notes   estimates        expenses    invoices
journal_entries notifications  online_payments  other_income
payments        profile        projects         reports     retainers
taxes           teams          time_entries
```

Three things that list does not tell you:

1. **`user:profile:read` is granted to every new app by default** and is needed for basic calls.
2. **The list is incomplete.** `user:uploads:read` and `user:uploads:write` are not on the scopes page, but they are
   documented as required on the expense-attachment and invoice-presentation-attachment pages. Trust the individual
   endpoint page over the summary list.
3. **`admin:all:legacy` is the pre-October-2021 blanket scope.** If an existing app still carries it, it is a legacy
   app and moving it to granular scopes forces every user to re-authorize.

How to establish the exact set, FreshBooks' own method:

- Read the **Access Requirements** box at the top of each endpoint page you call — it names the scopes verbatim.
- Then confirm empirically: **duplicate the app** with only `user:profile:read`, make each call you need, and read
  the **HTTP 403** back. Since October 2021 that error names both *the scopes you provided* and *the scopes needed
  for the API call*. Note them, add them to the duplicate, re-authorize, repeat until clean, and only then edit the
  production app.
- Request the narrow set. FreshBooks' AppStore requirements state that an app "should request only the permissions
  that are necessary for it to function and as described in your listing", and the consent screen shows the user
  exactly what you ticked.

**Product fact.** As of 2026-09-20, this platform's FreshBooks connector maps each unified object to the narrowest
scopes it needs rather than asking for one blanket list — drawn from `user:profile:read`, `user:invoices:read` /
`:write`, `user:clients:read` / `:write`, `user:expenses:read` / `:write`, `user:bills:read` / `:write`,
`user:credit_notes:read` / `:write`, `user:payments:read` / `:write`, `user:taxes:read` / `:write`,
`user:journal_entries:read` / `:write`, `user:billable_items:read` / `:write`, `user:projects:read` / `:write`,
`user:uploads:read` / `:write`, `user:teams:read`, `user:reports:read`, and `user:account:read` / `:write`. Two of
those are worth checking against the portal picker before you tick anything: **`user:uploads:*` is documented only
on the attachment pages**, and **`user:account:*` does not appear on FreshBooks' published object list at all** —
confirm both are offered on the app's scope picker, and if `user:account:*` is not, work out from the 403 responses
what the ledger-account, tax and report endpoints actually want. The connector's list is a **specification for what
to tick in the portal**, because the connector sends no `scope` parameter at authorize time (§6).

## 6. Capture the credentials

From the developer page → your app → **App Settings**:

- **Client ID** and **client secret** — both shown on the app page. The client ID is also embedded in the app's own
  portal URL and in the authorization link the portal generates, so it is not a secret; the client secret is.
- **Endpoints**, all shared between test and production — there is no sandbox host:

  | Purpose | URL |
  | --- | --- |
  | Authorize | `https://auth.freshbooks.com/oauth/authorize/` |
  | Token exchange and refresh | `https://api.freshbooks.com/auth/oauth/token` |
  | Revoke a bearer or refresh token | `https://api.freshbooks.com/auth/oauth/revoke` |
  | API base | `https://api.freshbooks.com` |
  | Identity | `https://api.freshbooks.com/auth/api/v1/users/me` |

- **Exchange format.** FreshBooks documents the token call as a POST with `grant_type`, `client_id`, `client_secret`,
  `code` and `redirect_uri` **in the request body** — a JSON body in the authentication reference, multipart form
  fields in the tutorial; both are shown working. There is no HTTP Basic variant, and no PKCE: the client secret
  travels in the body, so this is a confidential-client flow only.
- **Revocation** takes `client_id`, `client_secret` and `token` (either a bearer or a refresh token) in the body.

Rotating the secret breaks every exchange and refresh until the new value is deployed. Never rotate without explicit
go-ahead and a cutover plan. Report the secret once so the user can paste it into their secret store, say plainly
that it is now in the transcript and can be regenerated, then move on.

**Product fact.** As of 2026-09-20, this platform's FreshBooks connector sends only `client_id`, `redirect_uri`,
`response_type` and `state` on the authorize request — **no `scope` parameter**, which matches FreshBooks' documented
URL and confirms that the portal's scope picker is the whole story. It does **not** use PKCE. It exchanges the code
and refreshes with a JSON POST carrying the client id and secret in the body, and replaces the stored refresh token
with the rotated one from each response. It is configured to use the platform's shared FreshBooks OAuth credentials
by default. It does **not** currently wire up the revocation endpoint, so disconnecting a customer on this side does
not invalidate their FreshBooks tokens — raise that if a customer asks about revocation guarantees.

## 7. The identity indirection — one token, three identifiers

Nothing on the registration form warns you about this, and it is where FreshBooks connectors break.

Right after the exchange, call:

```
GET https://api.freshbooks.com/auth/api/v1/users/me
Authorization: Bearer <access_token>
```

FreshBooks calls this the **Identity Model**. A user is identified by email across the whole product, and the
response's `business_memberships` array lists every business they can reach. Each membership carries a `role` and a
`business` object with **`id`** (numeric business id), **`business_uuid`**, **`account_id`** (short alphanumeric,
e.g. `e6Wmk`) and `name`.

FreshBooks is candid about why there are three: *"For historical reasons, FreshBooks has shifted from an account
concept to a business concept, but many resources still make use of accountIds."*

| Identifier | Shape | Path family | Examples |
| --- | --- | --- | --- |
| **account id** | short alphanumeric | `/accounting/account/{accountId}/…`, `/uploads/account/{accountId}/…`, `/events/account/{accountId}/…` | invoices, clients, expenses, expense categories, taxes, payments, bills, credit notes, attachments, webhook callbacks |
| **business id** | integer | `/projects/business/{businessId}/…`, `/timetracking/business/{businessId}/…`, `/comments/business/{businessId}/…`, `/auth/api/v1/businesses/{businessId}/…` | projects, services, time entries, team members |
| **business UUID** | UUID | `/accounting/businesses/{business_uuid}/…` | chart of accounts, ledger accounts, journal entries, balance sheet, profit and loss, cash flow, general ledger, trial balance |

FreshBooks states the first two rules plainly — accounting endpoints take `accountId`
(`/accounting/account/{accountId}/invoices/invoices`), non-accounting endpoints take `businessId`
(`/timetracking/business/{businessId}/time_entries`) — and its webhook payloads carry **both** `account_id` and
`business_id` for exactly this reason: *"Because some FreshBooks resources use account and others business, both the
`account_id` and `business_id` are provided."* The third, the business UUID, is the newer ledger surface (§8).

What follows from that:

- **Resolve all three at connect time and store all three.** Deriving one from another later is not possible without
  another identity call.
- **A mismatch is a 404, not a 403.** Sending a business UUID where an account id belongs returns "not found", which
  in a sync pipeline reads as "this customer has no invoices". When a connection is healthy but every object is
  empty, suspect the identifier before you suspect the data.
- **One identity can belong to several businesses.** Accountants and bookkeepers routinely do. Picking one silently
  means syncing the wrong company. Decide explicitly whether one connection means one business — and if so, model
  several businesses as several connections.
- **Role matters.** FreshBooks maps API roles to UI names: `owner`/`admin` → Owner, `business_partner` → Admin,
  `business_manager` → Manager, `business_employee` → Employee, `business_accountant` → Accountant, plus
  `contractor` and `client`. FreshBooks says an **owner, admin or manager** can grant access — so a perfectly valid
  connecting user may arrive with a role that is neither `owner` nor `admin`. Any logic that only recognises those
  two will resolve nothing and produce the 404 pattern above.

**Product fact.** As of 2026-09-20, this platform's FreshBooks connector calls the identity endpoint once right
after authorization, picks the **first membership whose role is owner or admin**, and stores that business's account
id, numeric business id and business UUID against the connection; every later call builds its path from those three.
Two consequences to verify with the connector's owner before promising anything to a customer: a user who authorizes
with a **manager, admin-in-the-UI (`business_partner`), accountant or contractor** role resolves to nothing and all
subsequent calls 404; and a user with **several businesses** gets exactly one of them, chosen by FreshBooks'
ordering.

## 8. Two API generations — and the split inside the live one

Ask "which API generation" and you get two different answers. Both matter.

**The dead one is easy.** FreshBooks Classic used a single fixed URL (`https://<company>.freshbooks.com/api/2.1/xml-in`),
an XML payload with the action in the body (`<request method="invoice.create">`), and authenticated with an API token
or OAuth 1.0. The modern API at `api.freshbooks.com` is JSON, one URL per action, and **only supports OAuth 2.0**.
FreshBooks publishes a per-endpoint table of what survived the move — notably **staff, gateways, currencies, default
terms, email templates, contractors and the Classic detail reports did not**. If anything you are looking at mentions
`xml-in` or an API token, it is the dead generation; do not build on it.

**The live split is the one that bites.** Inside the modern API, two path families coexist:

- **Account-scoped legacy accounting**: `/accounting/account/{accountId}/…` — invoices, clients, expenses, expense
  categories, taxes, payments, bills, credit notes, and the older report family under
  `/accounting/account/{accountId}/reports/accounting/…` (invoice details, payments collected, tax summary).
- **Business-UUID-scoped ledger**: `/accounting/businesses/{business_uuid}/…` — the chart of accounts
  (`/ledger_accounts/accounts`), journal entries, and the ledger report family
  (`/reports/balance_sheet`, `/reports/profit_and_loss`, `/reports/trial_balance`, `/reports/cash_flow`,
  `/reports/general_ledger`, `/reports/chart_of_accounts`, `/reports/manual_journal_entry_report`).

Neither is deprecated; they address different things. Two practical rules:

- **Some business-scoped endpoints require extra ceremony.** Journal entries require the header
  `x-api-version: 2023-09-25` on create, read and update. The chart of accounts and several ledger reports require
  `use_ledger_entries=true` as a mandatory query parameter. Omit either and you get an error that looks like a
  permission or identifier problem but is not.
- **Ledger identifiers are UUIDs, not integers.** A journal entry line references an account by `account_uuid`, which
  you get from the chart-of-accounts call. Account ids from the legacy family will not substitute.

**Product fact.** As of 2026-09-20, this platform's FreshBooks connector reads the chart of accounts, journal
entries, profit and loss, and cash flow from the **business-UUID** family, and invoices, clients, expenses,
categories, taxes, payments, bills, credit notes, attachments and webhook callbacks from the **account-id** family —
matching FreshBooks' docs. It reads the **balance sheet and trial balance from the account-scoped path**
(`/accounting/account/{accountId}/reports/accounting/balance_sheet` and `…/trial_balance`), which still responds but
which FreshBooks' current documentation only shows under the business-UUID family. Treat those two as the pair most
likely to move; confirm with the connector's owner before relying on them.

## 9. Token behaviour: ~12 hours, one refresh token alive, rotation on every use

| Token | Lifetime | Rule |
| --- | --- | --- |
| authorization `code` | **5 minutes**, single use | Delay or reuse and the exchange fails |
| `access_token` | JWT; FreshBooks' own example returns **`expires_in: 43200`** (12 hours) | FreshBooks says only "check the expiry in the token" — read `expires_in`, do not hard-code 12 hours |
| `refresh_token` | **Never expires — but is single-use** | A new one is issued with every access token; all older ones become invalid immediately |

The three ways this bites:

1. **Rotation, with no grace window.** FreshBooks: *"Refresh Tokens live forever, but are one-time-use, and only one
   Refresh Token can be alive at any time per user per application. A new Refresh Token is generated every time a
   Bearer Token is issued … and all old Refresh Tokens immediately become invalid."* Persist the new refresh token in
   the same transaction as the access token. If your app fails to save one, the customer must re-authorize — there is
   no documented grace period, unlike some other vendors.
2. **Refreshing kills the old access token.** FreshBooks states that "requesting a new access token via a refresh
   token invalidates the previous access token immediately". Two workers refreshing the same connection concurrently
   will knock each other out; serialize refreshes per connection. (Note that FreshBooks' authentication reference
   also says bearer tokens "don't interfere with each other's lives" and that several can be valid at once — the two
   pages disagree. Design for the stricter reading: one live access token, one live refresh token.)
3. **An expired access token is a plain 401.** FreshBooks says so explicitly: call with an expired token and you get
   `401 unauthorized`, meaning refresh — not a scope or identifier problem. You do not have to wait for the 401; you
   can refresh proactively from `expires_in`.

## 10. Rate limits, pagination and the shape of errors

- **Rate limits**: no daily cap, but bursts are throttled. FreshBooks names **HTTP 429 `HTTP_RATE_LIMITED`** in its
  AppStore requirements and instructs apps to "hold off making api calls for a moment", reserving the right to
  disable an app that hits aggressively. **No numeric threshold and no `Retry-After` contract is published** — use
  exponential backoff and do not invent a number for the customer.
- **Pagination**: `per_page` is **silently capped at 100**. Page 0 returns page 1; a non-positive-integer page is an
  error; a page beyond the last returns an empty list. So a list that stops at 100 rows is the cap, not the data.
- **Filters** follow documented patterns per resource — Equals, In, Like, Between, Datetime, Bool. Date-range
  `Between` filters on columns like `updated` generally have **day-level granularity** and take `YYYY-MM-DD`.
- **Errors**: the accounting family returns a numbered error envelope — `1001 RequiredField`, `1003 AccessDenied`,
  `1004` for field validation, `1012 UnknownResource`, `409` for concurrent modification. Other families return plain
  HTTP semantics: 401 when no valid token was provided, 403 when the resource is not accessible to this token, 404
  when the resource in the URL is not found. A **403 that names the scopes provided and the scopes needed** is the
  scope error from §5, not a permission problem in the customer's account.

## 11. Webhooks, if the connector uses them

Not part of registration, but it fails in a way that looks like a registration problem.

- Callbacks are registered by POST to the **account-scoped** callbacks endpoint, and the scope required depends on
  the event noun (e.g. `bill` events need `user:bills:read`).
- **There is a verification handshake.** On registration FreshBooks POSTs a verification code and the callback id to
  your URI; you must PUT them back before any notifications flow. A webhook that registers cleanly and then delivers
  nothing is almost always an unanswered handshake. You can ask FreshBooks to resend the code at any time.
- **Deliveries are signed.** Every webhook carries `X-FreshBooks-Hmac-SHA256`: a base64-encoded HMAC of a UTF-8 JSON
  string of the parameters, keyed with **the verification code from the handshake**. Store that code — it is the
  signing secret. FreshBooks warns that the JSON must be serialized with a space after each `:` and `,` or the
  signature will not match.
- Payloads are form-urlencoded and carry `name`, `object_id`, `account_id`, `business_id` and `identity_id`.
- Anything other than a 2xx within **ten seconds** is a failure; failures are retried, then dropped, and a
  persistently failing URL may be disabled (re-enable by resending the verification token).

**Product fact.** As of 2026-09-20, this platform's FreshBooks connector registers callbacks under the account id and
answers the verification handshake, but does **not** verify the `X-FreshBooks-Hmac-SHA256` signature on inbound
deliveries. Flag that to the connector's owner as a security gap rather than assuming it is handled elsewhere.

## 12. Verify end-to-end

Testing with the account that owns the app hides the multi-business and role problems. Test wider.

1. Authorize through your platform's real connect flow and confirm the token response carries **both** an access
   token and a refresh token, and read `expires_in` rather than assuming it.
2. Call the identity endpoint and confirm you store **all three** identifiers for the chosen business (§7).
3. Make one read from each path family: an account-scoped one (invoices), a business-scoped one (projects or team
   members), and a business-UUID one (chart of accounts). All three must work before you call this done.
4. Authorize with a user whose role is **manager / `business_partner` / accountant**, not just the owner, if
   customers will connect that way.
5. Authorize an identity with **two businesses** and confirm the behaviour is what you told the customer it would be.
6. Force a **refresh**, then force a **second** refresh using the token the first one returned. That is what catches
   a client ignoring rotation.
7. Deliberately call an endpoint whose scope you did not tick, and confirm you surface the 403 as "reconnect
   needed", not as a server error.

| Symptom | Cause |
| --- | --- |
| Every accounting call 404s, connection otherwise healthy | Account id never resolved — often an authorizing user whose role is not owner/admin (§7) |
| Projects, services or time tracking 404 while invoices work | Account id sent where the numeric business id belongs (§7) |
| Chart of accounts, journal entries or ledger reports 404 | Business UUID missing, or an account id used on a business-scoped path (§7, §8) |
| Journal entry calls fail with a version or schema error | Missing `x-api-version: 2023-09-25` header (§8) |
| Chart of accounts or a ledger report returns an error on a correct-looking URL | Missing mandatory `use_ledger_entries=true` (§8) |
| 403 naming "the scopes you provided" and "the scopes needed" | Scope not ticked on the app, or the user consented before it was added — re-authorize (§5) |
| Customer consented but the consent screen listed fewer permissions than expected | Scopes come from the app record, not the authorize URL (§5) |
| 401 on a call that worked an hour ago | Access token past its life (~12h) — refresh (§9) |
| Refresh fails and the customer must reconnect | The stored refresh token was already spent; only one is alive per user per app (§9) |
| Second refresh fails, first one worked | Client is not persisting the rotated refresh token (§9) |
| Two workers both refresh, then both get 401 | Concurrent refreshes — each invalidates the other's access token (§9) |
| Exchange fails immediately after consent | Authorization code older than 5 minutes, already used, or a `redirect_uri` that differs from the one used for the code (§4, §9) |
| Authorize page errors, or the redirect is rejected | Redirect URI not registered exactly, not HTTPS, or carries a query string (§4) |
| 429 `HTTP_RATE_LIMITED` | Burst throttling — back off; no published threshold (§10) |
| A list never returns more than 100 rows | `per_page` is silently capped at 100 (§10) |
| Webhook registers, then nothing arrives | Verification handshake never answered (§11) |
| Data lands against the wrong company | The identity had several business memberships and one was picked silently (§7) |
| An old integration guide mentions `xml-in` or an API token | Classic API — the dead generation (§8) |

## 13. Hand off — never commit the secret

- **Do not** write the client secret into source control, a test, a fixture, a committed `.env`, a ticket, a PR body
  or a chat channel. Values go to the user, for the secret store or console.
- If a code change is needed (a redirect host, a scope addition, storing rotated refresh tokens), keep it
  secret-free and say what the human must set out of band.
- Close with: app name; the FreshBooks account that owns it; client ID; where the secret was delivered; the
  authorize, token and revoke endpoints; **the exact scope strings ticked on the app**; whether the client persists
  the rotated refresh token; how the account id, business id and business UUID are resolved and stored; whether the
  webhook signature is verified; and anything left for the user to do.

## Stop and ask

Hand back to a human rather than guessing when: the client does not persist rotated refresh tokens (report it — do
not register and hope); a scope you need is not offered on the app's scope picker; the customer needs multi-business
behaviour the connector does not implement (§7); existing users would have to re-authorize because scopes changed,
and nobody has agreed to that customer communication; the product needs AppStore listing, and the review's
requirements, timeline or commercial terms are in question; anyone asks for a guaranteed connection cap, rate-limit
number or `Retry-After` contract, none of which FreshBooks publishes; or the portal does not match the
**Platform state** section above.

## References

- Getting started / introduction — https://www.freshbooks.com/api/start
- Authentication (authorize URL, token, refresh, revoke, redirect URI rules) — https://www.freshbooks.com/api/authentication
- Get Authenticated tutorial (`expires_in`, 5-minute code, refresh behaviour) — https://www.freshbooks.com/api/get-authenticated-on-the-freshbooks-api
- Identity Model (account id vs business id, roles, `business_memberships`) — https://www.freshbooks.com/api/identity_model
- Scopes — https://www.freshbooks.com/api/scopes
- How to find the right scopes for your app (403 scope error, duplicate-app method) — https://www.freshbooks.com/api/how-to-find-the-right-scopes-for-your-app
- Errors and accounting error codes — https://www.freshbooks.com/api/errors
- Request limits — https://www.freshbooks.com/api/limits
- Search, paging and includes (`per_page` capped at 100) — https://www.freshbooks.com/api/parameters
- Webhook callbacks (handshake, HMAC header, payload fields) — https://www.freshbooks.com/api/webhooks
- Chart of Accounts (`/accounting/businesses/{business_uuid}/ledger_accounts/accounts`) — https://www.freshbooks.com/api/chart-of-accounts
- Journal Entries (`x-api-version: 2023-09-25`) — https://www.freshbooks.com/api/journal-entries
- Reports index (account-scoped vs business-UUID-scoped report paths) — https://www.freshbooks.com/api/reports
- Balance Sheet report — https://www.freshbooks.com/api/balance-sheet-report
- Cash Flow report — https://www.freshbooks.com/api/cash-flow-report
- General Ledger report — https://www.freshbooks.com/api/general-ledger-report
- Expense attachments (`user:uploads:*`) — https://www.freshbooks.com/api/expense-attachments
- Invoice presentation and attachments — https://www.freshbooks.com/api/invoice_presentation_attachments
- Team members (`/auth/api/v1/businesses/{business_id}/team_members`) — https://www.freshbooks.com/api/team-members
- Taxes — https://www.freshbooks.com/api/taxes
- How to migrate from the Classic XML API — https://www.freshbooks.com/api/how-to-migrate-from-the-classic-xml-api
- FreshBooks Classic migration (endpoint-by-endpoint survival table) — https://www.freshbooks.com/api/freshbooks-classic-migration
- Requirements for public apps on the AppStore (429, review statuses) — https://www.freshbooks.com/api/requirement-for-public-apps-on-the-freshbooks-app-store
- How to create your FreshBooks app listing — https://www.freshbooks.com/api/how-to-create-your-freshbooks-listing
- API changelog: bearer tokens replaced with JWT tokens — https://www.freshbooks.com/api/bearer-tokens-replaced-with-jwt-tokens
- Postman collection — https://www.freshbooks.com/api/postman-collection
- Developer page (create and edit apps; sign-in required) — https://my.freshbooks.com/#/developer
