Integrating Harvest with Lovable requires Edge Functions to proxy the Harvest REST API v2. Store your Harvest personal access token and account ID in Cloud Secrets, create an Edge Function that reads clients, projects, time entries, and invoices, and build time-to-invoice workflows in Lovable's React frontend. Harvest is the leading choice for freelancers and agencies who need both time tracking and invoice generation in one tool — its API covers the full billing cycle from tracked hours to paid invoices.
Why integrate Harvest with Lovable?
Harvest is the most complete time-to-invoicing solution for client-service businesses. While tools like Clockify and Everhour focus on time tracking, Harvest closes the full billing loop: you track time against projects, Harvest aggregates the hours, and you generate professional invoices that pull the tracked time into line items automatically. Add expense tracking and Harvest becomes a complete client billing system — one that 70,000+ businesses use as their primary revenue operations tool.
Building a custom Lovable integration with Harvest enables workflows that extend beyond what Harvest's native interface provides: a client-facing billing portal where clients can see tracked hours, pending invoices, and payment status without logging into Harvest, an automated invoice generation system that creates and sends invoices at the end of each billing period based on tracked time, a project profitability dashboard that compares revenue (from invoices) against labor costs (from tracked time with hourly rates) and expense totals, and a time approval workflow where project managers review and approve team members' time entries before they become billable.
Harvest's REST API v2 is well-designed and complete. Authentication requires two credentials: a Bearer token (personal access token or OAuth2 access token) and a Harvest-Account-ID header specifying which account to access. This dual-header pattern is consistent across all endpoints. The API covers all of Harvest's core functionality: time entries (full CRUD), projects, clients, tasks, invoices (create, send, mark paid), expenses, and reports. This tutorial covers the complete time-to-invoice workflow that makes Harvest integrations particularly valuable.
Integration method
Harvest has no native Lovable connector. All Harvest API calls use the REST API v2 at api.harvestapp.com/v2, proxied through Supabase Edge Functions. Personal access tokens and account IDs are stored in Cloud Secrets and sent as Authorization Bearer and Harvest-Account-ID headers. Edge Functions read clients, projects, time entries, and invoices — enabling complete time-to-invoice workflows within your Lovable application.
Prerequisites
- A Lovable project with Cloud enabled
- A Harvest account (Starter or Pro plan — the free plan does not include API access beyond basic reading)
- A Harvest personal access token — generate at id.getharvest.com/oauth2/access_tokens → Create new personal access token
- Your Harvest account ID — shown at id.getharvest.com/oauth2/access_tokens alongside your token, or in your Harvest account URL
- Client IDs and project IDs for the projects you want to work with — found in Harvest URLs or via the API
Step-by-step guide
Generate a Harvest personal access token and store credentials in Cloud Secrets
Generate a Harvest personal access token and store credentials in Cloud Secrets
Harvest personal access tokens (PATs) are the simplest way to authenticate server-side integrations. They provide full access to your Harvest account and don't expire unless manually revoked. To generate one, go to id.getharvest.com/oauth2/access_tokens in your browser — log in with your Harvest credentials if prompted. Click 'Create new personal access token', give it a name like 'Lovable Integration', and click 'Create Personal Access Token'. The token is displayed once — copy it immediately. On the same page, your account ID is displayed alongside the token — it's typically a 6-7 digit number. Copy this too. Both values are required for every Harvest API request: the token as a Bearer Authorization header and the account ID as a Harvest-Account-ID header. This dual-header pattern is Harvest's approach to supporting multiple accounts under one OAuth application, and it must be implemented correctly. In your Lovable project, open the Cloud tab via the '+' button, navigate to Secrets, and add two secrets: HARVEST_ACCESS_TOKEN with your PAT value, and HARVEST_ACCOUNT_ID with your numeric account ID. Lovable's security infrastructure encrypts both immediately and they will only be accessible from your Edge Functions — never from any client-side code. Lovable blocks approximately 1,200 hardcoded API keys per day; always use Secrets for credentials.
Pro tip: Harvest also supports OAuth2 for multi-user apps where different users connect their own Harvest accounts. Register an app at id.getharvest.com/oauth2/clients if you need per-user authentication. For single-account integrations, the PAT approach is much simpler.
Expected result: HARVEST_ACCESS_TOKEN and HARVEST_ACCOUNT_ID appear in Cloud Secrets with masked values. Both are ready for use in Edge Functions.
Create the Harvest proxy Edge Function
Create the Harvest proxy Edge Function
Harvest's API v2 requires two custom headers on every request: 'Authorization: Bearer YOUR_TOKEN' and 'Harvest-Account-ID: YOUR_ACCOUNT_ID'. The Edge Function reads both secrets and applies them to all outgoing requests. Harvest's API returns paginated responses — most list endpoints return a maximum of 100 items per page with pagination metadata (per_page, total_pages, next_page) in the response envelope. The Edge Function handles the most valuable Harvest operations through an action-based router. The invoice creation action is particularly important — Harvest invoices can be created from scratch with custom line items, or generated from tracked time using the 'harvest_time' creation type which automatically pulls in unbilled time entries as line items. This tutorial uses the manual line-item approach for maximum control, which is more suitable for custom Lovable workflows.
Create a Supabase Edge Function at supabase/functions/harvest-proxy/index.ts. Read HARVEST_ACCESS_TOKEN and HARVEST_ACCOUNT_ID from Deno.env.get(). Set headers: Authorization: Bearer {token} and Harvest-Account-ID: {accountId} on all requests. Handle CORS. Base URL https://api.harvestapp.com/v2. Accept POST with action and params. Implement: 'list_clients' (GET /clients), 'list_projects' (GET /projects with optional params.clientId and params.isActive), 'list_time_entries' with params.clientId, params.projectId, params.from, params.to (GET /time_entries with query params), 'create_time_entry' (POST /time_entries with project_id, task_id, spent_date, hours, notes from params), 'list_invoices' with params.clientId and optional params.state (GET /invoices), 'get_invoice' (GET /invoices/{params.invoiceId}), 'create_invoice' (POST /invoices with client_id, subject, issue_date, due_date, line_items array from params), 'send_invoice' (POST /invoices/{params.invoiceId}/messages with body from params), 'list_tasks' (GET /tasks), 'list_expenses' with date range and client filter (GET /expenses). Return full responses.
Paste this in Lovable chat
1// supabase/functions/harvest-proxy/index.ts2import { serve } from "https://deno.land/std@0.168.0/http/server.ts";34const BASE = "https://api.harvestapp.com/v2";5const corsHeaders = {6 "Access-Control-Allow-Origin": "*",7 "Access-Control-Allow-Headers": "authorization, x-client-info, apikey, content-type",8};910serve(async (req) => {11 if (req.method === "OPTIONS") {12 return new Response("ok", { headers: corsHeaders });13 }14 const token = Deno.env.get("HARVEST_ACCESS_TOKEN");15 const accountId = Deno.env.get("HARVEST_ACCOUNT_ID");16 if (!token || !accountId) {17 return new Response(JSON.stringify({ error: "Harvest credentials not configured" }), {18 status: 500, headers: { ...corsHeaders, "Content-Type": "application/json" },19 });20 }21 const authHeaders = {22 Authorization: `Bearer ${token}`,23 "Harvest-Account-ID": accountId,24 "Content-Type": "application/json",25 };26 const { action, params = {} } = await req.json();27 let resp;2829 switch (action) {30 case "list_clients":31 resp = await fetch(`${BASE}/clients?per_page=100`, { headers: authHeaders });32 break;33 case "list_projects": {34 const q = new URLSearchParams({ per_page: "100" });35 if (params.clientId) q.set("client_id", params.clientId);36 if (params.isActive !== undefined) q.set("is_active", String(params.isActive));37 resp = await fetch(`${BASE}/projects?${q}`, { headers: authHeaders });38 break;39 }40 case "list_time_entries": {41 const q = new URLSearchParams({ per_page: "100" });42 if (params.clientId) q.set("client_id", params.clientId);43 if (params.projectId) q.set("project_id", params.projectId);44 if (params.from) q.set("from", params.from);45 if (params.to) q.set("to", params.to);46 if (params.isBilled !== undefined) q.set("is_billed", String(params.isBilled));47 resp = await fetch(`${BASE}/time_entries?${q}`, { headers: authHeaders });48 break;49 }50 case "create_time_entry":51 resp = await fetch(`${BASE}/time_entries`, {52 method: "POST", headers: authHeaders,53 body: JSON.stringify({54 project_id: params.projectId, task_id: params.taskId,55 spent_date: params.spentDate, hours: params.hours,56 notes: params.notes || "",57 }),58 });59 break;60 case "list_invoices": {61 const q = new URLSearchParams({ per_page: "100" });62 if (params.clientId) q.set("client_id", params.clientId);63 if (params.state) q.set("state", params.state);64 resp = await fetch(`${BASE}/invoices?${q}`, { headers: authHeaders });65 break;66 }67 case "get_invoice":68 resp = await fetch(`${BASE}/invoices/${params.invoiceId}`, { headers: authHeaders });69 break;70 case "create_invoice":71 resp = await fetch(`${BASE}/invoices`, {72 method: "POST", headers: authHeaders,73 body: JSON.stringify({74 client_id: params.clientId, subject: params.subject,75 issue_date: params.issueDate, due_date: params.dueDate,76 line_items: params.lineItems,77 notes: params.notes || "",78 }),79 });80 break;81 case "send_invoice":82 resp = await fetch(`${BASE}/invoices/${params.invoiceId}/messages`, {83 method: "POST", headers: authHeaders,84 body: JSON.stringify({85 event_type: "send",86 body: params.body || "Please find your invoice attached.",87 }),88 });89 break;90 case "list_tasks":91 resp = await fetch(`${BASE}/tasks?per_page=100`, { headers: authHeaders });92 break;93 case "list_expenses": {94 const q = new URLSearchParams({ per_page: "100" });95 if (params.clientId) q.set("client_id", params.clientId);96 if (params.from) q.set("from", params.from);97 if (params.to) q.set("to", params.to);98 resp = await fetch(`${BASE}/expenses?${q}`, { headers: authHeaders });99 break;100 }101 default:102 return new Response(JSON.stringify({ error: `Unknown action: ${action}` }), {103 status: 400, headers: { ...corsHeaders, "Content-Type": "application/json" },104 });105 }106107 const data = await resp.json();108 return new Response(JSON.stringify(data), {109 status: resp.status, headers: { ...corsHeaders, "Content-Type": "application/json" },110 });111});Pro tip: Harvest's API returns paginated results with a per_page default of 100 and a maximum of 100. For accounts with many clients or a long time entry history, implement pagination by checking the response's total_pages field and making additional requests with the page parameter.
Expected result: The Edge Function deploys. Calling action 'list_clients' returns your Harvest clients. Calling 'list_invoices' returns invoice history with status, amounts, and client associations.
Build a time entry and invoicing dashboard
Build a time entry and invoicing dashboard
The core value of a Harvest integration is the time-to-invoice workflow. This dashboard shows unbilled time entries grouped by client, with the ability to review entries and generate invoices from them. The uninvoiced time query uses is_billed=false on the time entries endpoint — this filters to only entries that haven't been included in an invoice yet. When building invoice line items from time entries, group entries by project and calculate the subtotal for each group. Each line item needs: kind (typically 'Service'), description (project name and description), quantity (hours), unit_price (hourly rate), and amount (hours × rate). The hourly rate comes from the project's task hourly rate settings in Harvest — fetch these via the project tasks endpoint before building the invoice.
Create a HarvestInvoicingDashboard component. Fetch all clients and for each client with is_active=true, fetch uninvoiced time entries (is_billed=false) for the current month. Display clients in a list with: client name, total uninvoiced hours, total amount (hours × task rate from the time entry's task.default_hourly_rate). Click a client row to expand a detail panel showing all unbilled entries grouped by project and task. Add a 'Generate Invoice' button per client that: creates a Harvest invoice via action 'create_invoice' with line items built from the time entries (one line item per project, quantity = total hours, unit_price = hourly rate from entries), sets issue_date = today, due_date = 30 days out, then calls action 'send_invoice'. Show a success banner with the new invoice ID.
Paste this in Lovable chat
Pro tip: Harvest time entries include a billable_rate field directly on each entry — use this as the unit_price for invoice line items rather than looking up rates separately. The billable_amount field on each entry is pre-calculated as hours × billable_rate.
Expected result: The dashboard shows all clients with unbilled time totals. Expanding a client shows entry details. Clicking Generate Invoice creates a Harvest invoice and triggers the send message. The generated invoice number appears in the success message.
Build a profitability report with expense tracking
Build a profitability report with expense tracking
Beyond time tracking, Harvest's expense tracking completes the profitability picture for project-based businesses. Expenses are logged against projects — travel, software subscriptions, subcontractor costs — and can be included in invoices just like time entries. Your profitability report combines three data sources: paid invoice revenue (what clients paid), time entry labor cost (hours × internal cost rates stored in Supabase), and expenses (what you spent on the project). This three-way comparison gives true project margin: revenue minus all costs. For agencies where different team members have different cost rates (junior vs senior rates), the internal cost calculation requires matching time entry user IDs to cost rates stored in your Supabase database. RapidDev's team can help build a comprehensive profitability model that handles complex rate structures, overhead allocation, and multi-currency scenarios for larger agencies.
Build a profitability view at /profitability. Fetch active projects from Harvest. For each project, fetch: paid invoices (action 'list_invoices' with state='paid'), time entries for the project (action 'list_time_entries'), and expenses (action 'list_expenses' with projectId). From Supabase user_cost_rates table, get internal cost rates by user ID. Calculate: revenue = sum of paid invoice amounts, labor_cost = sum(entry.hours × user_cost_rate), expenses = sum of expense amounts, gross_profit = revenue - labor_cost - expenses, margin_pct = gross_profit / revenue. Display a table sorted by margin_pct with color coding (green >40%, yellow 20-40%, red <20%). Click a row to expand line-item breakdown. Add a date filter for the calculation period.
Paste this in Lovable chat
Pro tip: Harvest invoice states are: 'open' (sent, awaiting payment), 'paid' (payment received), 'draft' (not yet sent), 'overdue' (past due date). Use state='paid' when calculating revenue and state='open' when calculating accounts receivable.
Expected result: The profitability report renders project cards with calculated margins. Color coding accurately reflects financial health. Expanding a row shows the revenue, labor cost, and expense breakdown. The date filter correctly adjusts all calculations.
Common use cases
Automated end-of-month invoice generation from tracked time
An agency tracks all client work in Harvest and manually creates invoices at the end of each month. They want to automate this — at month end, a Lovable dashboard shows all unbilled time by client, and clicking 'Generate Invoice' creates a Harvest invoice with the tracked time as line items and sends it to the client automatically. The Edge Function handles the time-to-invoice workflow entirely through Harvest's API.
Build an invoice generation dashboard at /invoicing. Fetch all clients with uninvoiced time entries for the previous month from my Harvest Edge Function. Display each client with: client name, total uninvoiced hours, total invoiceable amount (hours × project rate). Add a 'Generate Invoice' button per client that calls the Harvest API to create an invoice, sets the issue date to the first of current month, due date to 30 days out, adds line items for each project with hours × rate, and marks it as sent. Show a success message with the invoice URL. Update the client row to 'Invoiced' status after generation.
Copy this prompt to try it in Lovable
Project profitability report combining time and expenses
A consultancy wants a dashboard showing true project profitability — revenue from invoices against the combined cost of time (hours × team member rates) and expenses. The Edge Function fetches invoices (revenue), time entries (labor cost), and expense entries for each project, and the frontend calculates and displays margin, burn rate, and estimated final profitability at project completion.
Build a profitability dashboard at /project-profitability. For each active Harvest project, fetch: total invoiced amount (from invoices with status paid), total time logged with hours and billable amounts, and total expenses. Calculate: gross revenue (paid invoices), labor cost (hours × internal cost rate from Supabase user_costs table), expense total, gross margin = (revenue - labor - expenses) / revenue. Display projects sorted by margin ascending. Color code: green over 40% margin, yellow 20-40%, red under 20%. Show a projected margin if the project finishes on budget. Add a CSV export.
Copy this prompt to try it in Lovable
Client billing portal with invoice history and payment status
Clients want to see their project hours, invoice history, and payment status without needing a Harvest login. A branded Lovable portal fetches the client's data from Harvest via the Edge Function, showing tracked hours this billing period, all invoices with their status (draft, sent, paid, overdue), and a summary of outstanding vs paid amounts — giving clients full billing visibility without exposing your internal Harvest account.
Build a client billing portal at /client-billing/{clientId}. Store client-to-Harvest-client-ID mappings in Supabase. Fetch invoices for the client from my Harvest Edge Function. Display: an invoice history table with columns for invoice number, date, due date, amount, and status badge (Draft/Sent/Paid/Overdue). Show outstanding balance total in a prominent card. Show time tracked this month for any active projects. Add a 'View Invoice' button per row that shows the invoice line items in a modal. Show a payment trend chart for the past 12 months.
Copy this prompt to try it in Lovable
Troubleshooting
API returns 401 Unauthorized with 'HTTP Token: Access denied'
Cause: Either the HARVEST_ACCESS_TOKEN secret is incorrect or expired, the Harvest-Account-ID header is missing or contains the wrong value, or both headers are required and only one is being set.
Solution: Verify both headers are being sent: Authorization: Bearer YOUR_TOKEN and Harvest-Account-ID: YOUR_ACCOUNT_ID. The account ID is a numeric string visible at id.getharvest.com/oauth2/access_tokens next to your token. Both are required on every request — omitting either one causes authentication to fail.
1const authHeaders = {2 Authorization: `Bearer ${token}`,3 "Harvest-Account-ID": accountId, // Required alongside Bearer token4 "Content-Type": "application/json",5};Invoice creation fails with 'Client not found' even with a valid client ID
Cause: The client_id used in invoice creation is a Harvest client ID, but the value being passed is a different ID (e.g., from a Supabase record or a display ID).
Solution: Fetch the client list via action 'list_clients' to get the correct Harvest-internal numeric client IDs. Harvest client IDs are typically 7-8 digit integers. Store the mapping of your app's clients to their Harvest client IDs in Supabase so the invoice creation always uses the correct Harvest ID.
Time entries list is missing entries that exist in Harvest
Cause: Harvest paginates time entry responses at 100 per page by default. If a date range has more than 100 entries, subsequent pages are not returned.
Solution: Add per_page=100 to all time entry requests (already the max) and check the response's total_entries field. If total_entries > 100, implement pagination: make additional requests with page=2, page=3, etc., until all entries are fetched. For monthly billing cycles, most clients won't exceed 100 entries unless you're fetching across an entire team.
1// Check if pagination needed2const data = await resp.json();3if (data.total_entries > data.per_page) {4 // Fetch remaining pages5 const totalPages = Math.ceil(data.total_entries / data.per_page);6 for (let page = 2; page <= totalPages; page++) {7 const pageResp = await fetch(`${url}&page=${page}`, { headers: authHeaders });8 const pageData = await pageResp.json();9 data.time_entries.push(...pageData.time_entries);10 }11}Invoice send action returns success but client never receives the email
Cause: The invoice message send action (POST /invoices/{id}/messages) creates a Harvest message event, but Harvest's email delivery depends on the client's email being correctly configured in Harvest, and the 'send' event type being specified in the request body.
Solution: Verify the client record in Harvest has a valid email address configured. Check the request body includes event_type: 'send' explicitly. Review the invoice in Harvest's native interface to confirm the message was created (it shows in the invoice's message history even if email delivery failed). For email delivery failures, check the client's spam folder and confirm their domain isn't blocking Harvest's sending domain.
Best practices
- Always include both the Authorization Bearer header and the Harvest-Account-ID header on every request — Harvest requires both for all API calls, and omitting the account ID header is the most common authentication mistake.
- Use the is_billed filter when fetching time entries for invoice generation to ensure you're only billing for unbilled hours — running the same invoicing logic twice would double-bill clients without this filter.
- Store Harvest client IDs and project IDs in your Supabase database mapped to your app's client records, so the invoice creation workflow always uses correct Harvest IDs without requiring API lookups.
- Always validate invoice line items before submitting — each line item needs kind, description, quantity, unit_price, and amount. Missing fields cause silent validation failures where the invoice creates with incorrect totals.
- Implement pagination handling for time entry fetches — monthly billing cycles for active teams can exceed the 100-entry limit, and missing entries results in underbilling.
- Use Harvest's is_active filter when listing projects and clients to exclude archived records from your UI, keeping the interface clean without additional client-side filtering.
- Cache client and project lists with a 15-minute TTL but always fetch time entries and invoice status in real time — the former changes rarely, but billing data must always be current to avoid financial errors.
Alternatives
Everhour focuses on project budget tracking with deep Asana and Jira integrations, without Harvest's built-in invoice generation and expense management.
Clockify offers a generous free tier for time tracking without the invoicing and expense features that make Harvest valuable for client billing workflows.
QuickBooks provides comprehensive accounting and invoicing as a full financial platform — better when you need double-entry bookkeeping alongside time-based billing.
Frequently asked questions
Does Harvest have a native connector in Lovable?
No, Harvest is not one of Lovable's 17 shared connectors. Integration requires Edge Functions proxying the Harvest REST API v2. Both your personal access token and account ID are stored in Cloud Secrets — the dual-header authentication pattern Harvest uses requires both values to be set correctly on every request.
Can I generate invoices automatically without manually clicking a button?
Yes — you can automate invoice generation using Supabase scheduled triggers (database functions that run on a schedule via pg_cron) or Lovable's scheduled Edge Functions. Set up a function that runs on the first day of each month, fetches all unbilled time entries from the previous month, groups them by client, creates invoices, and sends them — all without manual intervention. Store the invoice IDs in Supabase for tracking and audit purposes.
What Harvest plan is required for API access?
Harvest's API is available on all paid plans (Starter at $12/month, Pro at $12/seat/month). The free 30-day trial includes API access. There is no permanent free plan with API access. If you're evaluating the integration, the free trial gives you enough time to build and test before committing to a paid plan.
How do I handle multiple currencies if I invoice clients in different currencies?
Harvest supports multi-currency invoicing — each client can have a default currency, and invoices are created in that client's currency. The time entry billable_rate is stored in your account's base currency, so when creating invoices for clients in other currencies, you'll need to apply exchange rates to the line item amounts. Harvest does not perform currency conversion automatically — store exchange rates in Supabase and apply them during invoice line item calculation.
Can I track expenses against specific Harvest projects from Lovable?
Yes — the Harvest API's expense endpoints support creating expense entries with a project ID, category, amount, spent_date, and notes. Use the 'create_expense' action pattern (POST /expenses) with the appropriate project and category IDs. Expense categories are fetched via GET /expense_categories. Expenses logged via the API appear in Harvest's reporting and can be included in invoices alongside time entries.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation