# How to Integrate Bolt.new with FreshBooks

- Tool: Bolt.new
- Difficulty: Intermediate
- Time required: 40 minutes
- Last updated: April 2026

## TL;DR

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.

## Before you start

- 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

### 1. 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.

```
// .env.local
FRESHBOOKS_CLIENT_ID=your_freshbooks_client_id
FRESHBOOKS_CLIENT_SECRET=your_freshbooks_client_secret
FRESHBOOKS_REDIRECT_URI=https://your-app.netlify.app/api/freshbooks/callback

// lib/freshbooks-auth.ts
export const FB_API_BASE = 'https://api.freshbooks.com';
export const FB_AUTH_BASE = 'https://auth.freshbooks.com';

export function buildFreshBooksAuthUrl(): string {
  const params = new URLSearchParams({
    client_id: process.env.FRESHBOOKS_CLIENT_ID!,
    response_type: 'code',
    redirect_uri: process.env.FRESHBOOKS_REDIRECT_URI!,
  });
  return `${FB_AUTH_BASE}/oauth/authorize?${params.toString()}`;
}

// app/api/freshbooks/callback/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { FB_API_BASE, FB_AUTH_BASE } from '@/lib/freshbooks-auth';

export async function GET(request: NextRequest) {
  const { searchParams } = new URL(request.url);
  const code = searchParams.get('code');

  if (!code) {
    return NextResponse.redirect(new URL('/error?message=no_code', request.url));
  }

  try {
    const tokenRes = await fetch(`${FB_API_BASE}/auth/oauth/token`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        grant_type: 'authorization_code',
        client_id: process.env.FRESHBOOKS_CLIENT_ID,
        client_secret: process.env.FRESHBOOKS_CLIENT_SECRET,
        code,
        redirect_uri: process.env.FRESHBOOKS_REDIRECT_URI,
      }),
    });
    const tokens = await tokenRes.json() as { access_token: string; refresh_token: string; token_type: string };

    if (!tokens.access_token) throw new Error('No access token received');

    // Get account_id from the users/me endpoint
    const meRes = await fetch(`${FB_API_BASE}/auth/api/v1/users/me`, {
      headers: { Authorization: `Bearer ${tokens.access_token}` },
    });
    const me = await meRes.json() as {
      response: { business_memberships: Array<{ business: { account_id: string; name: string } }> };
    };
    const accountId = me.response.business_memberships[0]?.business?.account_id;

    if (!accountId) throw new Error('No FreshBooks account found');

    const response = NextResponse.redirect(new URL('/dashboard', request.url));
    const cookieOpts = { httpOnly: true, secure: process.env.NODE_ENV === 'production', sameSite: 'lax' as const, maxAge: 60 * 60 * 24 * 30, path: '/' };
    response.cookies.set('fb_access_token', tokens.access_token, { ...cookieOpts, maxAge: 60 * 60 * 12 });
    response.cookies.set('fb_refresh_token', tokens.refresh_token, cookieOpts);
    response.cookies.set('fb_account_id', accountId, cookieOpts);

    return response;
  } catch (error) {
    console.error('FreshBooks callback error:', error);
    return NextResponse.redirect(new URL('/error?message=freshbooks_auth_failed', request.url));
  }
}
```

**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.

### 2. 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.

```
// lib/freshbooks-api.ts
import { cookies } from 'next/headers';
import { FB_API_BASE } from './freshbooks-auth';

const STATUS_MAP: Record<number, string> = {
  1: 'Draft',
  2: 'Sent',
  4: 'Viewed',
  5: 'Outstanding',
  6: 'Overdue',
  7: 'Disputed',
  8: 'Paid',
  9: 'Auto-Paid',
};

export { STATUS_MAP };

export async function freshBooksCall<T>(
  path: string,
  options: RequestInit = {}
): Promise<T> {
  const cookieStore = cookies();
  const accessToken = cookieStore.get('fb_access_token')?.value;
  const accountId = cookieStore.get('fb_account_id')?.value;

  if (!accessToken || !accountId) {
    throw new Error('Not authenticated with FreshBooks');
  }

  const url = path.startsWith('http')
    ? path
    : `${FB_API_BASE}/accounting/account/${accountId}${path}`;

  const response = await fetch(url, {
    ...options,
    headers: {
      Authorization: `Bearer ${accessToken}`,
      'Content-Type': 'application/json',
      'Api-Version': 'alpha',
      ...(options.headers ?? {}),
    },
  });

  if (!response.ok) {
    throw new Error(`FreshBooks API error: ${response.status}`);
  }

  const data = await response.json() as { response: { result: T } };
  return data.response.result;
}

// app/api/freshbooks/invoices/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { freshBooksCall, STATUS_MAP } from '@/lib/freshbooks-api';

interface FBInvoice {
  invoiceid: number;
  number: string;
  currentorganization: string;
  create_date: string;
  due_date: string;
  amount: { amount: string; code: string };
  outstanding: { amount: string; code: string };
  v3_status: string;
  status: number;
}

interface FBInvoicesResponse {
  invoices: FBInvoice[];
  total_count: number;
  pages: number;
}

export async function GET(request: NextRequest) {
  const { searchParams } = new URL(request.url);
  const page = searchParams.get('page') ?? '1';

  try {
    const data = await freshBooksCall<FBInvoicesResponse>(
      `/invoices/invoices?per_page=100&page=${page}&include[]=late_reminders`
    );

    const invoices = data.invoices.map((inv) => ({
      id: inv.invoiceid,
      number: inv.number,
      clientName: inv.currentorganization,
      createDate: inv.create_date,
      dueDate: inv.due_date,
      total: parseFloat(inv.amount.amount),
      outstanding: parseFloat(inv.outstanding.amount),
      currency: inv.amount.code,
      status: inv.status,
      statusLabel: STATUS_MAP[inv.status] ?? 'Unknown',
      isPaid: inv.status === 8 || inv.status === 9,
    }));

    const totalOutstanding = invoices.filter((i) => !i.isPaid).reduce((s, i) => s + i.outstanding, 0);

    return NextResponse.json({ invoices, totalOutstanding, totalCount: data.total_count, pages: data.pages });
  } catch (error) {
    const message = error instanceof Error ? error.message : 'Failed to fetch invoices';
    return NextResponse.json({ error: message }, { status: 500 });
  }
}
```

**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.

### 3. 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.

```
// app/api/freshbooks/time-entries/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { cookies } from 'next/headers';
import { FB_API_BASE } from '@/lib/freshbooks-auth';

export async function GET(request: NextRequest) {
  const cookieStore = cookies();
  const accessToken = cookieStore.get('fb_access_token')?.value;
  // businessId is stored separately from accountId
  const businessId = cookieStore.get('fb_business_id')?.value;

  if (!accessToken || !businessId) {
    return NextResponse.json({ error: 'Not authenticated' }, { status: 401 });
  }

  const { searchParams } = new URL(request.url);
  const billedFilter = searchParams.get('billed') ?? 'false';

  try {
    const response = await fetch(
      `${FB_API_BASE}/timetracking/business/${businessId}/time_entries?billed=${billedFilter}&billable=true&per_page=200`,
      { headers: { Authorization: `Bearer ${accessToken}`, 'Api-Version': 'alpha' } }
    );

    if (!response.ok) throw new Error(`FreshBooks time API error: ${response.status}`);

    const data = await response.json() as {
      time_entries: Array<{
        id: number;
        client_id: number;
        project_id: number;
        duration: number;
        started_at: string;
        hourly_rate: string | null;
        billed: boolean;
        billable: boolean;
        note: string;
      }>;
    };

    const entries = data.time_entries.map((e) => ({
      id: e.id,
      clientId: e.client_id,
      projectId: e.project_id,
      durationHours: Math.round((e.duration / 3600) * 100) / 100,
      date: e.started_at?.split('T')[0],
      hourlyRate: e.hourly_rate ? parseFloat(e.hourly_rate) : null,
      amount: e.hourly_rate ? Math.round((e.duration / 3600) * parseFloat(e.hourly_rate) * 100) / 100 : null,
      note: e.note,
      billed: e.billed,
      billable: e.billable,
    }));

    const totalUnbilledHours = entries.filter((e) => !e.billed).reduce((s, e) => s + e.durationHours, 0);
    const totalUnbilledAmount = entries.filter((e) => !e.billed && e.amount).reduce((s, e) => s + (e.amount ?? 0), 0);

    return NextResponse.json({
      entries,
      totalEntries: entries.length,
      totalUnbilledHours: Math.round(totalUnbilledHours * 100) / 100,
      totalUnbilledAmount: Math.round(totalUnbilledAmount * 100) / 100,
    });
  } catch (error) {
    const message = error instanceof Error ? error.message : 'Failed to fetch time entries';
    return NextResponse.json({ error: message }, { status: 500 });
  }
}
```

**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.

### 4. 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.

```
// components/FreshBooksDashboard.tsx
'use client';

import { useEffect, useState } from 'react';

interface Invoice {
  id: number;
  number: string;
  clientName: string;
  createDate: string;
  dueDate: string;
  total: number;
  outstanding: number;
  currency: string;
  status: number;
  statusLabel: string;
  isPaid: boolean;
}

const STATUS_COLORS: Record<number, string> = {
  1: 'bg-gray-100 text-gray-600',
  2: 'bg-blue-100 text-blue-700',
  4: 'bg-blue-100 text-blue-700',
  5: 'bg-yellow-100 text-yellow-700',
  6: 'bg-red-100 text-red-700',
  8: 'bg-green-100 text-green-700',
  9: 'bg-green-100 text-green-700',
};

export default function FreshBooksDashboard() {
  const [invoices, setInvoices] = useState<Invoice[]>([]);
  const [unbilledHours, setUnbilledHours] = useState(0);
  const [unbilledAmount, setUnbilledAmount] = useState(0);
  const [loading, setLoading] = useState(true);
  const [statusFilter, setStatusFilter] = useState<number | null>(null);
  const [error, setError] = useState('');

  useEffect(() => {
    Promise.all([
      fetch('/api/freshbooks/invoices').then((r) => r.json()),
      fetch('/api/freshbooks/time-entries').then((r) => r.json()),
    ]).then(([invData, timeData]) => {
      if (invData.error) throw new Error(invData.error);
      setInvoices(invData.invoices ?? []);
      setUnbilledHours(timeData.totalUnbilledHours ?? 0);
      setUnbilledAmount(timeData.totalUnbilledAmount ?? 0);
    }).catch((e) => setError(e.message)).finally(() => setLoading(false));
  }, []);

  const totalOutstanding = invoices.filter((i) => !i.isPaid).reduce((s, i) => s + i.outstanding, 0);
  const overdueCount = invoices.filter((i) => i.status === 6).length;

  const filtered = statusFilter ? invoices.filter((i) => i.status === statusFilter) : invoices;

  if (loading) return <div className="p-8 text-center">Loading FreshBooks data...</div>;
  if (error) return <div className="p-8 text-red-600">Error: {error}</div>;

  return (
    <div className="p-6 max-w-5xl mx-auto">
      <h1 className="text-2xl font-bold mb-6">FreshBooks Dashboard</h1>

      <div className="grid grid-cols-2 md:grid-cols-4 gap-4 mb-8">
        {[{label: 'Outstanding', value: `$${totalOutstanding.toFixed(2)}`, color: 'text-blue-600'},
          {label: 'Overdue', value: String(overdueCount), color: 'text-red-600'},
          {label: 'Unbilled Hours', value: `${unbilledHours}h`, color: 'text-yellow-600'},
          {label: 'Unbilled Value', value: `$${unbilledAmount.toFixed(2)}`, color: 'text-purple-600'},
        ].map(({label, value, color}) => (
          <div key={label} className="border rounded-lg p-4">
            <p className="text-sm text-gray-500">{label}</p>
            <p className={`text-xl font-bold ${color}`}>{value}</p>
          </div>
        ))}
      </div>

      <div className="flex gap-2 mb-4 flex-wrap">
        <button onClick={() => setStatusFilter(null)} className={`px-3 py-1 rounded text-sm ${!statusFilter ? 'bg-blue-600 text-white' : 'bg-gray-100'}`}>All</button>
        {[{status: 2, label: 'Sent'}, {status: 6, label: 'Overdue'}, {status: 8, label: 'Paid'}, {status: 1, label: 'Draft'}].map(({status, label}) => (
          <button key={status} onClick={() => setStatusFilter(status)}
            className={`px-3 py-1 rounded text-sm ${statusFilter === status ? 'bg-blue-600 text-white' : 'bg-gray-100'}`}>{label}</button>
        ))}
      </div>

      <div className="overflow-x-auto">
        <table className="w-full text-sm">
          <thead>
            <tr className="border-b">
              <th className="text-left py-2 font-medium">Invoice #</th>
              <th className="text-left py-2 font-medium">Client</th>
              <th className="text-left py-2 font-medium">Date</th>
              <th className="text-right py-2 font-medium">Amount</th>
              <th className="text-left py-2 font-medium">Status</th>
              <th className="text-left py-2 font-medium">View</th>
            </tr>
          </thead>
          <tbody>
            {filtered.map((inv) => (
              <tr key={inv.id} className="border-b hover:bg-gray-50">
                <td className="py-3">{inv.number}</td>
                <td className="py-3">{inv.clientName}</td>
                <td className="py-3">{inv.createDate}</td>
                <td className="py-3 text-right">{inv.currency} {inv.total.toFixed(2)}</td>
                <td className="py-3">
                  <span className={`px-2 py-1 rounded-full text-xs ${STATUS_COLORS[inv.status] ?? 'bg-gray-100'}`}>
                    {inv.statusLabel}
                  </span>
                </td>
                <td className="py-3">
                  <a href={`https://my.freshbooks.com/#/invoice/${inv.id}`} target="_blank" rel="noopener noreferrer"
                    className="text-blue-500 hover:underline text-xs">Open →</a>
                </td>
              </tr>
            ))}
          </tbody>
        </table>
      </div>
    </div>
  );
}
```

**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.

## 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.

## 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.

Prompt example:

```
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.
```

### 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.

Prompt example:

```
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.
```

### 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.

Prompt example:

```
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.
```

## 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.

```
// Correct Authorization header format:
headers: {
  'Authorization': `Bearer ${accessToken}`,
  'Api-Version': 'alpha',
  'Content-Type': 'application/json',
}
```

### 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').

```
// Correct amount extraction:
const total = parseFloat(inv.amount?.amount ?? '0');
const outstanding = parseFloat(inv.outstanding?.amount ?? '0');
const 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.

```
// Store both IDs during OAuth callback:
const membership = me.response.business_memberships[0];
const accountId = membership.business.account_id;  // For accounting APIs
const businessId = String(membership.business.id);  // For time tracking APIs
response.cookies.set('fb_account_id', accountId, cookieOpts);
response.cookies.set('fb_business_id', businessId, cookieOpts);
```

## 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.

---

Source: https://www.rapidevelopers.com/bolt-ai-integrations/freshbooks
© RapidDev — https://www.rapidevelopers.com/bolt-ai-integrations/freshbooks
