How to Connect FreshBooks to Claude
An invoice is a financial record, not a database row. How to wire FreshBooks to Claude so the agent drafts and a human issues, plus the OAuth refresh bug that logs everyone out.
Most API writes are recoverable. You set a field wrong, you set it back. Invoices are not like that. An invoice is a numbered financial record that your accountant reconciles, your customer may already have received, and your tax filing eventually depends on. Deleting one can leave a gap in a sequence that someone will ask you about in nine months.
So the right shape for a FreshBooks agent is almost always: the agent drafts, a human issues. Everything below is built around that.
The OAuth detail that will bite you
FreshBooks uses OAuth 2.0, and two of its properties interact badly with agents.
Access tokens expire after 12 hours (expires_in is 43200). Fine on its own. The problem is the refresh token: it does not expire, but it is single use. Every refresh returns a brand new refresh token and immediately invalidates the old one.
Now picture two agent workers running the same job. Both notice the access token is stale. Both refresh with the same stored refresh token. One wins and gets a fresh pair. The other gets rejected, and worse, the token it was holding is now dead too. Your integration is logged out and someone has to reconnect by hand.
This is a genuinely common production incident with FreshBooks and it is not a FreshBooks bug. It is what happens when token rotation meets concurrency. You need the refresh to happen in exactly one place, with the new token written back before anything else reads it.
That is one reason to not hand OAuth to your agent code at all. Connect once through the CLI and the token lifecycle, including rotation, is handled outside your project in a single credential store, injected at call time. Details in the authentication docs.
accountId and businessId are different, and you need both
After connecting, call GET /auth/api/v1/users/me and look at business_memberships. Each entry gives you two identifiers that are easy to confuse:
business.account_idis the accountId. It looks like a short alphanumeric string and it is what/accountingendpoints take. Invoices live here.business.idis the businessId. It is a number, and it is what/timetrackingand/projectstake.
One is a string, one is a number, both are called "the ID" in conversation, and a model will absolutely swap them. If your invoice call returns a confusing 404, check which ID you sent before you check anything else.
Also worth knowing: a user can belong to several businesses. If that is true for your account, pick one deliberately rather than taking business_memberships[0] and hoping.
Setting it up
npx swytchcode
swy login
swy init --mode=sandbox
swy get freshbooks
swy auth connect freshbooks # opens the browser for OAuth, once
swy auth status
# find what you need, scoped to the provider
swy discover "create an invoice" --project freshbooks
swy discover "list time entries" --project freshbooks
swy list methods freshbooks
swy info <canonical_id> # see the inputs before allowing it
swy add method <canonical_id> # one method at a time, on purposeRun swy get before swy info, otherwise info will tell you a real method does not exist. And always pass --project to discover, because the search covers the whole catalog and other accounting products have similarly named endpoints.
Here is the allowlist we would actually ship for a billing agent:
- Read clients, read time entries, read projects, read invoices. All four, freely.
- Create an invoice. Yes, but gated by policy, and as a draft.
- Send an invoice, delete an invoice, edit a paid invoice. Not added at all. If the method is not in the allowlist, no instruction can reach it.
The invoice endpoint, and its slightly odd path
Creating an invoice is a POST to /accounting/account/{accountId}/invoices/invoices. Yes, "invoices" twice. That is correct and it catches everyone, including models that will confidently drop one of them.
Line items need at minimum a name and a unit_cost; qty defaults to 1 if you leave it out, which is a quiet way to bill for one hour instead of eleven. Have the agent always set quantity explicitly.
import { exec } from "@swytchcode/runtime";
// canonical_id from `swy list methods freshbooks`
const invoice = await exec("<freshbooks create-invoice canonical_id>", {
params: { accountId: process.env.FB_ACCOUNT_ID }, // the short string, not business.id
body: {
invoice: {
customerid: clientId,
create_date: "2026-10-23",
status: 1, // draft, not issued
lines: [
{ name: "Design retainer", unit_cost: { amount: "150.00", code: "USD" }, qty: 12 }
]
}
},
idempotencyKey: `invoice:${clientId}:${periodId}` // see below
});Check the exact body shape and status values against FreshBooks' current docs before you ship, since accounting payloads are fussier than most and the wrapper key matters. Run swy info <canonical_id> to see what the method expects.
The policy that makes this safe to ship
Two rules. Hold any invoice over a threshold for a human, and hold anything for a client the agent has not billed before, because a new payee is where mistakes get expensive.
{
"id": "invoices-over-2k",
"target": ["<freshbooks create-invoice canonical_id>"],
"when": { "field": "amount", "operator": ">", "value": 200000 },
"action": {
"type": "REQUIRES_APPROVAL",
"message": "Draft invoice over 2,000.00 needs a human before it is issued"
},
"approval_timeout": "4h"
}Amounts are in cents here, which is worth stating in a comment next to the rule because somebody will eventually read 200000 as two hundred thousand dollars. Held calls go to Slack or Telegram and fail closed: no approval inside the window means the invoice is never created. Setup is in the human approval docs, the rule schema is in the policy reference, and there is a full walkthrough in how to set up policy guardrails.
Because policies.json lives in your repo, changing the threshold is a pull request with a reviewer and a timestamp. When your accountant or auditor asks who approved raising the limit, git log is the answer. That is the whole idea behind policy-as-code.
Idempotency, or how you end up billing a client twice
The failure is not dramatic. The request reaches FreshBooks, the invoice is created, the response gets lost on the way back, and your agent thinks it failed. It retries. Now there are two invoices for the same work, with two different numbers, and your client has questions.
Swytchcode only auto-retries a POST or PATCH when an idempotency key is set, so an unprotected create fails visibly instead of silently duplicating. Build the key from the thing that makes the invoice unique in your world, not from a timestamp: client ID plus billing period works, because a retry an hour later produces the same key. The general version is in API idempotency: the complete guide and the mechanics are in the idempotency docs.
Belt and braces for accounting specifically: before creating, search for an existing invoice matching that client and period. One extra read removes the entire class of problem, and reads are cheap.
Rate limits and pagination gotchas
FreshBooks does not publish numeric rate limits. It returns 429 when you are going too fast, and it reserves the right to disable apps that hammer the API. That is a slightly uncomfortable combination for an agent, which is why backoff on 429 is automatic here and why you should cache client lists rather than refetching them in every reasoning step.
On pagination: per_page is silently capped at 100. Ask for 500 and you get 100 with no warning and no error. An agent that assumes it received everything will confidently report on a third of your invoices. Read pages from the response and keep going until you pass it.
Using it from Claude
For Claude Code, Claude Desktop, Cursor or Windsurf, connect through the MCP server and FreshBooks shows up as tools with exactly your allowlist and policies behind them. The per-client setup is in the MCP setup guide and the reference in the MCP docs. If you would rather Claude wired this up itself, hand it skills.md, which describes the commands and conventions. For the SDK route, the Anthropic SDK guide has a working tool-use loop.
One recommendation: keep the invoice-creating project separate from your general-purpose assistant. An editor session where you are debugging something is not where you want a billing method one tool call away.
What you can build
- Time entries to draft invoices. At month end, read unbilled time per project, group it, draft one invoice per client in draft status, and post a summary with links for review. The human clicks send. This is the highest-value version and it needs no send permission at all.
- An overdue chaser that drafts, not sends. Find invoices past due, draft a polite follow-up per client with the right amount and date, and put them in a review queue. Tone matters in collections, so a human reading five drafts is faster and safer than a human unpicking one bad send.
- Month-end close prep. Read invoices, expenses and payments, then produce a list of anomalies: missing client details, invoices with no line items, suspiciously round numbers, things dated in the wrong period. Pure read access, immediately useful to whoever does your books.
- Cross-system reconciliation. Compare FreshBooks records against payments in Stripe and flag what does not match. Two read-only credentials, one genuinely tedious job nobody enjoys.
FAQ
Should the agent ever be allowed to send an invoice?
For most teams, no, and that is not a limitation worth fighting. Drafting is where the time goes; clicking send takes two seconds and is where the judgement is. If you do automate sending, gate every one behind approval rather than using a threshold, because the embarrassing invoice is rarely the big one.
Our integration keeps getting logged out. Why?
Almost certainly the single-use refresh token being used twice by two processes, or an old copy of the refresh token being retried after a successful refresh already rotated it. Refresh in one place only, write the new token back immediately, and do not let two workers hold the same credential independently.
Can I keep credentials on our own infrastructure?
Yes. The execution kernel runs on your machine or your infrastructure, requests go straight from there to FreshBooks with no proxy hop, and credentials stay local. Enterprise deployments run in your own AWS, Azure or GCP account with a region of your choice. That is usually the answer finance teams need; see pricing for what sits on which tier.
How do I prove to an auditor what the agent did?
swy audit network lists every call that ran, swy audit policy lists every one that was held or denied, and the rule that did it is in version control with an approval history. What auditors actually accept is covered in API compliance for AI agent integrations.
Related reading
More content
How to Debug an AI Agent That Called the Wrong API
Working backwards from the response your agent got to the tool-selection decision that caused it, which is where most of the real debugging time goes.
iPaaS Was Built for Integrations, Not Agents
Deterministic trigger-action versus non-deterministic tool choice, and why the governance model that works for one doesn't transfer cleanly to the other.
Using the Airtable API from an AI Agent
Someone renames a field in the UI and your agent breaks with an error saying the field does not exist, while you are looking right at it. Airtable's schema drift problem, solved.
