freshbooks-mcp
An MCP server that exposes FreshBooks accounting, project and time-tracking data to Claude as typed tools: OAuth2 setup, read/lookup tools across most resources, and confirmation-gated writes.
Authenticate: get the OAuth consent URL (
freshbooks_auth_url), exchange the redirect code for tokens (freshbooks_auth_exchange), and verify credentials/reachability (freshbooks_healthcheck).Resolve identity:
freshbooks_get_identityreturns accountId, businessId, businessUuid, plus account/business roles and the list of businesses.Read records: list/get invoices, clients, estimates, payments, items, expenses, expense categories, projects, time entries and services, with pagination and raw FreshBooks filter params.
Generic long tail:
freshbooks_list_records/freshbooks_get_recordcover taxes, credit notes, invoice profiles, tasks, staff, gateways, bills, bill vendors, bill payments and other income.Write (confirmation-gated): create clients, invoices, projects, expenses and time entries; update invoices and estimates; record payments; accept, email or update estimates.
Explicit non-capability:
freshbooks_decline_estimatealways fails and explains that FreshBooks has no decline state.Safety behavior: writes require a confirmation prompt or a two-step preview + single-use
confirmToken; tokens expire, are refused if arguments change, and invoice/estimate tools guard against sending mail to non-client recipients.
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@freshbooks-mcp List my overdue invoices"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
freshbooks-mcp
MCP server for FreshBooks — invoices, clients, estimates and payments, exposed to Claude as typed tools.
This project was developed and is maintained by AI (Claude Code). Use at your own discretion.
Install
npm install -g @chrischall/freshbooks-mcpRelated MCP server: mcp-freshbooks
Setup
FreshBooks is OAuth2 only — there is no API key and no personal access token, so a one-time browser authorization is required.
Register an app at https://my.freshbooks.com/#/developer. The redirect URI must be HTTPS with no query string;
https://localhostworks and never needs to resolve.Note the Client ID and Client Secret.
Complete the one-time authorization:
From the server itself (no script): set
FRESHBOOKS_CLIENT_IDandFRESHBOOKS_CLIENT_SECRET, start it, then callfreshbooks_auth_url, open the URL it returns, approve, and pass the whole URL you land on tofreshbooks_auth_exchange. Itsstatemust match a consent URL the server issued (valid 15 minutes, single-use), so a bare code, or a redirect URL from anyone else's authorization, is refused before its code is spent. Local sessions save the tokens directly toFRESHBOOKS_TOKEN_STOREand return only a success message. Restart the local MCP server after that message. No token needs to be copied into chat or configuration. Keep the same app credentials and store path after restart.Hosted connections: mcp-host's
authFlowstill captures the refresh token from the exchange response and supplies it through its secret store.MCP_DATA_DIRidentifies this hosted environment. Do not set it for a local coding-agent connection: hosted mode intentionally returns the token. Issued states are kept as digests underMCP_DATA_DIR, so a child that is respawned while the person approves can still complete the login. The registration's flow must show the person theauthorize_urlthatfreshbooks_auth_urlreturns. That URL carries the one-timestate, and the exchange refuses any redirect without it. A static consent link typed into a step description has nostate, so every new login or repair through it fails. Carryauthorize_urlfrom thefreshbooks_auth_urlstep and show it with a step link (link: {from: 'authorize_url', hosts: ['auth.freshbooks.com']}, mcp-host 2.30.0 or later) on the step that prompts. That prompt must collect the whole redirect URL, not just its code. Make this change before a release with state checking reaches the hosted pin. Existing connections keep refreshing; only new logins and repairs depend on it.Legacy/manual setup: the scripts in
skills/freshbooks-curlprint tokens. Run them only in a private terminal, never in an agent's captured terminal.
Configure:
FRESHBOOKS_CLIENT_ID=...
FRESHBOOKS_CLIENT_SECRET=...
FRESHBOOKS_REFRESH_TOKEN=... # optional locally; if set during reauth, keep it set
FRESHBOOKS_REDIRECT_URI=https://localhost # optional; must match what you registered
FRESHBOOKS_TOKEN_STORE=~/.freshbooks-mcp/session.json # optional
FRESHBOOKS_BUSINESS_ID=... # optional; required for writes if you belong to several businesses
FRESHBOOKS_ACCOUNT_ID=... # optional; with FRESHBOOKS_BUSINESS_ID, confirms its accountId when FreshBooks omits it
FRESHBOOKS_REQUEST_TIMEOUT_MS=30000 # optional; per-request deadline for API callsIn Claude Desktop, both are optional fields in the extension's settings. If FreshBooks
returns no account_id for the chosen business, invoice/expense writes are refused
(projects and time entries still work) until you set both IDs yourself.
⚠️ Refresh tokens rotate
FreshBooks issues a new refresh token on every refresh and immediately invalidates the
old one. This server persists each rotation to FRESHBOOKS_TOKEN_STORE (mode 0600)
before the refresh is considered complete, and prefers the stored token over the
environment value — the stored one has rotated past it.
The login flow also writes a small file of pending consent state digests
(mode 0600, entries expire after 15 minutes). Locally it sits beside the token
store, at <FRESHBOOKS_TOKEN_STORE>.oauth-state.json (by default
~/.freshbooks-mcp/session.json.oauth-state.json). Hosted, it is
$MCP_DATA_DIR/freshbooks-oauth-state.json. It is created on the first
freshbooks_auth_url call, even if the login is never finished, and holds no
tokens.
Two consequences worth knowing:
Do not point two tools at the same store. The MCP server and the
freshbooks-curlskill keep separate state files on purpose; sharing one makes them spend each other's tokens and locks both out.If the store is lost, re-run the bootstrap. A spent refresh token cannot be recovered.
Local sign-in without an environment token binds the private store to the app's
client ID, secret, and redirect URI. Changing those requires a new sign-in.
For existing environment-token setups, the stored rotation still takes precedence;
changing FRESHBOOKS_REFRESH_TOKEN to a new value starts a new token chain.
If you reauthorize locally while FRESHBOOKS_REFRESH_TOKEN is set, keep that same
value configured: the saved login is bound to it. To remove the variable, first
unset it and complete local sign-in again so the store is bound to the app instead.
The local authorization exchange and save use the same lock as refreshes. A refresh
writes only while it holds that lock, so it cannot overwrite a newer login after
releasing the lock.
Restart every local server using the store after reauthorization.
Tools
Tool | Purpose |
| Resolve accountId / businessId / businessUuid |
| Get the consent URL to authorize this connection |
| Complete authorization; save tokens privately in local sessions or return the refresh token to the hosted auth flow |
| Verify the OAuth credential and FreshBooks reachability; distinguishes "no credential" from "rejected" from "FreshBooks is down" |
| Browse and fetch invoices |
| Browse and fetch clients |
| Browse and fetch estimates |
| Browse and fetch payments |
| Browse and fetch catalogue items |
| Create a client — confirmation-gated |
| Create an invoice — confirmation-gated |
| Update an invoice — confirmation-gated |
| Record a payment against an invoice — confirmation-gated |
| Accept an estimate ( |
| Update an estimate's lines, notes, terms, presentation — confirmation-gated |
| Email an estimate to the client ( |
| Always fails: FreshBooks has no decline. Answers with the alternatives |
| Browse and fetch expenses |
| Categories supplying |
| Record an expense — confirmation-gated |
| Projects (businessId-keyed) |
| Create a project — confirmation-gated |
| Tracked time, with |
| Log time in seconds — confirmation-gated |
| Billable work types for projects and time entries |
| Generic accessor for the accounting long tail (taxes, credit notes, invoice profiles, tasks, staff, gateways, bills, bill vendors, bill payments, other income) |
Confirmation-gated means the tool asks you before it writes. A client that can show a
confirmation prompt (Claude Code) shows one with exactly what would be sent. Elsewhere
(claude.ai, Claude Desktop) the first call makes no network call and returns a preview of
the method, path and body that would be sent, plus a single-use confirmToken; only a
repeat call with that token and the same arguments performs the write. A token is refused
if any argument changed since the preview (DRAFT_CHANGED), if it was already used
(TOKEN_REUSED) or once it expires.
Confirmations
variable | default | |
|
| What a write does on a client that cannot show a confirmation prompt (claude.ai, Claude Desktop). |
|
|
|
|
| How long a token stays valid. |
| random per process | Signing key; set it only if tokens must survive a server restart. |
Estimate writes
Acceptance is an action on the estimate, not a status field: status (int),
display_status and ui_status are computed and read-only, and they disagree with each
other by design (a viewed estimate reads status: 3, display_status: "viewed",
ui_status: "open"). Accepting is PUT estimates/estimates/{id} with
{"estimate": {"action_accept": true}} — see
docs/FRESHBOOKS-API.md for where that shape comes from.
Accept is idempotent. An estimate already accepted (or invoiced) comes back with
changed: falseand no write is sent — acceptance cannot be undone through the API, so a repeat call must not re-fire it.There is no decline. FreshBooks' estimate statuses are draft / sent / viewed / replied / accepted / invoiced; no declined state, no
action_deny, noestimate.declinewebhook.freshbooks_decline_estimateexists only to say so and point at the alternatives, rather than leave an agent to invent a write that changes nothing.Every write returns the re-fetched estimate, plus
before/afterstate andchanged/changedFields, so success is verified against the record rather than inferred from a200.changedcovers the status fields and the fields that write actually set, so a successful notes edit reportschanged: trueeven though no status moves. Onfreshbooks_send_estimateit describes the record only — emailing an already-sent estimate moves nothing, and retrying onchanged: falsewould send the client a second copy.
Writes require an owner/admin accounting account
FreshBooks separates the role you hold on a business from the role you hold on an
accounting account. You can own a business that has no accounting account
(account_id: null) while being only a client on the account you can actually see —
in which case reads succeed and every write returns 403 Permission Denied, even though
your OAuth token carries all the :write scopes.
freshbooks_get_identity reports accountRole and businessRole so this is visible up
front. If accountRole is client, the invoicing write tools will not work against that
account — that is an account permission, not a configuration problem.
Two things the API reports misleadingly
totalcounts records you may not be able to read. Expenses reportedtotal: 16while returning zero rows. List results attach anotewhen that happens, so it reads as a permission boundary rather than an empty account.Projects and time tracking are keyed by
businessId, notaccountId, and paginate under ametablock instead of flatpage/pages/total. They also work on a business with no accounting account at all.
The three identifiers
FreshBooks hands out three non-interchangeable ids, and using the wrong one returns a bare 404 that reads like a missing record:
Identifier | Used by |
|
|
|
|
|
|
Call freshbooks_get_identity first. Full API notes, including the four different error
envelopes, are in docs/FRESHBOOKS-API.md.
Shell access without the server
skills/freshbooks-curl covers the same API from a
shell with curl + jq, including the OAuth bootstrap and rotation-safe token handling.
Development
npm install
npm run build
npm testLicense
MIT
Available Tools
34 toolsfreshbooks_accept_estimateADestructiveIdempotent
Accept an estimate on behalf of the account, by sending FreshBooks' action_accept on the estimate (PUT estimates/estimates/{id} with {"estimate": {"action_accept": true}}). Acceptance is not reversible through the API — there is no un-accept action. Asks the user to confirm first: a confirmation prompt where the client supports one; otherwise the first call returns a preview and a confirmToken and makes no network call, and only a repeat call with that token proceeds (see MCP_CONFIRM_MODE). Idempotent: an estimate already accepted or invoiced is returned unchanged with changed: false and no write is sent. Returns the re-fetched estimate.
Inputs: id (required), confirmToken.
freshbooks_auth_exchangeADestructive
Complete FreshBooks authorization. Local sessions save the credentials privately and return only status; hosted sessions return the refresh token to the host's credential-capture flow. Pass the whole redirect URL: its state must match a consent URL freshbooks_auth_url issued, so a bare code is refused. The code is SINGLE-USE — if this fails, authorize again rather than retrying it.
Inputs: code (required).
freshbooks_auth_urlARead-only
Get the FreshBooks consent URL to authorize this connection. Open it, approve, and you'll land on the redirect URL — pass that whole URL to freshbooks_auth_exchange (its state parameter binds the approval to this server; valid 15 minutes, single-use). Read-only; contacts nothing.
Inputs: no named parameters.
freshbooks_create_clientADestructive
Create a client (customer) in FreshBooks. Asks the user to confirm first: a confirmation prompt where the client supports one; otherwise the first call returns a preview and a confirmToken and makes no network call, and only a repeat call with that token proceeds (see MCP_CONFIRM_MODE).
Inputs: email, fname, lname, fields, +3 more.
freshbooks_create_expenseADestructive
Record an expense. Asks the user to confirm first: a confirmation prompt where the client supports one; otherwise the first call returns a preview and a confirmToken and makes no network call, and only a repeat call with that token proceeds (see MCP_CONFIRM_MODE).
Inputs: amount (required), date, notes, fields, +5 more.
freshbooks_create_invoiceADestructive
Create an invoice for a client. Created invoices start as drafts. Asks the user to confirm first: a confirmation prompt where the client supports one; otherwise the first call returns a preview and a confirmToken and makes no network call, and only a repeat call with that token proceeds (see MCP_CONFIRM_MODE).
Inputs: customerid (required), lines, notes, fields, +5 more.
freshbooks_create_projectADestructive
Create a project. Asks the user to confirm first: a confirmation prompt where the client supports one; otherwise the first call returns a preview and a confirmToken and makes no network call, and only a repeat call with that token proceeds (see MCP_CONFIRM_MODE).
Inputs: title (required), rate, fields, due_date, +5 more.
freshbooks_create_time_entryADestructive
Log a time entry. Duration is in SECONDS. Asks the user to confirm first: a confirmation prompt where the client supports one; otherwise the first call returns a preview and a confirmToken and makes no network call, and only a repeat call with that token proceeds (see MCP_CONFIRM_MODE).
Inputs: duration (required), started_at (required), note, billable, +4 more.
freshbooks_decline_estimateARead-only
NOT SUPPORTED by the FreshBooks API — this tool always fails, and never sends a request. FreshBooks models no declined state for estimates: the status set is draft / sent / viewed / replied / accepted / invoiced, there is no action_deny or action_decline counterpart to action_accept in FreshBooks' own API collection, and there is no estimate.decline webhook event. The tool exists so this answers with the reason instead of a plausible-looking write that changes nothing. Call it to get the alternatives.
Inputs: id (required).
freshbooks_get_clientBRead-only
Get a single client by its numeric FreshBooks id.
Inputs: id (required).
freshbooks_get_estimateBRead-only
Get a single estimate by its numeric FreshBooks id.
Inputs: id (required).
freshbooks_get_expenseARead-only
Get a single expense by its numeric id.
Inputs: id (required).
freshbooks_get_identityARead-only
Resolve the authenticated FreshBooks user and their identifiers: accountId (alphanumeric, used by accounting and payments endpoints), businessId (integer, used by projects and time tracking) and businessUuid. The three are not interchangeable — using the wrong one returns a 404 rather than a useful error. When the identity belongs to several businesses, businesses lists them all; unless FRESHBOOKS_BUSINESS_ID picks one, writes are refused.
Inputs: no named parameters.
freshbooks_get_invoiceARead-only
Get a single invoice by its numeric FreshBooks id.
Inputs: id (required).
freshbooks_get_itemBRead-only
Get a single item by its numeric FreshBooks id.
Inputs: id (required).
freshbooks_get_paymentBRead-only
Get a single payment by its numeric FreshBooks id.
Inputs: id (required).
freshbooks_get_projectBRead-only
Get a single project by its numeric id.
Inputs: id (required).
freshbooks_get_recordBRead-only
Get a single record from any FreshBooks accounting resource by name and id. Ids are numeric on every resource mapped here; the schema also accepts a string so an id carried around as text still works.
Inputs: id (required), resource (required).
freshbooks_healthcheckVerify credentials and upstream reachabilityARead-onlyIdempotent
Resolves the credential the way real tools do, then makes one authenticated request to api.freshbooks.com. Reports which source supplied the credential, whether api.freshbooks.com accepted it, the round-trip time, and a plain-English hint distinguishing 'no credential' from 'credential rejected' from 'a api.freshbooks.com-side problem'. Read-only; never returns the credential itself. Call this when a real tool fails and you want to know which hop broke.
Inputs: no named parameters.
freshbooks_list_clientsARead-only
List clients for the authenticated FreshBooks account. Supports pagination and raw FreshBooks filter params. Returns items plus page/pages/total.
Inputs: page, search, per_page.
freshbooks_list_estimatesARead-only
List estimates for the authenticated FreshBooks account. Supports pagination and raw FreshBooks filter params. Returns items plus page/pages/total.
Inputs: page, search, per_page.
freshbooks_list_expense_categoriesBRead-only
List expense categories, which supply the categoryid used when creating an expense.
Inputs: page, per_page.
freshbooks_list_expensesARead-only
List expenses for the account, newest-first by default. Supports pagination and raw FreshBooks filters (e.g. {"search[categoryid]": 5}). If the response reports a total with no rows, the count includes records this identity cannot read — that is reported in the note field rather than looking like an empty account.
Inputs: page, search, per_page.
freshbooks_list_invoicesARead-only
List invoices for the authenticated FreshBooks account — what clients owe, with each invoice's due date and status. Supports pagination and raw FreshBooks filter params. Returns items plus page/pages/total; view "brief" returns one line per invoice instead.
Inputs: page, view, search, per_page.
freshbooks_list_itemsARead-only
List items for the authenticated FreshBooks account. Supports pagination and raw FreshBooks filter params. Returns items plus page/pages/total.
Inputs: page, search, per_page.
freshbooks_list_paymentsARead-only
List payments for the authenticated FreshBooks account. Supports pagination and raw FreshBooks filter params. Returns items plus page/pages/total.
Inputs: page, search, per_page.
freshbooks_list_projectsBRead-only
List projects for the business. Uses businessId (not accountId) and works even when the business has no accounting account.
Inputs: page, per_page.
freshbooks_list_recordsARead-only
List any FreshBooks accounting resource by name — the generic reader covering the long tail (taxes, credit notes, invoice profiles, tasks, staff, gateways, bills, bill vendors, bill payments, other income, expense categories) alongside the ones with dedicated tools. Returns items plus page/pages/total. Some resources are gated by plan or account role and will report that rather than returning rows.
Inputs: resource (required), page, search, per_page.
freshbooks_list_servicesBRead-only
List services (the billable work types available to projects and time entries).
Inputs: page, per_page.
freshbooks_list_time_entriesBRead-only
List tracked time entries for the business. The response envelope also carries total_logged and total_unbilled alongside the rows.
Inputs: page, per_page.
freshbooks_record_paymentADestructive
Record a payment against an invoice. Asks the user to confirm first: a confirmation prompt where the client supports one; otherwise the first call returns a preview and a confirmToken and makes no network call, and only a repeat call with that token proceeds (see MCP_CONFIRM_MODE).
Inputs: amount (required), invoiceid (required), date, note, +2 more.
freshbooks_send_estimateADestructive
Email an estimate to the client, by sending FreshBooks' action_email on the estimate (PUT estimates/estimates/{id} with {"estimate": {"action_email": true, …}}). This puts mail in a client's inbox. Asks the user to confirm first: a confirmation prompt where the client supports one; otherwise the first call returns a preview and a confirmToken and makes no network call, and only a repeat call with that token proceeds (see MCP_CONFIRM_MODE). Omitting email_recipients lets FreshBooks use the client's own address. email_recipients must be addresses on the estimate's client record unless allow_non_client_recipients is set, which is only for addresses the user named themselves. Returns the re-fetched estimate.
Inputs: id (required), body, subject, confirmToken, +2 more.
freshbooks_update_estimateADestructive
Update an existing estimate. Only the supplied fields are sent; anything omitted is left alone. Asks the user to confirm first: a confirmation prompt where the client supports one; otherwise the first call returns a preview and a confirmToken and makes no network call, and only a repeat call with that token proceeds (see MCP_CONFIRM_MODE). Supplying lines REPLACES the whole line set — include each existing line's lineid to keep it. Returns the re-fetched estimate.
Inputs: id (required), lines, notes, terms, +10 more.
freshbooks_update_invoiceADestructive
Update an existing invoice. Only the supplied fields are sent. Asks the user to confirm first: a confirmation prompt where the client supports one; otherwise the first call returns a preview and a confirmToken and makes no network call, and only a repeat call with that token proceeds (see MCP_CONFIRM_MODE). Note that changing an invoice out of draft can email it to the client. email_recipients in fields must be addresses on the invoice's client record unless allow_non_client_recipients is set, which is only for addresses the user named themselves.
Inputs: id (required), fields (required), confirmToken, allow_non_client_recipients.
Tool Schema Changelog
Recent tool additions, removals, and schema changes observed during successful MCP inspections.
Loading tool schema changelog…
TDQS
Scored across 34 tools
Each dedicated tool targets a distinct resource+action, and the freshbooks_ prefix plus resource noun makes per-resource selection straightforward. The main fuzziness comes from the generic get_record/list_records overlapping with the dedicated getters/lists, and from decline_estimate being a deliberately failing tool, but the descriptions call both out explicitly.
Every tool follows the same freshbooks_verb_noun pattern (list_invoices, get_invoice, create_invoice, update_invoice, record_payment), with singular nouns for single-record operations and plural for collections. Even the odd-man-out (decline_estimate) fits the verb_noun convention.
34 tools is heavy for what is essentially one accounting domain, and some of the load is redundancy: generic get_record/list_records duplicate the per-resource readers, and decline_estimate exists solely to return an error. The breadth (invoices, estimates, clients, payments, items, expenses, projects, time, services, auth) does justify a large surface, so it is borderline rather than excessive.
Read coverage is strong and the invoice/estimate lifecycles are well served (create, update, send, accept), but there is no delete anywhere and no update for clients, expenses, projects, time entries, or line-item edits beyond update_estimate. Agents can read most things via get_record/list_records, but write workflows beyond invoices and estimates will dead-end.
Maintenance
Related MCP Connectors
Free hosted MCP for invoices, estimates, clients, items and PDFs. OAuth authorization required.
MCP server for Codat — companies, connections, invoices, bills and financial statements.
Unofficial remote MCP server for Less Annoying CRM: OAuth 2.1, read-only grants, 10 tools.
Hosted MCP server with managed OAuth for 15+ toolkits: Google Workspace, Fitbit, Oura, Kalshi, etc.
Related MCP Servers
- FlicenseNot gradedqualityDmaintenanceAutomates FreshBooks invoicing and time tracking through Claude, allowing users to send invoices, list invoices, log billable hours, and get financial insights via natural language commands.2-
- AlicenseBqualityDmaintenanceProduction-grade MCP server for FreshBooks. 25 tools for invoices, clients, expenses, payments, time tracking, projects, estimates, and financial reports. OAuth2 with automatic token refresh.2513 PyPI3MIT
- FlicenseNot gradedqualityCmaintenanceAn MCP server that connects Claude to FreshBooks, enabling reading and management of invoices, clients, expenses, projects, and time entries via natural language.1-
- FlicenseNot gradedqualityDmaintenanceProvides 29 tools for Claude to query and update FreshBooks directly, including invoices, clients, estimates, expenses, payments, timesheets, and reports.-