Summary and recommendation
Harvest exposes a REST API at https://api.harvestapp.com/v2 supporting OAuth 2.0 and Personal Access Tokens. All requests require both an Authorization: Bearer {token} header and a Harvest-Account-Id header - omitting the latter returns a 401 even with a valid token.
The API covers full user CRUD, project assignments, and billable rate management, but has no native SCIM 2.0 endpoint and no webhook support for user events. Admin-level credentials are required to create, update, or delete users; non-admin tokens are scoped to GET /v2/users/me only.
API quick reference
| Has user API | Yes |
| Auth method | OAuth 2.0 or Personal Access Token (Bearer token in Authorization header) |
| Base URL | Official docs |
| SCIM available | No |
| SCIM plan required | Not available on any plan |
Authentication
Auth method: OAuth 2.0 or Personal Access Token (Bearer token in Authorization header)
Setup steps
- Navigate to https://id.getharvest.com/developers to create an OAuth2 application or generate a Personal Access Token (PAT).
- For OAuth 2.0: register your app, obtain client_id and client_secret, redirect users to https://id.getharvest.com/oauth2/authorize, exchange the authorization code for an access token at https://id.getharvest.com/api/v2/oauth2/token.
- For PAT: copy the token from the developer portal; no OAuth flow required.
- Include the token in all requests as: Authorization: Bearer {token}
- Include the required Harvest-Account-Id header: Harvest-Account-Id: {account_id}
- Set Content-Type: application/json for POST/PATCH requests.
Required scopes
| Scope | Description | Required for |
|---|---|---|
| all:read | Read access to all resources in the account | Listing and retrieving users |
| all:edit | Read and write access to all resources in the account | Creating, updating, and deleting users |
User object / data model
| Field | Type | Description | On create | On update | Notes |
|---|---|---|---|---|---|
| id | integer | Unique user ID | auto-assigned | immutable | Used in URL path for user-specific operations |
| first_name | string | User's first name | required | optional | |
| last_name | string | User's last name | required | optional | |
| string | User's email address; used for login | required | optional | Must be unique within the account | |
| telephone | string | User's telephone number | optional | optional | |
| timezone | string | User's timezone (e.g., 'Eastern Time (US & Canada)') | optional | optional | Defaults to account timezone if not set |
| has_access_to_all_future_projects | boolean | Whether user is auto-added to new projects | optional | optional | |
| is_contractor | boolean | Whether user is a contractor | optional | optional | |
| is_admin | boolean | Whether user has admin privileges | optional | optional | Admins can manage account settings and all users |
| is_project_manager | boolean | Whether user can manage projects | optional | optional | |
| can_see_rates | boolean | Whether user can view billing rates | optional | optional | |
| can_create_projects | boolean | Whether user can create projects | optional | optional | |
| can_create_invoices | boolean | Whether user can create invoices | optional | optional | |
| is_active | boolean | Whether user account is active | optional | optional | Set to false to deactivate without deleting |
| weekly_capacity | integer | User's weekly capacity in seconds | optional | optional | Used for capacity planning |
| default_hourly_rate | decimal | User's default billing rate | optional | optional | |
| cost_rate | decimal | User's internal cost rate | optional | optional | |
| roles | array[string] | List of role names assigned to the user | optional | optional | |
| avatar_url | string | URL of user's avatar image | auto-assigned | immutable | Read-only |
| created_at | datetime | ISO 8601 timestamp of user creation | auto-assigned | immutable | Read-only |
Core endpoints
List all users
- Method: GET
- URL:
https://api.harvestapp.com/v2/users - Watch out for: Returns both active and inactive users by default; use ?is_active=true to filter active only.
Request example
GET /v2/users?is_active=true&page=1
Authorization: Bearer {token}
Harvest-Account-Id: {account_id}
Response example
{
"users": [{"id":1234,"first_name":"Jane","last_name":"Doe","email":"jane@example.com","is_active":true}],
"per_page": 100,
"total_pages": 1,
"total_entries": 1,
"page": 1
}
Retrieve a user
- Method: GET
- URL:
https://api.harvestapp.com/v2/users/{userId}
Request example
GET /v2/users/1234
Authorization: Bearer {token}
Harvest-Account-Id: {account_id}
Response example
{
"id": 1234,
"first_name": "Jane",
"last_name": "Doe",
"email": "jane@example.com",
"is_admin": false,
"is_active": true
}
Retrieve the currently authenticated user
- Method: GET
- URL:
https://api.harvestapp.com/v2/users/me - Watch out for: Useful for resolving the authenticated user's ID from a PAT or OAuth token.
Request example
GET /v2/users/me
Authorization: Bearer {token}
Harvest-Account-Id: {account_id}
Response example
{
"id": 1234,
"first_name": "Jane",
"email": "jane@example.com",
"is_admin": true
}
Create a user
- Method: POST
- URL:
https://api.harvestapp.com/v2/users - Watch out for: Creating a user sends an invitation email to the provided address. The user counts against the account's seat limit immediately.
Request example
POST /v2/users
Content-Type: application/json
{"first_name":"John","last_name":"Smith","email":"john@example.com","is_admin":false}
Response example
{
"id": 5678,
"first_name": "John",
"last_name": "Smith",
"email": "john@example.com",
"is_active": true
}
Update a user
- Method: PATCH
- URL:
https://api.harvestapp.com/v2/users/{userId} - Watch out for: Only include fields you want to change; omitted fields retain their current values.
Request example
PATCH /v2/users/5678
Content-Type: application/json
{"is_admin":true,"weekly_capacity":144000}
Response example
{
"id": 5678,
"is_admin": true,
"weekly_capacity": 144000,
"updated_at": "2024-01-15T10:00:00Z"
}
Delete a user
- Method: DELETE
- URL:
https://api.harvestapp.com/v2/users/{userId} - Watch out for: Harvest recommends deactivating users (PATCH is_active=false) rather than deleting to preserve historical time entry data. Deletion is permanent.
Request example
DELETE /v2/users/5678
Authorization: Bearer {token}
Harvest-Account-Id: {account_id}
Response example
HTTP 200 OK
{}
List billable rates for a user
- Method: GET
- URL:
https://api.harvestapp.com/v2/users/{userId}/billable_rates - Watch out for: Requires admin or project manager access; returns rate history with effective date ranges.
Request example
GET /v2/users/1234/billable_rates
Authorization: Bearer {token}
Harvest-Account-Id: {account_id}
Response example
{
"billable_rates": [{"id":1,"amount":150.0,"start_date":"2024-01-01","end_date":null}]
}
List project assignments for a user
- Method: GET
- URL:
https://api.harvestapp.com/v2/users/{userId}/project_assignments - Watch out for: Returns only active project assignments by default; use ?is_active=false to include inactive ones.
Request example
GET /v2/users/1234/project_assignments
Authorization: Bearer {token}
Harvest-Account-Id: {account_id}
Response example
{
"project_assignments": [{"id":99,"project":{"id":10,"name":"Website Redesign"},"is_active":true}]
}
Rate limits, pagination, and events
- Rate limits: Harvest enforces a rate limit of 100 requests per 15 seconds per access token.
- Rate-limit headers: Yes
- Retry-After header: Yes
- Rate-limit notes: When the limit is exceeded, the API returns HTTP 429. The Retry-After header indicates when requests can resume. Headers X-RateLimit-Limit and X-RateLimit-Remaining are included in responses.
- Pagination method: offset
- Default page size: 100
- Max page size: 100
- Pagination pointer: page
| Plan | Limit | Concurrent |
|---|---|---|
| All plans | 100 requests per 15 seconds | 0 |
- Webhooks available: No
- Webhook notes: Harvest does not offer native webhooks for user management events. The API must be polled to detect user changes.
- Alternative event strategy: Poll GET /v2/users with updated_since query parameter to detect changes since a given timestamp.
SCIM API status
- SCIM available: No
- SCIM version: Not documented
- Plan required: Not available on any plan
- Endpoint: Not documented
Limitations:
- Harvest has no native SCIM 2.0 endpoint.
- User provisioning via SCIM is only available through third-party IdP connectors (e.g., OneLogin provisioning connector).
- SAML SSO is available on the Premium plan but does not include SCIM provisioning.
- Deprovisioning must be handled via the REST API (PATCH is_active=false or DELETE).
Common scenarios
Three core automation scenarios are well-supported by the API. First, provisioning: POST /v2/users with first_name, last_name, email, is_admin, and weekly_capacity (expressed in seconds - 40 hrs = 144000) creates the user and immediately triggers an invitation email; there is no silent provisioning path.
Second, deprovisioning: PATCH /v2/users/{userId} with {"is_active": false} deactivates the account and preserves all historical time entries; DELETE /v2/users/{userId} is permanent and irreversible, so deactivation is strongly preferred. Third, roster sync: GET /v2/users?
is_active=true with page-based pagination (max 100 records per page) returns the active user list; use ? updated_since={ISO8601_timestamp} on subsequent syncs to fetch only changed records, and add a 60-second buffer to account for clock skew.
Feeding this sync output into an identity graph allows downstream systems to maintain a consistent, current view of Harvest's active user state without full re-fetches on every cycle.
Provision a new employee
- POST /v2/users with first_name, last_name, email, is_admin, weekly_capacity, and default_hourly_rate.
- Capture the returned user id.
- POST /v2/projects/{projectId}/user_assignments with the new user_id to assign them to relevant projects.
- Optionally POST /v2/users/{userId}/billable_rates to set a project-specific billing rate.
Watch out for: Step 1 triggers an invitation email immediately. Ensure the email address is correct before calling the endpoint.
Deactivate a departing employee
- GET /v2/users?is_active=true to find the user's id by email.
- PATCH /v2/users/{userId} with {"is_active": false} to deactivate the account.
- Verify the user no longer appears in active user lists by calling GET /v2/users?is_active=true.
Watch out for: Deactivation preserves all historical time entries. Deletion (DELETE /v2/users/{userId}) is irreversible and removes the user's data associations.
Sync active user roster to an external directory
- GET /v2/users?is_active=true&page=1 and iterate through all pages using the next_page field.
- Store each user's id, email, first_name, last_name, is_admin, and roles.
- On subsequent syncs, use GET /v2/users?updated_since={last_sync_timestamp} to fetch only changed records.
- Compare results against the external directory and apply PATCH or deactivation as needed.
Watch out for: The updated_since parameter uses ISO 8601 format. Clock skew between systems can cause missed updates; add a 60-second buffer to the timestamp.
Why building this yourself is a trap
The absence of native SCIM and webhooks creates two structural gaps worth flagging explicitly. Without SCIM, any IdP-driven provisioning flow must be custom-built against the REST API or routed through a third-party connector such as OneLogin; neither path is zero-maintenance.
Without webhooks, detecting user state changes requires polling - GET /v2/users?updated_since= is the only supported mechanism, and missed updates are possible if the polling interval or timestamp buffer is misconfigured.
The rate limit of 100 requests per 15 seconds per access token is generous for most use cases, but high-frequency sync jobs should implement backoff on HTTP 429 responses and respect the Retry-After header. OAuth 2.0 access tokens expire and require refresh token handling; PATs do not expire but carry broader scope risk if leaked.
No public documentation confirms the OAuth token expiry duration - verify against the Harvest developer portal before implementing.
Automate Harvest workflows without one-off scripts
Stitchflow builds and maintains end-to-end IT automation across your SaaS stack, including apps without APIs. Built for exactly how your company works, with human approvals where they matter.