Skip to main content
RapidDev - Software Development Agency
bolt-ai-integrationsBolt Chat + API Route

How to Integrate Bolt.new with Harvest

Connect Bolt.new to Harvest using the Harvest REST API v2 with a personal access token and your Account ID. Build time tracking dashboards, pull project reports, and generate invoices from billable hours — all via Next.js API routes. Outbound Harvest API calls work in Bolt's WebContainer preview. Webhook callbacks for real-time event updates require deploying to Netlify or Bolt Cloud first.

What you'll learn

  • How to create a Harvest personal access token and locate your Account ID
  • How to fetch time entries, projects, and clients from the Harvest API v2
  • How to build a billable hours dashboard with per-client and per-project breakdowns
  • How to create invoices programmatically from tracked time entries
  • How to handle Harvest webhook events after deploying to Netlify or Bolt Cloud
Book a free consultation
4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members
Intermediate19 min read20 minutesProductivityApril 2026RapidDev Engineering Team
TL;DR

Connect Bolt.new to Harvest using the Harvest REST API v2 with a personal access token and your Account ID. Build time tracking dashboards, pull project reports, and generate invoices from billable hours — all via Next.js API routes. Outbound Harvest API calls work in Bolt's WebContainer preview. Webhook callbacks for real-time event updates require deploying to Netlify or Bolt Cloud first.

Build Time Tracking Dashboards and Invoicing Tools with Harvest and Bolt.new

Harvest sits at a unique intersection in the productivity tool landscape: it is simultaneously a time tracker and an invoicing platform, designed specifically for freelancers and service-based agencies that bill clients by the hour. While Harvest provides a solid native interface, many teams want custom views — a dashboard that shows exactly the metrics that matter to their business, a client portal that surfaces only relevant data, or an internal tool that combines Harvest time data with project management information from other sources.

Harvest's REST API v2 is straightforward and well-documented. Every endpoint returns clean JSON. Authentication uses a bearer token plus a second required header, Harvest-Account-Id, which identifies which Harvest account you are accessing. This two-header pattern is a common point of confusion for first-time integrators — forgetting the Account ID header results in 401 errors even with a valid token. Once both headers are in place, the API feels familiar: GET requests with optional query parameters for filtering by date range, client, project, or billable status.

For Bolt-built applications, Harvest is an excellent data backend for internal business tools. Time entry data is inherently tabular and amenable to custom visualizations — charts of billable vs non-billable hours over time, tables of hours by project and team member, summaries of outstanding invoices. Because Harvest already stores all the data, your Bolt app does not need to write much data back — most use cases are read-heavy dashboards pulling existing Harvest records rather than new entry creation. This makes the integration simpler and reduces the risk of writing malformed data to your authoritative time tracking system.

Integration method

Bolt Chat + API Route

Bolt generates Next.js API routes that call Harvest's REST API v2 using a personal access token and Account ID stored in .env. Harvest's API is clean, JSON-based REST — every request requires two headers: Authorization (bearer token) and Harvest-Account-Id. All outbound calls to Harvest work perfectly in Bolt's WebContainer during development. Webhook events (timers started, invoices paid) require a deployed URL since the WebContainer cannot receive incoming HTTP connections.

Prerequisites

  • A Bolt.new account with a Next.js project
  • A Harvest account at getharvest.com (free trial or paid plan)
  • At least one project with time entries in Harvest
  • A Harvest personal access token from the Developers section of your Harvest account

Step-by-step guide

1

Create a Harvest Personal Access Token and Find Your Account ID

Harvest uses personal access tokens for API authentication, combined with a second header called Harvest-Account-Id that tells the API which account to access. Both are required for every API call — missing either one results in a 401 Unauthorized response. To create a personal access token, log into your Harvest account and go to the Developers section. In the top navigation, click your profile picture or name, then select Developers. On the Developers page, you will see a section called Personal Access Tokens. Click Create New Personal Access Token. Give it a descriptive name like 'Bolt App Integration.' Harvest personal access tokens are not scoped — they grant full API access to your account. The token is shown once immediately after creation, starting with a long random string. Copy it immediately and add it to your Bolt project's .env file as HARVEST_ACCESS_TOKEN. Your Account ID appears directly on the Developers page below the Personal Access Tokens section — look for 'Your Account ID' with a numeric value (for example, 1234567). This is a number, not a string, but store it as a string in .env. Add it as HARVEST_ACCOUNT_ID. If you have access to multiple Harvest accounts, you will see multiple Account IDs listed — use the one corresponding to the account containing your projects and time entries. Every API request to Harvest must include both headers: Authorization: Bearer YOUR_TOKEN and Harvest-Account-Id: YOUR_ACCOUNT_ID. The API base URL is https://api.harvestapp.com/v2. Add both values to your .env file before prompting Bolt to build any integration code. Test the connection by calling GET /v2/users/me — this endpoint returns your user information and is a quick health check to confirm both headers are working.

Bolt.new Prompt

Add HARVEST_ACCESS_TOKEN and HARVEST_ACCOUNT_ID to the .env file with placeholder values. Create a lib/harvest.ts utility that exports a harvestFetch helper. It should accept an endpoint path (e.g., '/time_entries') and optional query params object, build the full URL as https://api.harvestapp.com/v2{path}, and make the GET request with both required headers: Authorization: Bearer and Harvest-Account-Id. Handle JSON response parsing and throw typed errors with the Harvest error message. Also export harvestPost and harvestPatch for write operations.

Paste this in Bolt.new chat

lib/harvest.ts
1// lib/harvest.ts
2const HARVEST_BASE_URL = 'https://api.harvestapp.com/v2';
3
4function getHeaders() {
5 const token = process.env.HARVEST_ACCESS_TOKEN;
6 const accountId = process.env.HARVEST_ACCOUNT_ID;
7
8 if (!token || !accountId) {
9 throw new Error('HARVEST_ACCESS_TOKEN and HARVEST_ACCOUNT_ID must be set in .env');
10 }
11
12 return {
13 Authorization: `Bearer ${token}`,
14 'Harvest-Account-Id': accountId,
15 'Content-Type': 'application/json',
16 'User-Agent': 'Bolt App (your@email.com)',
17 };
18}
19
20export async function harvestFetch<T = unknown>(
21 path: string,
22 params?: Record<string, string | number | boolean>
23): Promise<T> {
24 const url = new URL(`${HARVEST_BASE_URL}${path}`);
25 if (params) {
26 Object.entries(params).forEach(([k, v]) => url.searchParams.set(k, String(v)));
27 }
28
29 const response = await fetch(url.toString(), { headers: getHeaders() });
30
31 if (!response.ok) {
32 const error = await response.json().catch(() => ({ message: response.statusText })) as { message?: string };
33 throw new Error(error.message || `Harvest API error: ${response.status}`);
34 }
35
36 return response.json() as Promise<T>;
37}
38
39export async function harvestPost<T = unknown>(path: string, body: unknown): Promise<T> {
40 const response = await fetch(`${HARVEST_BASE_URL}${path}`, {
41 method: 'POST',
42 headers: getHeaders(),
43 body: JSON.stringify(body),
44 });
45
46 if (!response.ok) {
47 const error = await response.json().catch(() => ({ message: response.statusText })) as { message?: string };
48 throw new Error(error.message || `Harvest POST error: ${response.status}`);
49 }
50
51 return response.json() as Promise<T>;
52}
53
54export async function harvestPatch<T = unknown>(path: string, body: unknown): Promise<T> {
55 const response = await fetch(`${HARVEST_BASE_URL}${path}`, {
56 method: 'PATCH',
57 headers: getHeaders(),
58 body: JSON.stringify(body),
59 });
60
61 if (!response.ok) {
62 const error = await response.json().catch(() => ({ message: response.statusText })) as { message?: string };
63 throw new Error(error.message || `Harvest PATCH error: ${response.status}`);
64 }
65
66 return response.json() as Promise<T>;
67}

Pro tip: Harvest requires the User-Agent header to contain a valid email address for production API usage. Include your email like 'MyApp (your@email.com)' in the User-Agent header to comply with Harvest's API guidelines and avoid request blocking.

Expected result: The harvest.ts helper is in place. Test it by calling harvestFetch('/users/me') from an API route — you should see your Harvest user profile returned as JSON, confirming both the token and Account ID are correctly configured.

2

Build a Time Entries API Route with Filtering

The Harvest time entries endpoint is the core of most integrations. GET /v2/time_entries returns a paginated list of time entries with extensive filtering options. Understanding the key query parameters is essential for building useful dashboards and reports. The most important filter parameters are from and to for date range filtering — these accept ISO 8601 date strings (YYYY-MM-DD). Without a date range, Harvest returns entries starting from the most recent, paginated at 100 entries per page. For dashboard views, always set a from and to to get a specific billing period. The billable parameter accepts true or false to filter to only billable or non-billable time. client_id and project_id filter to specific clients or projects, and user_id filters to a specific team member. Each time entry in the response includes nested objects for client, project, and task — you get the name, id, and basic details of each without requiring additional API calls. This makes the time entries endpoint efficient for building dashboards that need to display time grouped by multiple dimensions. Harvest uses cursor-based pagination via the next_page parameter. When the response includes a next_page value (an integer), there are more results. Fetch subsequent pages by adding the page parameter to your request. For typical dashboard date ranges (a week or a month), most accounts will have fewer than 100 entries and pagination is not needed. For longer date ranges or large teams, build pagination into your API route. The Harvest API returns all monetary amounts in the account's currency. Hourly rates are set on the project-task assignment level, so not every time entry will have a calculated amount — only entries with a rate set will include a billable_amount value. Handle this gracefully in your UI by showing '-' or 'no rate set' for entries without a billable amount.

Bolt.new Prompt

Create a Next.js API route at app/api/harvest/time-entries/route.ts. Accept query params: from (required, ISO date), to (required, ISO date), clientId (optional), projectId (optional), billable (optional boolean). Use harvestFetch from lib/harvest.ts to call /time_entries with the appropriate params. Return the entries array plus summary stats: totalHours, billableHours, billableAmount, nonBillableHours. Handle pagination — if the response has next_page, fetch additional pages and combine. Add 30-second in-memory cache keyed by the full param set.

Paste this in Bolt.new chat

app/api/harvest/time-entries/route.ts
1// app/api/harvest/time-entries/route.ts
2import { NextResponse } from 'next/server';
3import { harvestFetch } from '@/lib/harvest';
4
5interface HarvestTimeEntry {
6 id: number;
7 hours: number;
8 billable: boolean;
9 billable_rate: number | null;
10 billable_amount: number | null;
11 notes: string | null;
12 spent_date: string;
13 client: { id: number; name: string };
14 project: { id: number; name: string };
15 task: { id: number; name: string };
16 user: { id: number; name: string };
17}
18
19interface HarvestEntriesResponse {
20 time_entries: HarvestTimeEntry[];
21 next_page: number | null;
22 total_pages: number;
23}
24
25const cache = new Map<string, { data: unknown; expiresAt: number }>();
26
27export async function GET(request: Request) {
28 const { searchParams } = new URL(request.url);
29 const from = searchParams.get('from');
30 const to = searchParams.get('to');
31 const clientId = searchParams.get('clientId');
32 const projectId = searchParams.get('projectId');
33 const billable = searchParams.get('billable');
34
35 if (!from || !to) {
36 return NextResponse.json({ error: 'from and to query params are required' }, { status: 400 });
37 }
38
39 const cacheKey = `${from}-${to}-${clientId}-${projectId}-${billable}`;
40 const cached = cache.get(cacheKey);
41 if (cached && Date.now() < cached.expiresAt) {
42 return NextResponse.json(cached.data);
43 }
44
45 try {
46 const allEntries: HarvestTimeEntry[] = [];
47 let page = 1;
48
49 while (true) {
50 const params: Record<string, string | number | boolean> = { from, to, page };
51 if (clientId) params.client_id = clientId;
52 if (projectId) params.project_id = projectId;
53 if (billable !== null) params.billable = billable === 'true';
54
55 const data = await harvestFetch<HarvestEntriesResponse>('/time_entries', params);
56 allEntries.push(...data.time_entries);
57
58 if (!data.next_page) break;
59 page = data.next_page;
60 }
61
62 const totalHours = allEntries.reduce((sum, e) => sum + e.hours, 0);
63 const billableHours = allEntries.filter(e => e.billable).reduce((sum, e) => sum + e.hours, 0);
64 const billableAmount = allEntries.reduce((sum, e) => sum + (e.billable_amount || 0), 0);
65
66 const result = {
67 entries: allEntries,
68 summary: {
69 totalHours: Math.round(totalHours * 100) / 100,
70 billableHours: Math.round(billableHours * 100) / 100,
71 nonBillableHours: Math.round((totalHours - billableHours) * 100) / 100,
72 billableAmount: Math.round(billableAmount * 100) / 100,
73 billablePercentage: totalHours > 0 ? Math.round((billableHours / totalHours) * 100) : 0,
74 },
75 };
76
77 cache.set(cacheKey, { data: result, expiresAt: Date.now() + 30_000 });
78 return NextResponse.json(result);
79 } catch (err) {
80 const message = err instanceof Error ? err.message : 'Failed to fetch time entries';
81 return NextResponse.json({ error: message }, { status: 500 });
82 }
83}

Pro tip: Harvest date filtering uses spent_date — the date the work was performed — not the time entry creation date. If users report missing entries, confirm they are filtering by the correct date column. Use ISO format YYYY-MM-DD for the from and to params, not timestamp strings.

Expected result: The API route fetches time entries from Harvest and returns them with a summary object. In the Bolt preview, test by appending ?from=2025-04-01&to=2025-04-30 to the route URL to see real time entries from your account.

3

Build a Billable Hours Dashboard in React

With the API route in place, build the dashboard UI that displays Harvest data visually. A useful time tracking dashboard shows multiple views simultaneously: a top-level summary (total hours, billable percentage, revenue), a breakdown by project or client, and a time-series view of daily or weekly effort. Fetch data on component mount using useEffect, storing the results in state. Pass from and to query params to your API route based on the selected date range. For the default view, use the current week or current month — calculate these programmatically so the dashboard is always showing current data rather than hardcoded dates. For grouping time entries by project or client, use JavaScript's reduce method to aggregate the flat entries array from Harvest into a nested structure. Group by client first, then by project within each client. Calculate subtotals at each level. Sort by hours descending so the most time-intensive projects appear first. For the time-series chart, transform entries into daily totals. Create an object keyed by spent_date, summing billable and non-billable hours separately for each date. Convert this to an array sorted by date for use with Recharts or a similar charting library. A stacked bar chart with one bar per day, split between billable (green) and non-billable (gray) hours, is the most intuitive visualization for this data type. Add a date range picker so users can change the reporting period. Simple options like 'This Week,' 'Last Week,' 'This Month,' and 'Last Month' cover most needs without requiring a full calendar widget. Update the fetch on change and show a loading indicator while new data is being retrieved. Cache results in component state to avoid re-fetching when switching between already-loaded periods.

Bolt.new Prompt

Build a HarvestDashboard React component that fetches from /api/harvest/time-entries with from/to params. Show at the top: total hours, billable hours, billable %, and billable amount in USD. Below, show a stacked bar chart (Recharts BarChart) of daily hours split by billable vs non-billable for the period. Below the chart, show a project breakdown table with columns: Project, Client, Hours, Billable Hours, Amount. Sort by total hours descending. Add date range quick-select buttons: This Week, Last Week, This Month, Last Month. Show loading skeletons while fetching.

Paste this in Bolt.new chat

Pro tip: Harvest returns hours as decimals (e.g., 1.5 for 90 minutes). When displaying to users, convert to hours and minutes format: const h = Math.floor(hours); const m = Math.round((hours - h) * 60); → '1h 30m'. This is more readable than '1.50 hours' in a dashboard context.

Expected result: The dashboard renders with real Harvest data showing billable hours summaries, a daily hours chart, and a project breakdown table. Switching between date range presets updates all three sections simultaneously.

4

Handle Harvest Webhooks After Deployment

Harvest supports webhook notifications for key events: when a time entry is created, updated, or deleted; when an invoice is created, sent, or paid; when an expense is created. These webhooks enable real-time dashboard updates, invoice payment notifications sent to Slack, and automated workflows triggered by billing events. During development in Bolt's WebContainer, the preview URL runs inside a browser tab and cannot receive incoming HTTP connections from Harvest's servers. This is a fundamental WebContainer architecture limitation — the Service Worker that handles networking only routes requests to and from the browser session itself, not from external servers. Webhook testing requires a publicly accessible URL. To use Harvest webhooks, deploy your Bolt app to Netlify or Bolt Cloud first. Once deployed, you will have a stable URL like https://yourapp.netlify.app. In your Harvest account, go to Settings → Integrations → Webhooks (or via the Harvest API with POST /v2/webhooks). Create a new webhook, set the payload URL to https://yourapp.netlify.app/api/harvest/webhook, and select the events you want to receive. Harvest signs webhook payloads with HMAC-SHA256 using a secret you set when creating the webhook. Verify this signature in your handler to confirm requests are genuinely from Harvest. The signature is sent in the Harvest-Webhook-Delivery-Id and HTTP_X_HARVEST_SIGNATURE headers — Harvest's documentation shows the exact verification pattern. For the webhook handler, return a 200 response quickly and process the event asynchronously if it involves database writes or external API calls. Harvest will retry failed deliveries (non-2xx responses) multiple times over several hours. Your handler should be idempotent — processing the same event twice should not cause duplicate data.

Bolt.new Prompt

Create a Harvest webhook handler at app/api/harvest/webhook/route.ts. Accept POST requests from Harvest. Parse the JSON body — it will have a type field (like 'time_entry.created') and a payload with the event data. For time_entry events, invalidate the in-memory cache in the time-entries API route. For invoice.paid events, log the invoice ID and amount. Return 200 with { received: true }. Add a comment explaining this endpoint only works on the deployed site (Netlify or Bolt Cloud), not in the Bolt WebContainer preview.

Paste this in Bolt.new chat

app/api/harvest/webhook/route.ts
1// app/api/harvest/webhook/route.ts
2// NOTE: This webhook handler requires deployment to Netlify or Bolt Cloud.
3// Harvest cannot send POST requests to the Bolt WebContainer preview URL.
4// After deploying, register your webhook URL in Harvest Settings → Integrations → Webhooks.
5import { NextResponse } from 'next/server';
6
7interface HarvestWebhookPayload {
8 type: string;
9 payload: {
10 id: number;
11 [key: string]: unknown;
12 };
13 created_at: string;
14}
15
16export async function POST(request: Request) {
17 const body = await request.json() as HarvestWebhookPayload;
18
19 const { type, payload } = body;
20
21 console.log(`[Harvest Webhook] Event: ${type}, ID: ${payload.id}`);
22
23 switch (type) {
24 case 'time_entry.created':
25 case 'time_entry.updated':
26 case 'time_entry.deleted':
27 // Time entry changed — signal frontend to refresh dashboard data
28 // In a real app, invalidate your cache or emit to a WebSocket/SSE stream
29 console.log(`[Harvest] Time entry ${type.split('.')[1]}: ${payload.id}`);
30 break;
31
32 case 'invoice.paid':
33 console.log(`[Harvest] Invoice paid: ${payload.id}`);
34 // Add notification logic here (Slack, email, etc.)
35 break;
36
37 default:
38 console.log(`[Harvest] Unhandled event type: ${type}`);
39 }
40
41 return NextResponse.json({ received: true });
42}

Pro tip: Harvest retries webhook deliveries up to 10 times over 72 hours if it receives a non-2xx response. Always return 200 immediately, even if your processing logic encounters an error — handle errors internally and log them rather than letting Harvest retry aggressively.

Expected result: The webhook handler is deployed and registered in Harvest. When a new time entry is created in Harvest, the webhook fires within a few seconds and you see the log output in Netlify's function logs or Bolt Cloud's logs panel.

Common use cases

Agency Billable Hours Dashboard

Build an internal dashboard that pulls time entries from Harvest and visualizes billable versus non-billable hours by client, project, and team member for the current billing period. The dashboard helps agency owners and project managers see at a glance which projects are on track, which clients are over or under budget, and which team members have remaining capacity.

Bolt.new Prompt

Build a Harvest time tracking dashboard. Create a Next.js API route at /api/harvest/time-entries that fetches this week's time entries from Harvest using GET https://api.harvestapp.com/v2/time_entries with query params from and to (ISO date strings). Include client_id, project_id, task_id, billable, hours, and notes fields. Build a React dashboard with: a summary bar showing total hours, billable hours, and billable percentage; a breakdown table grouped by project showing hours and billable amount; and a bar chart using Recharts showing daily hours for the week. Store HARVEST_ACCESS_TOKEN and HARVEST_ACCOUNT_ID in process.env.

Copy this prompt to try it in Bolt.new

Client Time Report Generator

Create a tool that generates a formatted time report for a specific client — useful before client meetings, invoice reviews, or end-of-month billing cycles. Fetch all time entries for a client filtered by date range, group them by project and task, calculate totals, and render a clean printable or exportable view that can be shared with the client.

Bolt.new Prompt

Create a client time report page using the Harvest API. Build /api/harvest/client-report that accepts query params clientId, from, and to, then fetches all time entries for that client and period. Group entries by project then task. Calculate subtotals (hours, billable amount) per project and a grand total. Fetch client details from /api/harvest/clients. Build a React report page with a client selector dropdown (populated from /api/harvest/clients), date range pickers, a Generate Report button, and a rendered report showing the grouped time breakdown with a total row. Include a print button that triggers window.print().

Copy this prompt to try it in Bolt.new

Invoice Creation from Tracked Time

Build a tool that creates Harvest invoices from uninvoiced time entries. Show a list of clients with outstanding tracked-but-uninvoiced hours, let the user select a client and date range, preview the invoice line items, and submit the invoice creation via the Harvest API. This eliminates manual invoice creation for recurring billing cycles.

Bolt.new Prompt

Build an invoice creation tool using the Harvest API. Create /api/harvest/uninvoiced that calls GET /v2/reports/uninvoiced with from and to params to get clients with uninvoiced hours. Create /api/harvest/invoices/create that accepts POST with { clientId, from, to, subject, dueDate } and calls POST /v2/invoices to create the invoice from time entries. Build a React workflow: step 1 shows a list of clients with uninvoiced hours and a date range selector; step 2 shows a preview of line items for the selected client; step 3 is a confirm button that creates the invoice and shows a success message with the invoice number.

Copy this prompt to try it in Bolt.new

Troubleshooting

401 Unauthorized on every Harvest API request even with a valid-looking token

Cause: Harvest API requires two headers on every request: Authorization: Bearer TOKEN and Harvest-Account-Id: ACCOUNT_ID. Missing the Harvest-Account-Id header always produces a 401, even if the token itself is valid.

Solution: Confirm your API route includes both the Authorization and Harvest-Account-Id headers. Log both values to the console during development to verify they are being read correctly from process.env. Also verify the token does not have NEXT_PUBLIC_ prefix — Harvest tokens must never be exposed client-side.

typescript
1// Both headers are required on EVERY Harvest API request:
2const headers = {
3 Authorization: `Bearer ${process.env.HARVEST_ACCESS_TOKEN}`,
4 'Harvest-Account-Id': process.env.HARVEST_ACCOUNT_ID,
5 'Content-Type': 'application/json',
6};

API returns 403 Forbidden when trying to create or update records

Cause: Personal access tokens inherit the permissions of the Harvest user who created them. If the user account has a limited role (e.g., Member rather than Administrator), certain API operations like creating invoices or accessing other users' time entries will be restricted.

Solution: Check the role of the Harvest user whose token you are using. For full API access including invoices and all users' time entries, the token must belong to an Administrator account. If you need limited access for security reasons, create a dedicated Harvest user with the exact role required for your integration.

Time entries from the current day are missing from API results

Cause: Harvest's date filtering is by spent_date (the date work was performed). Time entries created today but for a different date range will appear on the spent_date, not today. Also, very recently created entries (within the last minute) may not appear in API results immediately due to eventual consistency.

Solution: Ensure the to parameter in your API call includes today's date. Use new Date().toISOString().split('T')[0] to get today's date in YYYY-MM-DD format. For live dashboards, reduce the cache TTL to 30 seconds or less so recent entries appear quickly.

typescript
1// Get today's date in YYYY-MM-DD format:
2const today = new Date().toISOString().split('T')[0];
3// First day of current month:
4const firstOfMonth = new Date(new Date().getFullYear(), new Date().getMonth(), 1)
5 .toISOString().split('T')[0];

Harvest webhook events are not arriving at the deployed API route

Cause: The webhook URL was registered before deployment, pointing to the Bolt WebContainer preview URL instead of the deployed Netlify or Bolt Cloud URL. The WebContainer preview cannot receive incoming HTTP connections from external services.

Solution: Deploy your Bolt app to Netlify or Bolt Cloud first, then register the deployed URL (e.g., https://yourapp.netlify.app/api/harvest/webhook) in Harvest Settings. Delete any webhook registrations pointing to WebContainer preview URLs. Verify the webhook is active in Harvest's integrations panel and test it using Harvest's 'Send test delivery' feature.

Best practices

  • Always include both the Authorization and Harvest-Account-Id headers in every API request — Harvest will reject requests with 401 even if the token is valid without the Account ID header.
  • Store HARVEST_ACCESS_TOKEN and HARVEST_ACCOUNT_ID without any NEXT_PUBLIC_ prefix to ensure these credentials are only accessible server-side and never bundled into client JavaScript.
  • Cache Harvest API responses for at least 30 seconds on dashboard routes — time tracking data does not change in real time and caching prevents unnecessary API calls, especially for reports that multiple team members might view simultaneously.
  • Harvest personal access tokens expire never by default, but rotate them periodically and create new tokens with minimal scope descriptions that explain what each token is used for, making auditing easier.
  • For date range queries, always explicitly set both from and to parameters — Harvest's default behavior without dates returns entries starting from the most recent, which may not align with your billing period expectations.
  • Display time in hours and minutes format (1h 30m) rather than decimal hours (1.50) in user-facing interfaces — decimal hours are harder to reason about for non-technical stakeholders reviewing reports.
  • Test invoice creation in Harvest's test mode or with a development project first — mistakenly sending invoices to real clients from a testing workflow is a serious operational risk.
  • Register Harvest webhooks only after deploying to a stable URL — WebContainer preview URLs change on every session and cannot receive incoming HTTP connections from Harvest's servers.

Alternatives

Frequently asked questions

Can I use the Harvest API from Bolt's WebContainer preview without deploying?

Yes — outbound API calls to Harvest's REST API work perfectly in Bolt's WebContainer preview. You can fetch time entries, projects, clients, and create invoices during development without deploying. The only thing that requires deployment is Harvest webhook callbacks, which are incoming connections that the WebContainer cannot receive.

Does Bolt.new have a native Harvest integration?

No — Bolt.new does not include a built-in Harvest connector. The integration uses Harvest's REST API v2 directly from Next.js API routes with a personal access token. Bolt's AI can generate the full integration code from a description of what you need, making setup straightforward even without a native connector.

What Harvest plan do I need to use the API?

Harvest's API is available on all plans including the free trial. The free Harvest plan allows up to 2 projects and 1 user — sufficient for testing the integration. Paid plans (Harvest Pro at $12/seat/month) remove the project and user limits. The API itself does not have rate limits documented publicly, but Harvest recommends reasonable usage and may throttle excessive calls.

How do I access time entries for all team members, not just my own?

By default, GET /v2/time_entries returns all time entries visible to the authenticated user. If your Harvest account role is Administrator, you will see all users' entries. If your role is Member, you will only see your own. For a team dashboard showing all members' time, the token must belong to an Administrator account. Use the user_id query parameter to filter to a specific team member.

Can I create time entries from my Bolt app back into Harvest?

Yes — POST /v2/time_entries creates new time entries. Required fields are user_id, project_id, task_id, spent_date, and hours. Optionally include notes and billable fields. The harvestPost helper handles this. Keep in mind that creating time entries modifies your authoritative time tracking data, so add appropriate confirmation UI before submitting.

How do I handle multiple Harvest accounts in one Bolt app?

The Harvest-Account-Id header determines which account is accessed. If your app needs to switch between accounts, store multiple account ID and token pairs as separate environment variables and select the appropriate pair based on the user or context. Harvest's OAuth 2.0 flow (more complex than personal access tokens) is the appropriate solution for multi-tenant apps where different users authorize access to their own Harvest accounts.

RapidDev

Talk to an Expert

Our team has built 600+ apps. Get personalized help with your project.

Book a free consultation

Need help with your project?

Our experts have built 600+ apps and can accelerate your development. Book a free consultation — no strings attached.

Book a free consultation

We put the rapid in RapidDev

Need a dedicated strategic tech and growth partner? Discover what RapidDev can do for your business! Book a call with our team to schedule a free, no-obligation consultation. We'll discuss your project and provide a custom quote at no cost.