Connect Retool to Harvest using a REST API Resource with your personal access token and account ID in the request headers. Query time entries, projects, and invoices to build team utilization dashboards with billable vs non-billable breakdowns and reporting that Harvest's native reports cannot customize.
| Fact | Value |
|---|---|
| Tool | Harvest |
| Category | Productivity |
| Method | REST API Resource |
| Difficulty | Beginner |
| Time required | 15 minutes |
| Last updated | April 2026 |
Build Time Tracking and Utilization Dashboards in Retool with Harvest
Harvest's built-in reports provide useful summaries, but they are limited in customization and cannot easily combine time tracking data with other internal data sources. Retool solves this by connecting directly to Harvest's API, enabling custom dashboards that show exactly the metrics your operations or finance team needs.
Harvest's API v2 covers the full platform: time entries with project and task associations, projects and clients, team members, tasks, invoices, and estimates. The API uses cursor-based pagination and returns up to 100 records per request. Authentication is simple — a personal access token tied to a user account, combined with your Harvest account ID, both passed as headers with every request.
Common Retool apps built on Harvest include: team utilization dashboards showing billable percentage per team member, project profitability panels comparing budgeted vs actual hours, invoice tracking dashboards for finance teams monitoring outstanding receivables, and capacity planning tools that combine Harvest time data with project management data from other sources.
Integration method
Harvest's API v2 uses personal access token authentication with your account ID passed in headers. Configure a Retool REST API Resource with the Harvest API base URL and these two headers, then query time entries, projects, clients, and invoices. All requests are proxied server-side through Retool, keeping credentials secure.
Prerequisites
- A Harvest account with API access (available on all plans including Free)
- A Harvest personal access token (generated in Harvest Developer Settings)
- Your Harvest Account ID (visible in your profile or API documentation)
- A Retool account with permission to create Resources
Step-by-step guide
Generate a Harvest personal access token
Log into your Harvest account. Navigate to your profile settings by clicking your name in the top-left corner, then selecting 'My Profile'. Scroll to the bottom of the profile page to find the 'Developers' section. Click 'Create New Personal Access Token'. Give the token a descriptive name like 'Retool Integration' and click 'Create Personal Access Token'. Harvest displays the token value once — copy it immediately. You also need your Harvest Account ID: this is displayed in the same Developers section as 'Account ID', typically a 6-7 digit number. Copy both values. Unlike some API integrations, Harvest personal access tokens do not expire — they remain valid until you revoke them in this same settings page. Personal access tokens have the same access level as your Harvest account, so use a dedicated account or be aware that all requests will be attributed to you.
Pro tip: Your Harvest Account ID is also visible in the URL when logged into Harvest: harvest.com/accounts/YOUR_ACCOUNT_ID. Note this number as you need it for every API request.
Expected result: You have a Harvest personal access token and account ID copied and ready to configure in Retool.
Create the Harvest REST API Resource in Retool
In Retool, go to Resources → Create New → REST API. Set the Base URL to 'https://api.harvestapp.com/v2'. This is Harvest's API v2 base endpoint. Under the Headers section, add three required headers: first, 'Authorization' with value 'Bearer YOUR_TOKEN' (use {{ retoolContext.configVars.HARVEST_TOKEN }} once your config variable is set), second 'Harvest-Account-Id' with your account ID number ({{ retoolContext.configVars.HARVEST_ACCOUNT_ID }}), and third 'User-Agent' with a descriptive value like 'Retool Integration (yourteam@company.com)'. The User-Agent header is required by Harvest's API — requests without it may be rejected. Set up configuration variables in Retool Settings → Configuration Variables before saving the resource, marking the token as secret. Click 'Save Resource'. The test connection should return a 200 OK response.
1{2 "Base URL": "https://api.harvestapp.com/v2",3 "Headers": {4 "Authorization": "Bearer {{ retoolContext.configVars.HARVEST_TOKEN }}",5 "Harvest-Account-Id": "{{ retoolContext.configVars.HARVEST_ACCOUNT_ID }}",6 "User-Agent": "Retool-Harvest-Integration (your-email@company.com)",7 "Content-Type": "application/json"8 }9}Pro tip: The Harvest-Account-Id header is required for every request — without it, Harvest returns a 422 error even if the access token is valid. Do not forget this header when configuring the resource.
Expected result: The Harvest REST API Resource is saved with all three required headers. A test GET to /users/me returns your Harvest user profile with a 200 response.
Query time entries with date and user filters
Create a new query using the Harvest resource. Set method to GET and path to '/time_entries'. Harvest's time entries endpoint accepts query parameters for filtering: 'from' and 'to' accept date strings in YYYY-MM-DD format for date range filtering, 'user_id' filters by a specific team member, 'project_id' filters by project, 'billable' accepts true or false to filter billable/non-billable entries, and 'per_page' sets page size up to 100. Add a DateRangePicker component to the Retool app and bind 'from' to its start value and 'to' to its end value. The response contains a 'time_entries' array and 'pagination' object with total_count and next_page for pagination. Each time entry includes hours, notes, the associated project (as a nested object), task, and user. Run the query to verify the data structure.
1// GET /time_entries2// Query parameters:3// from: {{ dateRange.start || new Date(new Date().getFullYear(), new Date().getMonth(), 1).toISOString().split('T')[0] }}4// to: {{ dateRange.end || new Date().toISOString().split('T')[0] }}5// user_id: {{ userFilter.value || '' }}6// project_id: {{ projectFilter.value || '' }}7// billable: {{ billableFilter.value || '' }}8// per_page: 1009// page: {{ pagination.page || 1 }}Pro tip: The default date range without filters fetches all time entries, which may be thousands of records. Always include 'from' and 'to' parameters defaulting to the current month to keep response times reasonable.
Expected result: The query returns a time_entries array with entries for the specified date range. Each entry shows hours logged, the project name, task name, user, and billable flag.
Build a utilization summary transformer and charts
Add a transformer to the time entries query to aggregate hours by user and calculate utilization metrics. Group entries by user ID and sum up billable and non-billable hours separately. Calculate the billable percentage for each user. This produces a summary array that can drive both a summary Table and a Chart component. Add the transformer to the query's Advanced tab. Create two views in your Retool app: a detail view showing the raw time entry table, and a summary view showing the per-user aggregation. Add a Bar Chart component bound to the summary transformer output, with the x-axis as user names and bars for billable vs non-billable hours. Add a target line at 80% billable rate using a custom reference line in the Chart settings.
1// Transformer: aggregate time entries by user2const entries = data.time_entries || [];3const userMap = {};45entries.forEach(entry => {6 const userId = entry.user.id;7 const userName = entry.user.name;89 if (!userMap[userId]) {10 userMap[userId] = {11 user_id: userId,12 user_name: userName,13 total_hours: 0,14 billable_hours: 0,15 non_billable_hours: 016 };17 }1819 userMap[userId].total_hours += entry.hours;20 if (entry.billable) {21 userMap[userId].billable_hours += entry.hours;22 } else {23 userMap[userId].non_billable_hours += entry.hours;24 }25});2627return Object.values(userMap).map(user => ({28 ...user,29 total_hours: Math.round(user.total_hours * 100) / 100,30 billable_hours: Math.round(user.billable_hours * 100) / 100,31 non_billable_hours: Math.round(user.non_billable_hours * 100) / 100,32 billable_pct: user.total_hours > 033 ? `${Math.round((user.billable_hours / user.total_hours) * 100)}%`34 : '0%'35})).sort((a, b) => b.billable_hours - a.billable_hours);Pro tip: Hours in Harvest are returned as decimal numbers (e.g., 1.5 = 1 hour 30 minutes). Round to 2 decimal places in your transformer using Math.round(hours * 100) / 100 to avoid floating point display issues.
Expected result: The Chart displays a grouped bar chart showing billable and non-billable hours per team member. The utilization Table shows each person's billable percentage for the selected date range.
Add invoice tracking and project queries
Create two additional queries for the finance and project management views. For invoices, use GET method with path '/invoices' and filter parameters: 'state' accepts 'open', 'partial', 'draft', 'paid', 'unpaid', 'overdue'. The invoice response includes client name, amount, due_on date, and amount_due fields. Add a transformer that calculates days_overdue for open invoices: Math.max(0, Math.floor((Date.now() - new Date(invoice.due_on)) / 86400000)). For the projects view, use GET '/projects' which returns project names, budgets, budget_by (hours vs money), and fee amounts. Combine project data with time entry data using a JavaScript query that joins on project ID to show hours logged vs budget remaining per project.
1// GET /invoices2// Query parameters:3// state: {{ invoiceStateFilter.value || 'open' }}4// per_page: 10056// Transformer: format invoice data7const invoices = data.invoices || [];8return invoices.map(inv => ({9 id: inv.id,10 invoice_number: inv.number,11 client: inv.client.name,12 amount: `$${parseFloat(inv.amount).toFixed(2)}`,13 amount_due: `$${parseFloat(inv.due_amount).toFixed(2)}`,14 issue_date: inv.issue_date,15 due_date: inv.due_on,16 status: inv.state,17 days_overdue: inv.state === 'open' && inv.due_on < new Date().toISOString().split('T')[0]18 ? Math.floor((Date.now() - new Date(inv.due_on)) / 86400000)19 : 020})).sort((a, b) => b.days_overdue - a.days_overdue);Pro tip: The Harvest invoices endpoint returns amount_due as 0 for fully paid invoices. Use the 'state' field ('paid', 'open', 'partial') rather than checking amount_due to determine payment status.
Expected result: The invoices tab shows all open invoices sorted by days overdue, with total outstanding amount displayed as a summary metric above the table.
Common use cases
Build a team utilization and billable hours dashboard
Query all time entries for the current month grouped by team member. Calculate each person's total hours, billable hours, non-billable hours, and utilization percentage. Display in a Retool Table with charts showing individual and team-level billable rates, helping managers identify underutilized team members or overbilling risks.
Build a Retool team utilization dashboard showing each team member's total hours, billable hours, and billable percentage for the current month, with a bar chart comparing utilization rates across the team and a target line at 80%.
Copy this prompt to try it in Retool
Create a project budget and burn rate tracker
Pull all time entries for active projects and compare total logged hours against the project's budgeted hours. Display a progress bar for each project showing budget consumed percentage. Flag projects where burn rate suggests they will exceed budget before the deadline.
Create a Retool project health dashboard showing all active Harvest projects with budget hours, logged hours, remaining hours, and a percentage bar. Highlight in red any project where logged hours exceed 85% of the budget.
Copy this prompt to try it in Retool
Build an invoice tracking and accounts receivable panel
Query all open invoices from Harvest and display outstanding amounts by client, due date, and invoice age. Build a finance team tool that shows overdue invoices, total accounts receivable, and historical payment patterns — providing visibility into cash flow without requiring access to Harvest's billing module.
Build a Retool invoice dashboard showing all Harvest invoices with status 'open' or 'partial', sorted by due date ascending, with columns for client name, invoice amount, amount due, and days overdue. Include a total outstanding amount at the top.
Copy this prompt to try it in Retool
Troubleshooting
API returns 422 Unprocessable Entity on every request
Cause: The Harvest-Account-Id header is missing, invalid, or formatted incorrectly. This header is mandatory for Harvest API v2 — every request requires it alongside the Authorization token.
Solution: Verify the Harvest Account ID in your Harvest profile settings (My Profile → Developers section). The ID is a numeric value, typically 6-7 digits. Ensure it is configured as a plain number in the header, not wrapped in quotes or prefixed with text. Check for accidental whitespace in the configuration variable.
Time entries query returns fewer records than expected
Cause: Harvest paginates responses at 100 records per page. If your date range contains more than 100 time entries, only the first page is returned by default. The pagination.total_count in the response will indicate the actual total.
Solution: Check the pagination object in the response: data.pagination.total_pages > 1 means there are more pages. Implement pagination in Retool using a pagination component connected to the 'page' query parameter. For bulk data exports, use Retool Workflows to loop through all pages and store results in a temporary database table.
User IDs in time entries do not match the IDs returned by the /users endpoint
Cause: Harvest uses different user ID representations in different contexts. The user.id in time_entries is the Harvest platform user ID, which matches /users endpoint IDs. If you are seeing mismatches, you may be comparing against email addresses or display names rather than numeric IDs.
Solution: Always join on the numeric user.id field. Run a /users query to get all team members and their IDs, then use this as a lookup table to enrich time entry data with user details like email and role.
Best practices
- Store your Harvest personal access token in Retool configuration variables as a secret — never include it directly in query headers
- Always include date range filters (from/to parameters) in time entry queries — open-ended queries on large Harvest accounts can return thousands of entries and cause slow load times
- Cache time entry queries in Retool's query caching settings for 15-30 minutes — time tracking data does not change in real time and does not need to be fetched on every component interaction
- Use Retool Workflows for generating end-of-week or end-of-month reports that aggregate large date ranges — Workflows handle pagination automatically and can export to Google Sheets or email
- Build a users lookup query that runs once when the app loads and stores user IDs and names — use this to populate dropdown filters rather than asking users to know Harvest user IDs
- Include both billable and non-billable hours in utilization calculations to give an accurate picture of total team capacity, not just client-facing work
Alternatives
Everhour integrates directly into project management tools like Asana and Jira, making it better suited if your team tracks time within their existing PM workflow rather than as a standalone tool.
Clockify is free for unlimited users and provides a more accessible API for teams that need basic time tracking without Harvest's invoicing features.
Asana provides task management with time estimate fields and can be combined with Harvest via Retool for teams that want to connect project planning with actual time tracking in one dashboard.
Frequently asked questions
Does Retool have a native Harvest connector?
No, Retool does not have a native Harvest connector. You connect using a REST API Resource with a personal access token. Harvest's API v2 is well-documented and straightforward, and the REST API Resource approach provides access to all Harvest data including time entries, projects, invoices, clients, and team members.
Can I create and update time entries from Retool?
Yes. Use POST to /time_entries to create new entries with a JSON body containing project_id, task_id, hours, spent_date, and optional notes. Use PATCH to /time_entries/{id} to update existing entries. Use DELETE to /time_entries/{id} to delete them. All write operations respect the permissions of the personal access token's user account.
How do I get the project IDs and user IDs needed for filtering?
Make GET requests to /projects and /users endpoints respectively. These return all active projects and team members with their Harvest IDs. Build Select components in your Retool app populated from these queries, binding their values to the filter parameters in your time entries query. This eliminates the need for users to know numeric IDs.
Can I access Harvest data for multiple accounts from one Retool Resource?
Each Harvest Resource in Retool corresponds to one Harvest account (identified by the Account ID header). If you manage multiple Harvest accounts, create separate Retool Resources for each account with their respective tokens and account IDs. You can then build a Retool app that queries multiple resources and combines the data using JavaScript transformers.
Are there rate limits on the Harvest API?
Harvest enforces a rate limit of 100 requests per 15 seconds per user. For normal Retool dashboard usage, this limit is rarely reached. However, if you build Retool Workflows that process large volumes of time entries in a loop, add a delay between iterations to stay within the rate limit. Harvest returns a Retry-After header when the limit is hit.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation