Integrate Bolt.new with FreshBooks by creating a developer app at my.freshbooks.com/developer, implementing OAuth 2.0 through Next.js API routes (requires deployment for the callback URL), then calling FreshBooks' REST API to read clients, invoices, and time entries. FreshBooks targets freelancers and small agencies with simpler accounting than QuickBooks. Store credentials in .env and proxy all API calls server-side.
Building Freelancer Invoicing Tools with FreshBooks' REST API
FreshBooks is designed for freelancers, consultants, and small service agencies who bill clients for time and project work. Unlike QuickBooks or Xero — which target broader accounting with inventory, payroll, and complex chart of accounts — FreshBooks is purpose-built for the service economy: create an invoice, track time against a project, collect a payment, and run a simple income report. This focus makes FreshBooks API simpler and more approachable for Bolt developers building tools for this audience.
The FreshBooks API covers the core freelancing workflow: clients (the customers you bill), invoices (the bills you send), time entries (hours tracked against projects), expenses (costs you incur), projects (groupings of work), and estimates (quotes you send before invoicing). The API is RESTful with consistent patterns — resource-based URLs, HTTP verbs, JSON bodies, and predictable pagination. One unique aspect of FreshBooks API design is the account_id in the URL path: every API call includes the FreshBooks account ID (the business identifier, not the user ID) in the URL, which you obtain after OAuth.
FreshBooks targets the North American freelance market but has customers worldwide. Its time tracking integration is particularly strong — if you are building project management or time tracking tools for service businesses, integrating with FreshBooks lets users create invoices directly from their time entries without manual data re-entry. The FreshBooks API is stable and has been actively maintained since 2012, making it a reliable integration target.
Integration method
Bolt generates the FreshBooks integration code — OAuth 2.0 authorization routes, API route handlers for client and invoice data, and React dashboard components — through conversation with the AI. FreshBooks requires OAuth 2.0, so users must authorize your app through FreshBooks' consent screen. The OAuth callback needs a deployed HTTPS URL — it cannot be tested in Bolt's WebContainer preview. All FreshBooks API calls go through server-side Next.js routes to keep credentials out of the browser.
Prerequisites
- A FreshBooks account (free trial at freshbooks.com — no credit card required initially)
- A FreshBooks developer app registered at my.freshbooks.com/developer with OAuth 2.0 credentials (Client ID and Client Secret)
- A deployed Bolt.new app on Netlify or Bolt Cloud (required for the OAuth redirect URI — the callback cannot be tested in the WebContainer)
- Your FreshBooks account_id (obtained from the FreshBooks API after first OAuth authorization)
- A Next.js project in Bolt — FreshBooks does not have an official Node.js SDK so you will use the REST API directly
Step-by-step guide
Create a FreshBooks developer app and get OAuth credentials
Create a FreshBooks developer app and get OAuth credentials
FreshBooks provides a developer portal at developers.freshbooks.com where you register applications. Log into your FreshBooks account and navigate to my.freshbooks.com/developer (or developers.freshbooks.com and sign in). Create a new app by clicking Create App. Fill in the required fields: application name (visible to users during authorization), a brief description, and the redirect URI. Add your deployed URL (e.g., https://your-app.netlify.app/api/freshbooks/callback) and http://localhost:3000/api/freshbooks/callback for local development. After saving, FreshBooks shows your Client ID and Client Secret. FreshBooks OAuth 2.0 uses the standard authorization code flow. The authorization endpoint is https://auth.freshbooks.com/oauth/authorize and the token endpoint is https://api.freshbooks.com/auth/oauth/token. The scopes you can request are limited — FreshBooks provides access based on the account type rather than granular scopes, so requesting access typically grants access to the standard accounting endpoints your app needs. An important FreshBooks-specific detail: after OAuth, you receive the access token but must also call the /auth/api/v1/users/me endpoint to get the user's account_id. This account_id is a string like 'xbKPqa' and is embedded in every FreshBooks API URL path. Store it alongside your tokens. Without it, you cannot make any accounting API calls. FreshBooks access tokens expire after 12 hours (significantly longer than most platforms). Refresh tokens last 30 days. The relatively long access token lifetime means token refresh is less frequent, but you still need refresh token logic for long-running applications.
Set up FreshBooks OAuth 2.0 in my Next.js app. Create lib/freshbooks-auth.ts with a buildFreshBooksAuthUrl function that generates the auth URL pointing to https://auth.freshbooks.com/oauth/authorize with FRESHBOOKS_CLIENT_ID and FRESHBOOKS_REDIRECT_URI. Create /api/freshbooks/authorize that redirects to the auth URL. Create /api/freshbooks/callback that exchanges the code for tokens at https://api.freshbooks.com/auth/oauth/token, then calls /auth/api/v1/users/me to get the account_id, and stores access_token, refresh_token, and account_id in HTTP-only cookies.
Paste this in Bolt.new chat
1// .env.local2FRESHBOOKS_CLIENT_ID=your_freshbooks_client_id3FRESHBOOKS_CLIENT_SECRET=your_freshbooks_client_secret4FRESHBOOKS_REDIRECT_URI=https://your-app.netlify.app/api/freshbooks/callback56// lib/freshbooks-auth.ts7export const FB_API_BASE = 'https://api.freshbooks.com';8export const FB_AUTH_BASE = 'https://auth.freshbooks.com';910export function buildFreshBooksAuthUrl(): string {11 const params = new URLSearchParams({12 client_id: process.env.FRESHBOOKS_CLIENT_ID!,13 response_type: 'code',14 redirect_uri: process.env.FRESHBOOKS_REDIRECT_URI!,15 });16 return `${FB_AUTH_BASE}/oauth/authorize?${params.toString()}`;17}1819// app/api/freshbooks/callback/route.ts20import { NextRequest, NextResponse } from 'next/server';21import { FB_API_BASE, FB_AUTH_BASE } from '@/lib/freshbooks-auth';2223export async function GET(request: NextRequest) {24 const { searchParams } = new URL(request.url);25 const code = searchParams.get('code');2627 if (!code) {28 return NextResponse.redirect(new URL('/error?message=no_code', request.url));29 }3031 try {32 const tokenRes = await fetch(`${FB_API_BASE}/auth/oauth/token`, {33 method: 'POST',34 headers: { 'Content-Type': 'application/json' },35 body: JSON.stringify({36 grant_type: 'authorization_code',37 client_id: process.env.FRESHBOOKS_CLIENT_ID,38 client_secret: process.env.FRESHBOOKS_CLIENT_SECRET,39 code,40 redirect_uri: process.env.FRESHBOOKS_REDIRECT_URI,41 }),42 });43 const tokens = await tokenRes.json() as { access_token: string; refresh_token: string; token_type: string };4445 if (!tokens.access_token) throw new Error('No access token received');4647 // Get account_id from the users/me endpoint48 const meRes = await fetch(`${FB_API_BASE}/auth/api/v1/users/me`, {49 headers: { Authorization: `Bearer ${tokens.access_token}` },50 });51 const me = await meRes.json() as {52 response: { business_memberships: Array<{ business: { account_id: string; name: string } }> };53 };54 const accountId = me.response.business_memberships[0]?.business?.account_id;5556 if (!accountId) throw new Error('No FreshBooks account found');5758 const response = NextResponse.redirect(new URL('/dashboard', request.url));59 const cookieOpts = { httpOnly: true, secure: process.env.NODE_ENV === 'production', sameSite: 'lax' as const, maxAge: 60 * 60 * 24 * 30, path: '/' };60 response.cookies.set('fb_access_token', tokens.access_token, { ...cookieOpts, maxAge: 60 * 60 * 12 });61 response.cookies.set('fb_refresh_token', tokens.refresh_token, cookieOpts);62 response.cookies.set('fb_account_id', accountId, cookieOpts);6364 return response;65 } catch (error) {66 console.error('FreshBooks callback error:', error);67 return NextResponse.redirect(new URL('/error?message=freshbooks_auth_failed', request.url));68 }69}Pro tip: FreshBooks business_memberships in the /users/me response may contain multiple businesses if the user has more than one FreshBooks account. Store all account IDs and names so multi-business users can switch between them in your app.
Expected result: After authorizing on FreshBooks' consent screen and redirecting back, fb_access_token, fb_refresh_token, and fb_account_id cookies are set. The account_id is a 6-character string like 'xbKPqa' that you will embed in every API URL.
Build a FreshBooks API utility and fetch invoices
Build a FreshBooks API utility and fetch invoices
FreshBooks API URLs embed the account_id in the path: https://api.freshbooks.com/accounting/account/{accountId}/invoices/invoices. This pattern applies to all accounting resources. The API also wraps all responses in a consistent JSON envelope: the data you want is at response.result.invoices (or response.result.invoice for single items). Create a utility function that reads the stored account_id and access token from cookies, automatically constructs the correct URL prefix, and adds the Authorization header. This utility should also handle token refresh — FreshBooks tokens expire after 12 hours, so long-running server sessions need refresh logic. FreshBooks invoice resources include: invoiceid, number (invoice number), customerid, create_date, due_offset_days, amount (total amount), outstanding (unpaid balance), currentorganization (client name), and status (values are: 1=draft, 2=sent, 4=viewed, 5=outstanding, 6=overdue, 7=disputed, 8=paid, 9=auto-paid). Note that status is a numeric code, not a string — map it to human-readable labels in your API route. FreshBooks also supports filtering invoices by date range, client, and status. The query parameters are date_min, date_max, and include[]=late_reminders. Pagination uses page and per_page parameters (max 100 per page). The response includes total_count and pages to support full pagination.
Create lib/freshbooks-api.ts with a freshBooksCall function that reads fb_access_token and fb_account_id from cookies, auto-prefixes the accounting URL base https://api.freshbooks.com/accounting/account/{accountId}, adds the Bearer auth header, and extracts the response.result data. Then create /api/freshbooks/invoices that calls GET /invoices/invoices with per_page=100 and returns normalized invoices: id, number, clientName, createDate, amount, outstanding, statusLabel (mapped from numeric status), and isPaid (boolean).
Paste this in Bolt.new chat
1// lib/freshbooks-api.ts2import { cookies } from 'next/headers';3import { FB_API_BASE } from './freshbooks-auth';45const STATUS_MAP: Record<number, string> = {6 1: 'Draft',7 2: 'Sent',8 4: 'Viewed',9 5: 'Outstanding',10 6: 'Overdue',11 7: 'Disputed',12 8: 'Paid',13 9: 'Auto-Paid',14};1516export { STATUS_MAP };1718export async function freshBooksCall<T>(19 path: string,20 options: RequestInit = {}21): Promise<T> {22 const cookieStore = cookies();23 const accessToken = cookieStore.get('fb_access_token')?.value;24 const accountId = cookieStore.get('fb_account_id')?.value;2526 if (!accessToken || !accountId) {27 throw new Error('Not authenticated with FreshBooks');28 }2930 const url = path.startsWith('http')31 ? path32 : `${FB_API_BASE}/accounting/account/${accountId}${path}`;3334 const response = await fetch(url, {35 ...options,36 headers: {37 Authorization: `Bearer ${accessToken}`,38 'Content-Type': 'application/json',39 'Api-Version': 'alpha',40 ...(options.headers ?? {}),41 },42 });4344 if (!response.ok) {45 throw new Error(`FreshBooks API error: ${response.status}`);46 }4748 const data = await response.json() as { response: { result: T } };49 return data.response.result;50}5152// app/api/freshbooks/invoices/route.ts53import { NextRequest, NextResponse } from 'next/server';54import { freshBooksCall, STATUS_MAP } from '@/lib/freshbooks-api';5556interface FBInvoice {57 invoiceid: number;58 number: string;59 currentorganization: string;60 create_date: string;61 due_date: string;62 amount: { amount: string; code: string };63 outstanding: { amount: string; code: string };64 v3_status: string;65 status: number;66}6768interface FBInvoicesResponse {69 invoices: FBInvoice[];70 total_count: number;71 pages: number;72}7374export async function GET(request: NextRequest) {75 const { searchParams } = new URL(request.url);76 const page = searchParams.get('page') ?? '1';7778 try {79 const data = await freshBooksCall<FBInvoicesResponse>(80 `/invoices/invoices?per_page=100&page=${page}&include[]=late_reminders`81 );8283 const invoices = data.invoices.map((inv) => ({84 id: inv.invoiceid,85 number: inv.number,86 clientName: inv.currentorganization,87 createDate: inv.create_date,88 dueDate: inv.due_date,89 total: parseFloat(inv.amount.amount),90 outstanding: parseFloat(inv.outstanding.amount),91 currency: inv.amount.code,92 status: inv.status,93 statusLabel: STATUS_MAP[inv.status] ?? 'Unknown',94 isPaid: inv.status === 8 || inv.status === 9,95 }));9697 const totalOutstanding = invoices.filter((i) => !i.isPaid).reduce((s, i) => s + i.outstanding, 0);9899 return NextResponse.json({ invoices, totalOutstanding, totalCount: data.total_count, pages: data.pages });100 } catch (error) {101 const message = error instanceof Error ? error.message : 'Failed to fetch invoices';102 return NextResponse.json({ error: message }, { status: 500 });103 }104}Pro tip: FreshBooks wraps all responses in response.result — watch for this double nesting. Invoices are at response.result.invoices, a single invoice is at response.result.invoice, and clients are at response.result.clients. If you see unexpected empty responses, check you are accessing the right nesting level.
Expected result: Calling /api/freshbooks/invoices returns your FreshBooks invoices with human-readable status labels, formatted amounts, and a totalOutstanding sum for the outstanding balance dashboard metric.
Fetch time entries and clients for a complete dashboard
Fetch time entries and clients for a complete dashboard
Beyond invoices, FreshBooks time entries and client data are the most useful resources for freelancer tools. Time entries (at /time_tracking/time_entries) record hours worked against projects, with the duration in seconds, the associated project and client, hourly rate, and whether the entry is billable and already invoiced. For a time-to-invoice workflow, filter for time entries where billed=false (not yet invoiced) and billable=true (marked as billable). Group these by client to show the total unbilled hours per client. When creating an invoice, collect the relevant entry IDs and create a FreshBooks invoice with line items corresponding to those hours. FreshBooks client data (at /contacts/clients) provides the customer name, email, business phone, billing address, and outstanding balance. The outstanding field shows the total unpaid invoice balance for that client — useful for an accounts receivable overview without needing to sum invoices manually. A practical limitation to be aware of: FreshBooks time tracking uses a different URL pattern than accounting resources. Time entries are at https://api.freshbooks.com/timetracking/business/{businessId}/time_entries — note 'timetracking' not 'accounting', and 'businessId' not 'accountId'. The business ID is also returned in the /users/me response. Store both the account_id (for accounting APIs) and the business_id (for time tracking APIs) after OAuth. All these API calls are outbound HTTP from your Next.js server routes — they work in Bolt's WebContainer preview without deployment. Only OAuth callback needs deployment.
Create two more FreshBooks API routes. First: /api/freshbooks/clients that fetches all clients using GET /contacts/clients and returns id, name, email, phone, outstanding balance, and currency. Second: /api/freshbooks/time-entries that fetches unbilled billable time entries using the timetracking API. For time entries, return: id, client name, project name, duration in hours (seconds/3600), date, hourly rate, and calculated amount. Also create a summary endpoint /api/freshbooks/summary that calls both invoices and clients in parallel and returns total outstanding, overdue count, top 5 clients by balance, and total unbilled hours.
Paste this in Bolt.new chat
1// app/api/freshbooks/time-entries/route.ts2import { NextRequest, NextResponse } from 'next/server';3import { cookies } from 'next/headers';4import { FB_API_BASE } from '@/lib/freshbooks-auth';56export async function GET(request: NextRequest) {7 const cookieStore = cookies();8 const accessToken = cookieStore.get('fb_access_token')?.value;9 // businessId is stored separately from accountId10 const businessId = cookieStore.get('fb_business_id')?.value;1112 if (!accessToken || !businessId) {13 return NextResponse.json({ error: 'Not authenticated' }, { status: 401 });14 }1516 const { searchParams } = new URL(request.url);17 const billedFilter = searchParams.get('billed') ?? 'false';1819 try {20 const response = await fetch(21 `${FB_API_BASE}/timetracking/business/${businessId}/time_entries?billed=${billedFilter}&billable=true&per_page=200`,22 { headers: { Authorization: `Bearer ${accessToken}`, 'Api-Version': 'alpha' } }23 );2425 if (!response.ok) throw new Error(`FreshBooks time API error: ${response.status}`);2627 const data = await response.json() as {28 time_entries: Array<{29 id: number;30 client_id: number;31 project_id: number;32 duration: number;33 started_at: string;34 hourly_rate: string | null;35 billed: boolean;36 billable: boolean;37 note: string;38 }>;39 };4041 const entries = data.time_entries.map((e) => ({42 id: e.id,43 clientId: e.client_id,44 projectId: e.project_id,45 durationHours: Math.round((e.duration / 3600) * 100) / 100,46 date: e.started_at?.split('T')[0],47 hourlyRate: e.hourly_rate ? parseFloat(e.hourly_rate) : null,48 amount: e.hourly_rate ? Math.round((e.duration / 3600) * parseFloat(e.hourly_rate) * 100) / 100 : null,49 note: e.note,50 billed: e.billed,51 billable: e.billable,52 }));5354 const totalUnbilledHours = entries.filter((e) => !e.billed).reduce((s, e) => s + e.durationHours, 0);55 const totalUnbilledAmount = entries.filter((e) => !e.billed && e.amount).reduce((s, e) => s + (e.amount ?? 0), 0);5657 return NextResponse.json({58 entries,59 totalEntries: entries.length,60 totalUnbilledHours: Math.round(totalUnbilledHours * 100) / 100,61 totalUnbilledAmount: Math.round(totalUnbilledAmount * 100) / 100,62 });63 } catch (error) {64 const message = error instanceof Error ? error.message : 'Failed to fetch time entries';65 return NextResponse.json({ error: message }, { status: 500 });66 }67}Pro tip: FreshBooks time entry durations are in seconds. Divide by 3600 to get hours. When displaying to users, round to 2 decimal places — 7200 seconds is 2.00 hours, not 2.000000000001 from floating point arithmetic. Use Math.round(hours * 100) / 100 for clean output.
Expected result: Calling /api/freshbooks/time-entries returns unbilled time entries with duration in hours, calculated amounts, and summary totals for total unbilled hours and amount owed.
Build a freelancer invoicing dashboard UI
Build a freelancer invoicing dashboard UI
With the FreshBooks API routes ready, build the React dashboard that presents the core freelancer accounting view: outstanding invoices, top clients, unbilled time, and quick actions. This dashboard covers the daily workflow of checking what is owed, what is overdue, and what billable hours have not been invoiced yet. Design the dashboard around three sections: the financial overview at the top (total outstanding, overdue count, unbilled time value), the invoice list in the middle (sortable by due date, filterable by status), and the unbilled time entries at the bottom (grouped by client, with a create-invoice action). For status color coding: grey for draft, blue for sent/viewed, orange for overdue, green for paid. This visual language matches what FreshBooks users expect. FreshBooks status codes are numeric (1-9), so map them in your display logic. The invoice table should include a View in FreshBooks link using the invoice ID. The FreshBooks web URL pattern for invoices is https://my.freshbooks.com/#/invoice/{invoiceId} — construct this link from the invoice ID returned by the API. All data fetching works in the Bolt WebContainer preview — your API routes make outbound calls to FreshBooks which is supported. Test the complete data pipeline in preview before deploying, then deploy to Netlify or Bolt Cloud for the production version accessible to real users.
Build a FreshBooksDashboard React component. Fetch from /api/freshbooks/invoices and /api/freshbooks/time-entries in parallel on mount. Show summary cards: Total Outstanding ($), Overdue Invoices (count), Unbilled Hours, Unbilled Amount. Below, show the invoice table with Invoice Number, Client, Date, Amount, Status badge (color-coded), and a View link to FreshBooks. Add a status filter. Show unbilled time entries grouped by client with total hours and amount, plus a 'Create Invoice' button per client. Handle loading and error states.
Paste this in Bolt.new chat
1// components/FreshBooksDashboard.tsx2'use client';34import { useEffect, useState } from 'react';56interface Invoice {7 id: number;8 number: string;9 clientName: string;10 createDate: string;11 dueDate: string;12 total: number;13 outstanding: number;14 currency: string;15 status: number;16 statusLabel: string;17 isPaid: boolean;18}1920const STATUS_COLORS: Record<number, string> = {21 1: 'bg-gray-100 text-gray-600',22 2: 'bg-blue-100 text-blue-700',23 4: 'bg-blue-100 text-blue-700',24 5: 'bg-yellow-100 text-yellow-700',25 6: 'bg-red-100 text-red-700',26 8: 'bg-green-100 text-green-700',27 9: 'bg-green-100 text-green-700',28};2930export default function FreshBooksDashboard() {31 const [invoices, setInvoices] = useState<Invoice[]>([]);32 const [unbilledHours, setUnbilledHours] = useState(0);33 const [unbilledAmount, setUnbilledAmount] = useState(0);34 const [loading, setLoading] = useState(true);35 const [statusFilter, setStatusFilter] = useState<number | null>(null);36 const [error, setError] = useState('');3738 useEffect(() => {39 Promise.all([40 fetch('/api/freshbooks/invoices').then((r) => r.json()),41 fetch('/api/freshbooks/time-entries').then((r) => r.json()),42 ]).then(([invData, timeData]) => {43 if (invData.error) throw new Error(invData.error);44 setInvoices(invData.invoices ?? []);45 setUnbilledHours(timeData.totalUnbilledHours ?? 0);46 setUnbilledAmount(timeData.totalUnbilledAmount ?? 0);47 }).catch((e) => setError(e.message)).finally(() => setLoading(false));48 }, []);4950 const totalOutstanding = invoices.filter((i) => !i.isPaid).reduce((s, i) => s + i.outstanding, 0);51 const overdueCount = invoices.filter((i) => i.status === 6).length;5253 const filtered = statusFilter ? invoices.filter((i) => i.status === statusFilter) : invoices;5455 if (loading) return <div className="p-8 text-center">Loading FreshBooks data...</div>;56 if (error) return <div className="p-8 text-red-600">Error: {error}</div>;5758 return (59 <div className="p-6 max-w-5xl mx-auto">60 <h1 className="text-2xl font-bold mb-6">FreshBooks Dashboard</h1>6162 <div className="grid grid-cols-2 md:grid-cols-4 gap-4 mb-8">63 {[{label: 'Outstanding', value: `$${totalOutstanding.toFixed(2)}`, color: 'text-blue-600'},64 {label: 'Overdue', value: String(overdueCount), color: 'text-red-600'},65 {label: 'Unbilled Hours', value: `${unbilledHours}h`, color: 'text-yellow-600'},66 {label: 'Unbilled Value', value: `$${unbilledAmount.toFixed(2)}`, color: 'text-purple-600'},67 ].map(({label, value, color}) => (68 <div key={label} className="border rounded-lg p-4">69 <p className="text-sm text-gray-500">{label}</p>70 <p className={`text-xl font-bold ${color}`}>{value}</p>71 </div>72 ))}73 </div>7475 <div className="flex gap-2 mb-4 flex-wrap">76 <button onClick={() => setStatusFilter(null)} className={`px-3 py-1 rounded text-sm ${!statusFilter ? 'bg-blue-600 text-white' : 'bg-gray-100'}`}>All</button>77 {[{status: 2, label: 'Sent'}, {status: 6, label: 'Overdue'}, {status: 8, label: 'Paid'}, {status: 1, label: 'Draft'}].map(({status, label}) => (78 <button key={status} onClick={() => setStatusFilter(status)}79 className={`px-3 py-1 rounded text-sm ${statusFilter === status ? 'bg-blue-600 text-white' : 'bg-gray-100'}`}>{label}</button>80 ))}81 </div>8283 <div className="overflow-x-auto">84 <table className="w-full text-sm">85 <thead>86 <tr className="border-b">87 <th className="text-left py-2 font-medium">Invoice #</th>88 <th className="text-left py-2 font-medium">Client</th>89 <th className="text-left py-2 font-medium">Date</th>90 <th className="text-right py-2 font-medium">Amount</th>91 <th className="text-left py-2 font-medium">Status</th>92 <th className="text-left py-2 font-medium">View</th>93 </tr>94 </thead>95 <tbody>96 {filtered.map((inv) => (97 <tr key={inv.id} className="border-b hover:bg-gray-50">98 <td className="py-3">{inv.number}</td>99 <td className="py-3">{inv.clientName}</td>100 <td className="py-3">{inv.createDate}</td>101 <td className="py-3 text-right">{inv.currency} {inv.total.toFixed(2)}</td>102 <td className="py-3">103 <span className={`px-2 py-1 rounded-full text-xs ${STATUS_COLORS[inv.status] ?? 'bg-gray-100'}`}>104 {inv.statusLabel}105 </span>106 </td>107 <td className="py-3">108 <a href={`https://my.freshbooks.com/#/invoice/${inv.id}`} target="_blank" rel="noopener noreferrer"109 className="text-blue-500 hover:underline text-xs">Open →</a>110 </td>111 </tr>112 ))}113 </tbody>114 </table>115 </div>116 </div>117 );118}Pro tip: FreshBooks invoice IDs in the API response are numeric integers (not UUIDs). The web URL uses these IDs directly: https://my.freshbooks.com/#/invoice/{invoiceId}. This pattern is reliable for all FreshBooks accounts regardless of region.
Expected result: The Bolt preview shows a FreshBooks dashboard with summary cards, a filterable invoice table with colored status badges, and unbilled time entry totals. All data loads from your live FreshBooks account.
Common use cases
Freelancer invoice dashboard with quick-send
Build a clean invoice management view showing all outstanding invoices, their amounts, and due dates. Include a one-click Send button that marks draft invoices as sent and triggers FreshBooks to email them to the client. Simpler than navigating FreshBooks' full interface for daily invoicing tasks.
Create a Next.js app with a FreshBooks integration. Build an invoice dashboard that fetches all invoices using the FreshBooks API, displays them with status badges (draft, sent, viewed, partial, paid, overdue), shows total outstanding balance, and has a Send Invoice button for draft invoices. Use the FreshBooks account ID from the OAuth response. Store FRESHBOOKS_CLIENT_ID and FRESHBOOKS_CLIENT_SECRET in .env.
Copy this prompt to try it in Bolt.new
Time entries to invoice conversion
Show all uninvoiced time entries grouped by client and project. Let the user select which time entries to include and create a new invoice with those hours at the client's billing rate. This automates the most tedious part of freelance billing.
Build a time-to-invoice converter using the FreshBooks API. Fetch all unbilled time entries grouped by client using the time_entries API endpoint. Display them with total hours and calculated amounts based on hourly rate. When the user clicks 'Create Invoice', call the FreshBooks invoices API to create an invoice for that client with line items for each time entry. Return the created invoice ID.
Copy this prompt to try it in Bolt.new
Client portal with invoice history
Create a simple client-facing page where customers can view their invoice history, see payment status, and download PDFs — a lighter alternative to giving clients direct FreshBooks access. Your app acts as a read-only client portal powered by FreshBooks data.
Build a client invoice portal using FreshBooks API. After a client logs in with their email, find their FreshBooks client record using the clients API search. Fetch all invoices for that client ID and display them: invoice number, date, amount, status, and a Download PDF button. PDF download should call the FreshBooks invoice PDF endpoint and return the file. Show total paid and outstanding amounts.
Copy this prompt to try it in Bolt.new
Troubleshooting
FreshBooks OAuth callback never fires — browser shows 404 or blank page after authorizing
Cause: The OAuth flow is being tested in Bolt's WebContainer preview. FreshBooks redirects to your registered callback URL after authorization, but the WebContainer cannot receive incoming HTTP redirects from external services.
Solution: Deploy your app to Netlify or Bolt Cloud. Register the deployed HTTPS URL (e.g., https://your-app.netlify.app/api/freshbooks/callback) in your FreshBooks developer app's redirect URIs. Set FRESHBOOKS_REDIRECT_URI to the deployed URL. Test the full auth flow on the deployed app, not in the Bolt preview.
API calls return 401 Unauthorized even immediately after completing OAuth
Cause: The fb_access_token cookie is not being set correctly, or the token was not included in the Authorization header. FreshBooks requires the header format 'Bearer {token}' with uppercase B.
Solution: Confirm the fb_access_token cookie is set by checking browser DevTools → Application → Cookies after the OAuth callback. Verify the Authorization header format is exactly 'Bearer {token}'. Also check that the Api-Version: alpha header is included — FreshBooks requires it for many endpoints.
1// Correct Authorization header format:2headers: {3 'Authorization': `Bearer ${accessToken}`,4 'Api-Version': 'alpha',5 'Content-Type': 'application/json',6}Invoice amounts show as '0' or NaN in the dashboard
Cause: FreshBooks returns monetary amounts as strings (e.g., '1500.00') rather than numbers, nested inside an object with amount and code keys. Accessing the field without parseFloat causes type coercion issues.
Solution: Always use parseFloat() when reading FreshBooks monetary values: parseFloat(invoice.amount.amount) for the total and parseFloat(invoice.outstanding.amount) for the unpaid balance. The nested object structure is by design — the code field contains the currency code (e.g., 'USD').
1// Correct amount extraction:2const total = parseFloat(inv.amount?.amount ?? '0');3const outstanding = parseFloat(inv.outstanding?.amount ?? '0');4const currency = inv.amount?.code ?? 'USD';Time entries API returns empty array despite having logged hours in FreshBooks
Cause: The time tracking API uses businessId in the URL path, not accountId. If you stored only the accountId after OAuth and are using it for time tracking URLs, you will get 404 or empty responses.
Solution: Extract and store the business_id separately from the account_id during the OAuth callback. In the /users/me response, business_memberships[0].business.id is the businessId used for time tracking, while business_memberships[0].business.account_id is used for accounting endpoints. Store both in separate cookies.
1// Store both IDs during OAuth callback:2const membership = me.response.business_memberships[0];3const accountId = membership.business.account_id; // For accounting APIs4const businessId = String(membership.business.id); // For time tracking APIs5response.cookies.set('fb_account_id', accountId, cookieOpts);6response.cookies.set('fb_business_id', businessId, cookieOpts);Best practices
- Deploy before testing OAuth — FreshBooks redirect URIs require publicly accessible HTTPS URLs that Bolt's WebContainer cannot provide.
- Store both account_id (for accounting APIs at /accounting/account/) and business_id (for time tracking at /timetracking/business/) after OAuth. They are different identifiers and using the wrong one causes 404 errors.
- Parse FreshBooks monetary values with parseFloat() — they are returned as strings inside nested amount objects like { amount: '1500.00', code: 'USD' }.
- Include the Api-Version: alpha header on all FreshBooks API requests. Missing this header causes some endpoints to return outdated response formats.
- Test invoice sending with your own email as a test client before integrating real client emails. FreshBooks sends real emails immediately when you mark an invoice as sent via the API.
- Handle the multi-business case by storing all business memberships from /users/me. Freelancers who use FreshBooks for multiple clients often have separate business accounts.
- Cache client and time entry data that does not change frequently. FreshBooks imposes rate limits, and dashboard pages making many API calls on each render can cause 429 errors.
Alternatives
QuickBooks is more comprehensive than FreshBooks and better suited for growing businesses with inventory, payroll, and complex accounting needs.
Wave is completely free with a GraphQL API — choose it when users cannot pay for FreshBooks and need basic invoicing and expense tracking at no cost.
Harvest focuses exclusively on time tracking and invoicing with a clean REST API, making it a stronger choice when time tracking is the primary workflow rather than full accounting.
Xero has better multi-currency support and is more popular outside North America, making it the right choice for internationally-focused businesses.
Frequently asked questions
Can I test the FreshBooks API in Bolt's preview without deploying?
Partially. Once you have valid tokens (obtained from the OAuth flow on your deployed app), all outbound API calls to fetch invoices, clients, and time entries work in the Bolt preview. The OAuth authorization flow specifically — where FreshBooks redirects back to your callback URL — requires a deployed public URL. Complete auth on deployment, then use the tokens for testing in the preview.
Does FreshBooks have an official Node.js SDK?
FreshBooks does not maintain an official Node.js SDK as of 2026. The integration uses direct REST API calls with the Authorization header and standard fetch requests. The API is well-documented at developers.freshbooks.com with clear endpoint references and request/response examples for all resources.
How do I handle the FreshBooks rate limit?
FreshBooks limits API calls to 500 requests per minute. For a typical dashboard making 3-4 API calls per page load, this limit is very permissive. If you are building bulk data processing (exporting all historical invoices), implement delays between requests and use the per_page=100 maximum to fetch more data per request. The API does not currently document a specific retry-after header, so implement fixed 1-second delays between calls if you approach the limit.
Can I create invoices from time entries using the FreshBooks API?
Yes. After fetching unbilled time entries, create an invoice using POST /invoices/invoices with line items corresponding to the hours. Include the time_entry_id in each line item if available so FreshBooks marks those entries as billed. This is the standard time-to-invoice workflow. After creating the invoice, use PUT /invoices/invoices/{id}/status to send it.
What is the difference between account_id and business_id in FreshBooks?
The account_id is used in accounting API URLs (invoices, clients, expenses) and looks like a short alphanumeric string (e.g., 'xbKPqa'). The business_id is a numeric integer used in time tracking API URLs. Both are returned in the /users/me response under business_memberships. Store both after OAuth — using the wrong one for a given API path results in 404 errors or empty responses.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation