Summary and recommendation
The Greenhouse Harvest API exposes user management under `https://harvest.greenhouse.io/v1/users` using HTTP Basic Auth - the API key is Base64-encoded as the username with an empty password. All write operations (POST, PATCH) require an `On-Behalf-Of: <greenhouse_user_id>` header referencing an active Site Admin; omitting it returns a 403.
There is no official SDK; all integrations are raw HTTP. API keys are displayed only once at creation and must be stored immediately.
Rate limits are enforced at 50 requests per 10 seconds per key. Exceeding the limit returns HTTP 429 with `X-RateLimit-Limit` and `X-RateLimit-Remaining` headers; no `Retry-After` header is provided, so callers must implement their own backoff.
Pagination uses `page` + `per_page` (max 500); Link headers are absent - check whether the returned array length equals `per_page` to determine if additional pages exist.
API quick reference
| Has user API | Yes |
| Auth method | HTTP Basic Auth (API key as username, empty password); key is Base64-encoded in Authorization header |
| Base URL | Official docs |
| SCIM available | Yes |
| SCIM plan required | Advanced or Expert (custom/quote-based pricing) |
Authentication
Auth method: HTTP Basic Auth (API key as username, empty password); key is Base64-encoded in Authorization header
Setup steps
- Log in to Greenhouse as a Site Admin.
- Navigate to Configure > Dev Center > API Credential Management.
- Click 'Create New API Key', select 'Harvest' as the API type, and assign permissions.
- Copy the generated API key immediately (shown only once).
- Encode the key as Base64 (key + ':') and pass as the Authorization header: 'Authorization: Basic
'.
Required scopes
| Scope | Description | Required for |
|---|---|---|
| Users – View | Read access to user records (GET /users, GET /users/{id}) | Listing and retrieving users |
| Users – Edit | Write access to update user records (PATCH /users/{id}) | Updating user attributes such as name or employee ID |
| Users – Disable | Permission to disable a user account | Deactivating users via PATCH /users/{id} |
User object / data model
| Field | Type | Description | On create | On update | Notes |
|---|---|---|---|---|---|
| id | integer | Unique Greenhouse user ID | system-assigned | immutable | Used as path parameter in user-specific endpoints |
| name | string | Full name of the user | required | optional | |
| first_name | string | User's first name | required | optional | |
| last_name | string | User's last name | required | optional | |
| primary_email_address | string | Primary email address used for login | required | optional | Must be unique across the organization |
| emails | array | All email addresses associated with the user | optional | optional | |
| employee_id | string | External employee identifier | optional | optional | Useful for HRIS correlation |
| updated_at | datetime (ISO 8601) | Timestamp of last update | system-assigned | system-assigned | Useful for delta sync filtering via updated_after query param |
| created_at | datetime (ISO 8601) | Timestamp of user creation | system-assigned | immutable | |
| disabled | boolean | Whether the user account is disabled | defaults to false | optional | Set to true to deactivate; cannot delete users via API |
| site_admin | boolean | Whether the user has Site Admin privileges | optional | optional | Requires elevated API key permissions to modify |
| linked_candidate_ids | array of integers | Candidate records linked to this user | system-assigned | read-only | |
| offices | array of objects | Offices the user is associated with | optional | optional | |
| departments | array of objects | Departments the user is associated with | optional | optional | |
| timezone | string | User's timezone (IANA format) | optional | optional |
Core endpoints
List Users
- Method: GET
- URL:
https://harvest.greenhouse.io/v1/users - Watch out for: Pagination is required for large orgs; max per_page is 500. Use updated_after for incremental sync.
Request example
GET /v1/users?per_page=100&page=1&updated_after=2024-01-01T00:00:00Z
Authorization: Basic <base64_api_key>
Response example
[
{
"id": 112233,
"name": "Jane Doe",
"primary_email_address": "jane@example.com",
"disabled": false,
"site_admin": false
}
]
Get Single User
- Method: GET
- URL:
https://harvest.greenhouse.io/v1/users/{id} - Watch out for: Can also look up by email using GET /v1/users?email=jane@example.com
Request example
GET /v1/users/112233
Authorization: Basic <base64_api_key>
Response example
{
"id": 112233,
"name": "Jane Doe",
"primary_email_address": "jane@example.com",
"employee_id": "EMP-001",
"disabled": false
}
Get User by Email
- Method: GET
- URL:
https://harvest.greenhouse.io/v1/users?email={email} - Watch out for: Returns an array; check for empty array if user not found.
Request example
GET /v1/users?email=jane%40example.com
Authorization: Basic <base64_api_key>
Response example
[
{
"id": 112233,
"name": "Jane Doe",
"primary_email_address": "jane@example.com"
}
]
Update User
- Method: PATCH
- URL:
https://harvest.greenhouse.io/v1/users/{id} - Watch out for: Only fields included in the body are updated. Cannot change primary_email_address via this endpoint.
Request example
PATCH /v1/users/112233
Content-Type: application/json
{"first_name":"Jane","last_name":"Smith","employee_id":"EMP-002"}
Response example
{
"id": 112233,
"name": "Jane Smith",
"employee_id": "EMP-002",
"disabled": false
}
Disable User
- Method: PATCH
- URL:
https://harvest.greenhouse.io/v1/users/{id}/disable - Watch out for: There is no DELETE endpoint for users; disabling is the only deactivation method via the Harvest API.
Request example
PATCH /v1/users/112233/disable
Authorization: Basic <base64_api_key>
Response example
{
"id": 112233,
"name": "Jane Smith",
"disabled": true
}
Enable User
- Method: PATCH
- URL:
https://harvest.greenhouse.io/v1/users/{id}/enable - Watch out for: Re-enabling a user restores their access but does not restore removed job permissions.
Request example
PATCH /v1/users/112233/enable
Authorization: Basic <base64_api_key>
Response example
{
"id": 112233,
"name": "Jane Smith",
"disabled": false
}
List User Permissions (Job)
- Method: GET
- URL:
https://harvest.greenhouse.io/v1/users/{id}/permissions/jobs - Watch out for: Job-level permissions are separate from site-level roles; both must be managed independently.
Request example
GET /v1/users/112233/permissions/jobs
Authorization: Basic <base64_api_key>
Response example
[
{
"id": 9988,
"job_id": 5544,
"user_role_id": 2
}
]
Add User (via Job Board / Invite)
- Method: POST
- URL:
https://harvest.greenhouse.io/v1/users - Watch out for: The On-Behalf-Of header (On-Behalf-Of:
) is required to attribute the action to a specific admin user.
Request example
POST /v1/users
Content-Type: application/json
{"first_name":"John","last_name":"Doe","email":"john@example.com","send_email_invite":true}
Response example
{
"id": 223344,
"name": "John Doe",
"primary_email_address": "john@example.com",
"disabled": false
}
Rate limits, pagination, and events
- Rate limits: Greenhouse enforces rate limits on the Harvest API. The documented limit is 50 requests per 10 seconds per API key.
- Rate-limit headers: Yes
- Retry-After header: No
- Rate-limit notes: When the limit is exceeded, the API returns HTTP 429. Response headers include X-RateLimit-Limit and X-RateLimit-Remaining. Retry after a brief backoff; no Retry-After header is documented.
- Pagination method: offset
- Default page size: 100
- Max page size: 500
- Pagination pointer: page / per_page
| Plan | Limit | Concurrent |
|---|---|---|
| All plans (Harvest API) | 50 requests per 10 seconds | 0 |
- Webhooks available: Yes
- Webhook notes: Greenhouse supports webhooks for key recruiting events. User-specific webhook events are limited; most webhooks cover candidate and job lifecycle events.
- Alternative event strategy: For user change detection, poll GET /v1/users with the updated_after query parameter for incremental sync.
- Webhook events: candidate_hired, candidate_stage_change, job_post_updated, offer_created, offer_updated, application_submitted, prospect_created
SCIM API status
SCIM available: Yes
SCIM version: 2.0
Plan required: Advanced or Expert (custom/quote-based pricing)
Endpoint: https://harvest.greenhouse.io/scim/v2
Supported operations: Create user (POST /Users), Read user (GET /Users/{id}), List users (GET /Users), Update user (PUT /Users/{id}), Deactivate user (PATCH /Users/{id} with active=false)
Limitations:
- Group provisioning is not supported; only user provisioning.
- SSO must be configured before SCIM can be enabled.
- Azure Entra ID sync interval is approximately 40 minutes.
- SCIM does not provision job-level permissions; those must be managed manually or via Harvest API.
- Supported IdPs: Okta, Azure Entra ID, Google Workspace, OneLogin.
- Deprovisioning sets the user to disabled; it does not delete the user record.
Common scenarios
Three integration patterns cover the primary use cases against the Harvest API, each with a distinct identity graph consideration.
Onboarding with job permission assignment: POST /v1/users creates the account (include send_email_invite=true and the On-Behalf-Of header). Capture the returned id, then POST /v1/users/{id}/permissions/jobs with the target job_id and user_role_id. SCIM alone cannot complete this flow - job-level permissions sit outside the SCIM schema and must be set via Harvest API.
Incremental sync to an external identity graph: GET /v1/users?updated_after=<ISO8601_UTC>&per_page=500 drives delta pulls. Use primary_email_address or employee_id as the correlation key when upserting into a downstream identity graph. Subtract a 5-minute buffer from the last sync timestamp to guard against clock skew causing missed updates.
SCIM offboarding (Okta): Unassigning the user in Okta triggers a SCIM PATCH /Users/{id} with active=false, setting disabled=true in Greenhouse. Confirm via GET /v1/users/{id}. Note that group membership changes in Okta do not propagate - Greenhouse SCIM does not support group provisioning.
Onboard a new employee and assign job permissions
- POST /v1/users with first_name, last_name, email, and send_email_invite=true (include On-Behalf-Of header).
- Capture the returned user id.
- POST /v1/users/{id}/permissions/jobs with the target job_id and user_role_id to grant job-level access.
Watch out for: The On-Behalf-Of header must reference an active Site Admin user ID, or the request returns 403.
Incremental sync of updated users to an external system
- Store the timestamp of the last successful sync.
- GET /v1/users?updated_after=
&per_page=500&page=1. - Iterate pages until the returned array has fewer items than per_page.
- Upsert each user record into the external system using primary_email_address or employee_id as the correlation key.
- Update the stored timestamp to the current time.
Watch out for: updated_after uses ISO 8601 UTC; ensure the timestamp is URL-encoded. Clock skew can cause missed updates - subtract a small buffer (e.g., 5 minutes) from the last sync time.
Offboard a departing employee via SCIM (Okta)
- In Okta, unassign the user from the Greenhouse SCIM application or deactivate the Okta user.
- Okta sends a SCIM PATCH /Users/{id} with active=false to Greenhouse.
- Greenhouse sets the user's disabled flag to true, revoking login access.
- Verify via GET /v1/users/{id} that disabled=true.
Watch out for: SCIM deprovisioning only disables the user; job-level permissions and historical records are retained. Group membership changes in Okta do not propagate to Greenhouse (groups not supported).
Why building this yourself is a trap
Several API behaviors are non-obvious and will cause silent failures if not handled explicitly.
- No delete endpoint exists.
PATCH /v1/users/{id}/disableis the only deactivation path; disabled users retain all historical data and can be re-enabled, restoring access but not removed job permissions. primary_email_addressis immutable via API. ThePATCH /users/{id}endpoint cannot change the primary email; that requires a manual admin action in the UI, breaking any automated identity graph update that relies on email as a mutable key.- SCIM scope is user-only. Group provisioning is not supported. Role and job-level permission assignment falls entirely outside the SCIM 2.0 implementation at
https://harvest.greenhouse.io/scim/v2, requiring a parallel Harvest API call for every provisioned user. - SCIM requires Advanced or Expert tier plus a fully configured SSO. Attempting to enable SCIM before SSO is active will fail without a clear error surfaced to the end user.
Automate Greenhouse 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.