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 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
Create a Harvest Personal Access Token and Find Your Account ID
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.
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
1// lib/harvest.ts2const HARVEST_BASE_URL = 'https://api.harvestapp.com/v2';34function getHeaders() {5 const token = process.env.HARVEST_ACCESS_TOKEN;6 const accountId = process.env.HARVEST_ACCOUNT_ID;78 if (!token || !accountId) {9 throw new Error('HARVEST_ACCESS_TOKEN and HARVEST_ACCOUNT_ID must be set in .env');10 }1112 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}1920export 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 }2829 const response = await fetch(url.toString(), { headers: getHeaders() });3031 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 }3536 return response.json() as Promise<T>;37}3839export 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 });4546 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 }5051 return response.json() as Promise<T>;52}5354export 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 });6061 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 }6566 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.
Build a Time Entries API Route with Filtering
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.
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
1// app/api/harvest/time-entries/route.ts2import { NextResponse } from 'next/server';3import { harvestFetch } from '@/lib/harvest';45interface 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}1819interface HarvestEntriesResponse {20 time_entries: HarvestTimeEntry[];21 next_page: number | null;22 total_pages: number;23}2425const cache = new Map<string, { data: unknown; expiresAt: number }>();2627export 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');3435 if (!from || !to) {36 return NextResponse.json({ error: 'from and to query params are required' }, { status: 400 });37 }3839 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 }4445 try {46 const allEntries: HarvestTimeEntry[] = [];47 let page = 1;4849 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';5455 const data = await harvestFetch<HarvestEntriesResponse>('/time_entries', params);56 allEntries.push(...data.time_entries);5758 if (!data.next_page) break;59 page = data.next_page;60 }6162 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);6566 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 };7677 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.
Build a Billable Hours Dashboard in React
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.
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.
Handle Harvest Webhooks After Deployment
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.
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
1// app/api/harvest/webhook/route.ts2// 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';67interface HarvestWebhookPayload {8 type: string;9 payload: {10 id: number;11 [key: string]: unknown;12 };13 created_at: string;14}1516export async function POST(request: Request) {17 const body = await request.json() as HarvestWebhookPayload;1819 const { type, payload } = body;2021 console.log(`[Harvest Webhook] Event: ${type}, ID: ${payload.id}`);2223 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 data28 // In a real app, invalidate your cache or emit to a WebSocket/SSE stream29 console.log(`[Harvest] Time entry ${type.split('.')[1]}: ${payload.id}`);30 break;3132 case 'invoice.paid':33 console.log(`[Harvest] Invoice paid: ${payload.id}`);34 // Add notification logic here (Slack, email, etc.)35 break;3637 default:38 console.log(`[Harvest] Unhandled event type: ${type}`);39 }4041 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.
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.
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.
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.
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.
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
Clockify is a free time tracker focused purely on tracking without invoicing, making it better for teams that handle billing separately; Harvest includes invoicing built in, making it the better choice for freelancers and agencies that bill clients directly.
Toggl Track offers a simpler API and a more generous free tier, making it easier to start with; Harvest is better when you need the full time-tracking-to-invoicing workflow in a single platform.
FreshBooks is a full accounting platform with time tracking as one feature among many; Harvest is purpose-built for time tracking with invoicing, making it more focused for service-based businesses that primarily bill by the hour.
Everhour is better for teams that track time directly inside project management tools like Asana or Jira; Harvest is better as a standalone time tracking and billing platform for client-service workflows.
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.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation