Connect Bubble to Harvest by configuring three required shared headers in Bubble's API Connector: Authorization Bearer token (Private), Harvest-Account-Id, and User-Agent. Missing the Harvest-Account-Id header causes a 422 error on every call. Bubble proxies all calls server-side so CORS is not an issue. Always filter time entries by date range — fetching without dates returns the entire account history.
| Fact | Value |
|---|---|
| Tool | Harvest |
| Category | Productivity |
| Method | Bubble API Connector |
| Difficulty | Beginner |
| Time required | 1–2 hours |
| Last updated | July 2026 |
Harvest's Three-Header Auth Pattern and Why Date Filtering Is Mandatory
Most API integrations in Bubble require one authentication header. Harvest requires three — and missing any one of them causes the API to reject your request. This is the #1 reason Bubble developers struggle with Harvest setup: they configure the Bearer token correctly but forget either the Harvest-Account-Id or the User-Agent header.
Here is why all three are required: the Authorization Bearer token identifies who you are (your personal access token). The Harvest-Account-Id is a separate numeric identifier for the specific Harvest account you are querying — this matters because some users belong to multiple Harvest accounts and the token alone is ambiguous. The User-Agent header is required by Harvest's API Terms of Service; requests without it may be rejected. All three go in the 'Shared headers' section of your API Connector group so they are sent on every call automatically.
Because Bubble's API Connector runs server-side, your token never appears in browser network traffic. The Private checkbox on the Authorization header enforces this at the Bubble level — Bubble strips it from any client-side code and only sends it from its own servers. CORS is not a concern.
The second major Harvest-specific consideration is date filtering. The GET /time_entries endpoint, without any date parameters, returns every time entry ever logged in the account — potentially thousands of rows going back years. A Bubble Repeating Group trying to display all of this at once will hang or time out. Always pass from and to date parameters (YYYY-MM-DD format) when initializing and using the time entries call. Bubble's DatePicker element makes this easy to bind to a user-selected date range.
The most common Bubble + Harvest use case is a client-facing time portal: an agency builds a Bubble app where clients log in and see the hours logged against their projects, along with outstanding invoices. The entire integration — time entries, projects, and invoices — can be built with GET calls only, no write operations needed for this pattern.
Integration method
Harvest REST API v2 called from Bubble's API Connector with a Bearer token (Private), Harvest-Account-Id, and User-Agent as shared headers — all requests run server-side through Bubble's servers.
Prerequisites
- A Harvest account on the Grow plan ($11/seat/mo) or Pro plan — the Free plan (1 seat, 2 projects, 1 client) may be sufficient for testing but has limited data
- Your Harvest Personal Access Token — go to Harvest → your name in the top right → My Profile → Developers → Create new personal access token → copy the token string
- Your Harvest Account ID — found on the same Developers page, labeled 'Account ID'; this is a 6-7 digit number, NOT the same as the token
- A Bubble app with the API Connector plugin installed — Plugins tab → Add plugins → search 'API Connector' → Install
Step-by-step guide
Locate Your Harvest Token and Account ID
Before opening Bubble, collect both credentials from Harvest — they are on the same page but are distinct values that serve different purposes. Log in to Harvest at app.harvestapp.com. Click your name or avatar in the top-right corner → 'My Profile'. Scroll down to the 'Developers' section. You will see two important items on this page: 1. Personal Access Tokens: click 'Create new personal access token' → give it a name like 'Bubble Integration' → copy the token string. It is a long alphanumeric string. This is your Authorization Bearer token. 2. Account ID: just below the token section you will see a field labeled 'Account ID' — it is a 6-7 digit number like 1234567. This is your Harvest-Account-Id. Copy it separately. These two values look nothing alike: the token is long and complex, the Account ID is a short number. Both are required. A common mistake is copying only the token and assuming the Account ID is the same thing — it is not. Note also your user_id on this page if you want to filter time entries to specific team members later.
Pro tip: Store both the token and the Account ID in a secure password manager or note before leaving this page. You do not need to rotate Harvest personal access tokens regularly, but if you ever regenerate it, you must update the Private header in Bubble's API Connector.
Expected result: You have two separate values ready: your Harvest personal access token (long alphanumeric string) and your numeric Account ID.
Create the API Connector Group with Three Required Headers
This step sets up the Harvest connection in Bubble. The three shared headers you configure here will automatically be included on every Harvest API call in this group. In your Bubble editor, go to Plugins → Add plugins → search 'API Connector' → Install. Once installed, click 'API Connector' in the plugins list. Click 'Add another API'. Name it 'Harvest'. In the 'Shared headers' section, add these three headers in order: Header 1 — Authorization: Key: Authorization Value: Bearer YOUR_HARVEST_TOKEN (replace with your actual token) Check the 'Private' checkbox — this is mandatory. Marking it Private means Bubble sends this header from its servers only and it never appears in browser requests. Header 2 — Harvest Account ID: Key: Harvest-Account-Id Value: YOUR_NUMERIC_ACCOUNT_ID (the 6-7 digit number, e.g. 1234567) Do NOT check Private for this one — it is not sensitive, just a routing identifier. Header 3 — User Agent: Key: User-Agent Value: BubbleApp (your-email@company.com) Replace with your actual email. Harvest's API terms require a descriptive User-Agent that identifies your application. Leave the base URL as the default blank for now — you will set the full URL per call. Click 'Save' on the group before adding calls.
1{2 "shared_headers": [3 {4 "key": "Authorization",5 "value": "Bearer YOUR_HARVEST_TOKEN",6 "private": true7 },8 {9 "key": "Harvest-Account-Id",10 "value": "YOUR_NUMERIC_ACCOUNT_ID",11 "private": false12 },13 {14 "key": "User-Agent",15 "value": "BubbleApp (your-email@company.com)",16 "private": false17 }18 ]19}Pro tip: If you configure only the Authorization header and forget Harvest-Account-Id, every API call will return a 422 'Account Not Found' error — not a 401. This confuses many developers because 422 looks like a data error, not an auth error. The Account ID is the fix.
Expected result: The Harvest API group exists in your API Connector with all three shared headers configured. The Authorization header has the Private checkbox checked.
Add and Initialize the Time Entries Call
The time entries call is the core of most Harvest + Bubble integrations. It fetches logged hours filtered by date range and optionally by project or user. In the Harvest API group, click 'Add a call'. Name it 'Get Time Entries'. Method: GET URL: https://api.harvestapp.com/v2/time_entries In the 'Parameters' section (query string params), add: - Key: from, Value: 2024-01-01 (a real date for initialization) - Key: to, Value: 2024-01-31 (a real date for initialization) You can also add optional filters: - Key: project_id, Value: YOUR_PROJECT_ID (limits results to one project) - Key: user_id, Value: YOUR_USER_ID (limits to one team member) Set 'Use as' to 'Data' — you want to display this in a Repeating Group. Click 'Initialize call'. Bubble sends the request with your shared headers and the date params. If successful, you will see the time_entries array detected with fields including: id, spent_date, hours, notes, is_billable, and nested objects like project (id, name, code) and user (id, name). Hours are returned as decimal floats: 1.5 means 1 hour 30 minutes. To display hours and minutes in Bubble, multiply by 60 to get minutes, or use a calculated field. The spent_date comes back as a string in YYYY-MM-DD format. After initialization, click Save. You can now use this call as a Repeating Group data source.
1GET https://api.harvestapp.com/v2/time_entries?from=2024-01-01&to=2024-01-31&project_id=12345678Pro tip: For the Initialize call, use a real date range that you know has time entries in your Harvest account. If no time entries exist in the range you specify, Bubble gets back an empty array and cannot detect the schema fields — initialize with a date range that has actual data.
Expected result: The 'Get Time Entries' call initializes successfully and Bubble detects the time_entries array with fields including spent_date, hours, notes, and nested project and user objects.
Add Calls for Projects and Invoices
Add two more GET calls to complete the integration: one for project listings (useful for dropdowns) and one for invoices (useful for billing portals). Projects call: Add a call named 'Get Projects'. Method: GET. URL: https://api.harvestapp.com/v2/projects. Optional params: - is_active: true (filter to only active projects) - client_id: YOUR_CLIENT_ID (filter to one client's projects) Set 'Use as' to 'Data'. Initialize call — Bubble detects the projects array with fields: id, name, code, client (id, name), budget, cost_budget, is_active, starts_on, ends_on. Save. Invoices call: Add a call named 'Get Open Invoices'. Method: GET. URL: https://api.harvestapp.com/v2/invoices. Params: - state: open (only unpaid invoices) You can also use state: draft, pending, paid, or omit to get all. Set 'Use as' to 'Data'. Initialize call — Bubble detects: id, client_key, number, amount, due_amount, due_date, state, payment_url, client (id, name). With these three calls you have everything needed for a client portal: GET /projects feeds a filter dropdown, GET /time_entries filtered by date and project feeds the main data table, and GET /invoices shows outstanding amounts. Important for multi-tenant apps: your personal token shows ALL time entries and invoices across ALL clients. If you are building an app where each client logs in and sees only their data, you must filter by project_id or client_id on the server side using Bubble's workflow parameters — do not rely on hiding data in the UI.
1GET https://api.harvestapp.com/v2/projects?is_active=true2GET https://api.harvestapp.com/v2/invoices?state=openPro tip: Harvest's API returns paginated results with a 'next_page' cursor for large datasets. If you have more than 100 time entries or projects, you will only see the first 100 by default. Add a 'per_page' param set to 100 and handle pagination in a Backend Workflow if you need full history.
Expected result: You have three initialized calls in the Harvest group: Get Time Entries, Get Projects, and Get Open Invoices — all working and showing detected schema fields in the API Connector.
Wire Calls to Repeating Groups with Date Filtering
Now connect your Harvest API calls to actual Bubble UI elements to create the working time portal. Date filter inputs: on your page, add two DatePicker elements — name them 'DateFrom' and 'DateTo'. These let users select the time range they want to view. Repeating Group for time entries: drag a Repeating Group onto the page. Set its Type of content to 'Harvest - Get Time Entries' (Bubble creates a data type based on your API call schema). In the Data source, select 'Get data from an external API' → 'Harvest - Get Time Entries'. In the call parameters: - from: DateFrom's value formatted as 'YYYY-MM-DD' (use Bubble's ':formatted as' date operator → custom format → YYYY-MM-DD) - to: DateTo's value formatted as 'YYYY-MM-DD' In the Repeating Group cell, add Text elements: - 'Current cell's Get Time Entries's spent_date' - 'Current cell's Get Time Entries's hours' (displays as a decimal number like 1.5) - 'Current cell's Get Time Entries's project's name' - 'Current cell's Get Time Entries's notes' Project dropdown: add a Dropdown element. Type of choices: Harvest - Get Projects. Choice label: option's name. Choice value: option's id. On change, refresh the Repeating Group's data source with the new project_id parameter. For WU economy: avoid binding the Repeating Group to a data source that fires on page load without a date range. Default to the current week (DateFrom = current week start, DateTo = today) so the Repeating Group always loads with a reasonable, bounded dataset rather than potentially thousands of rows. Privacy rules: if you store any Harvest data in Bubble's database, go to Data → Privacy → add rules on the data type so only the appropriate users can access it. RapidDev's team has helped dozens of agencies build exactly this kind of client portal — book a free scoping call at rapidevelopers.com/contact if you need architecture guidance.
Pro tip: Bubble's Date formatting operator (':formatted as') with the pattern 'YYYY-MM-DD' produces the exact date string Harvest expects. If you send a date in any other format (like MM/DD/YYYY), Harvest returns a 422 validation error on the date parameter.
Expected result: A working Bubble page with DatePicker inputs, a project Dropdown, and a Repeating Group that displays time entries from Harvest filtered by the selected date range and project. The page is fully functional in Bubble's preview mode.
Common use cases
Client-Facing Time and Invoice Portal
An agency builds a Bubble app where each client can log in and view the hours logged against their retainer or project, broken down by week. A separate tab shows their outstanding invoices fetched from Harvest. The client never needs a Harvest login — the Bubble app surfaces only the data relevant to them via server-side project ID filtering.
Write Bubble API Connector configuration for GET /time_entries filtered by a specific project_id and a date range from the first of this month to today, returning spent_date, hours, notes, and the user's name for each entry.
Copy this prompt to try it in Bubble
Internal Team Hours Dashboard
An internal Bubble dashboard lets a studio director see all time entries across active projects for the current week. A Dropdown lets them switch between projects fetched from GET /projects. A summary shows total hours billed per project using Bubble's sum expression on the Repeating Group.
Show me a Bubble workflow that first calls GET /projects to populate a Dropdown, then on Dropdown change, calls GET /time_entries filtered by the selected project_id and the current week's date range.
Copy this prompt to try it in Bubble
Automated Invoice Status Tracker
A Bubble app polls GET /invoices?state=open on a recurring basis (using a scheduled Backend Workflow on paid plans) and stores open invoice data in a Bubble database. The app sends a notification when invoice status changes to 'paid'. This avoids the WU cost of live API calls on every page view.
Write the Bubble API Connector call for GET /invoices filtered to state=open, showing the fields client_name, invoice_number, amount, due_date, and state that I need to map in the Initialize call.
Copy this prompt to try it in Bubble
Troubleshooting
Every Harvest API call returns 422 'Account Not Found' regardless of credentials
Cause: The Harvest-Account-Id shared header is missing or contains the wrong value. This is the #1 setup failure — the numeric Account ID is not the same as the API token and must be sent as a separate header on every request.
Solution: In API Connector → Harvest group → Shared Headers, verify that the 'Harvest-Account-Id' header exists with the correct 6-7 digit numeric value from your Harvest profile → Developers page. The Account ID looks like 1234567, not a long alphanumeric string. Add or correct this header and re-initialize your calls.
API returns 401 Unauthorized even with the correct token
Cause: The Authorization header value is missing the 'Bearer ' prefix, or the Private checkbox is unchecked causing the header to be stripped from the API Connector's server-side call.
Solution: Check the Authorization shared header value — it must be exactly 'Bearer YOUR_TOKEN' with a space after 'Bearer'. Confirm the Private checkbox is checked. If the token was recently regenerated in Harvest, update the header value and re-initialize all calls.
'There was an issue setting up your call' during Initialize on the time entries call
Cause: The Initialize call uses date parameters with no actual time entries in that range, so Harvest returns an empty array. Bubble cannot detect field schemas from an empty array response.
Solution: Change the from and to initialization values to a date range that definitely has logged time in your Harvest account — for example, the previous full month. Once Bubble detects the schema from a non-empty response, the initialization saves. After initialization you can use empty ranges at runtime with no issue.
Repeating Group hangs or loads very slowly when showing Harvest time entries
Cause: GET /time_entries was called without date range parameters, causing Harvest to return the entire account history — potentially thousands of entries across many pages.
Solution: Always pass from and to date parameters in the Repeating Group's data source call. Set default values (e.g., current week start and today) so the parameters are never empty. Add a Bubble Condition on the Repeating Group: only show data when both DateFrom and DateTo inputs have values to prevent accidental unfiltered queries.
Best practices
- Always mark the Authorization Bearer token as Private in the API Connector shared headers — your personal Harvest token grants access to your entire Harvest account and must never appear in browser network requests.
- Include all three required headers — Authorization, Harvest-Account-Id, and User-Agent — in the shared headers section; missing any one will cause 422 or 401 errors on every call, regardless of how correct the other headers are.
- Always filter GET /time_entries with from and to date parameters — fetching without date bounds returns the complete account history and will cause Bubble Repeating Groups to hang on large accounts.
- Format Bubble Date values using ':formatted as' with 'YYYY-MM-DD' pattern before passing to Harvest date parameters — Harvest rejects dates in any other format with a 422 validation error.
- Display Harvest hours (decimal floats like 1.75) with a calculated label showing hours and minutes — multiply by 60 for total minutes, then divide and floor for the minutes component; raw decimals are not intuitive for clients.
- Filter time entries by project_id server-side when building multi-tenant client portals — never rely on hiding data in the UI; use Bubble workflow parameters to pass only the relevant project IDs per logged-in user.
- Cache static Harvest data (project names, client names) in a Bubble database table updated by a scheduled Backend Workflow rather than fetching it live on every page load — this reduces WU consumption and speeds up initial page render.
- Set Privacy rules in Bubble's Data tab on any Harvest data types you store in the database — this prevents unauthorized users from accessing other clients' project or invoice data through Bubble's Data API.
Alternatives
Clockify offers a free unlimited time tracking plan with a similar REST API. If your clients or team are on a budget and do not need Harvest's invoicing features, Clockify is a lower-cost alternative with an easier single-header auth setup.
Xero focuses on accounting and invoicing rather than time tracking. If invoice management is the primary need rather than hour logging, Xero provides more comprehensive financial reporting and invoice workflows through its Bubble integration.
QuickBooks Online covers invoicing, expense tracking, and basic time tracking with broader accounting features than Harvest. If your clients need full bookkeeping alongside time tracking, QuickBooks may be the better backend for a Bubble finance portal.
Frequently asked questions
Why does Harvest need three headers when most APIs only need one?
Harvest requires the Authorization token to identify your credentials, the Harvest-Account-Id because some users belong to multiple Harvest accounts (the token alone does not specify which account), and the User-Agent to comply with Harvest's API Terms of Service that require apps to identify themselves. All three must be present or Harvest rejects the request.
Can I use this integration to write time entries into Harvest from Bubble?
Yes — Harvest's API supports POST /time_entries to create new entries and PATCH /time_entries/{id} to update them. Configure these as Action-type calls in the API Connector with spent_date, hours, project_id, and task_id in the JSON body. The same three shared headers apply to write calls.
Does Harvest have webhooks I can receive in Bubble?
Yes — Harvest supports webhooks for time entries, invoices, and other events. To receive them in Bubble, you need a Backend Workflow configured as a public API endpoint. Backend Workflows are available on paid Bubble plans only. Register the Bubble endpoint URL in Harvest under Settings → Integrations → Webhooks.
Will multiple clients see each other's time data if they use my Bubble app?
Only if you do not filter the API calls. Your personal Harvest token returns all data across all projects. Always pass project_id or client_id parameters in your API Connector calls based on which project the logged-in Bubble user should see. Store the mapping of Bubble User → Harvest client/project ID in your Bubble database and use it as a filter parameter.
How should I display hours like 1.75 as '1h 45m' in Bubble?
Bubble math expressions let you calculate this. Create two calculated values: hours_whole = floor(hours), and minutes = round((hours - floor(hours)) × 60). Then display as hours_whole + 'h ' + minutes + 'm'. Alternatively, store and display hours as a simple decimal and add a tooltip explaining the format.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation