Summary and recommendation
BambooHR's REST API uses HTTP Basic Auth with an API key as the username and any non-empty string as the password - there is no OAuth 2.0 support.
The API key inherits the BambooHR permissions of the user account that generated it; use a dedicated service account with scoped permissions rather than an admin's personal key. The base URL is `https://api.bamboohr.com/api/gateway.php/{companyDomain}/v1`, where `companyDomain` is your subdomain only (e.g., `acme` for `acme.bamboohr.com`).
Rate limiting is approximately 100 requests per minute per API key; BambooHR does not publish exact limits or rate-limit headers, so implement exponential backoff on 429 responses.
For teams that need pre-built, maintained connectivity across identity and IT systems, Stitchflow's MCP server with ~100 deep IT/identity integrations covers BambooHR alongside the rest of your stack without requiring custom API maintenance.
API quick reference
| Has user API | Yes |
| Auth method | HTTP Basic Auth (API key as username, any string as password) |
| Base URL | Official docs |
| SCIM available | No |
| SCIM plan required | Custom |
Authentication
Auth method: HTTP Basic Auth (API key as username, any string as password)
Setup steps
- Log in to BambooHR as an admin.
- Navigate to your account icon (top right) > API Keys.
- Click 'Add New Key', give it a name, and copy the generated API key.
- Use the API key as the username in HTTP Basic Auth. The password field can be any non-empty string (e.g., 'x').
- Set the Accept header to 'application/json' to receive JSON responses.
Required scopes
| Scope | Description | Required for |
|---|---|---|
| N/A - permission-based | BambooHR does not use OAuth scopes. Access is governed by the BambooHR user account permissions assigned to the user who generated the API key. | All API operations |
User object / data model
| Field | Type | Description | On create | On update | Notes |
|---|---|---|---|---|---|
| id | integer | BambooHR internal employee ID | auto-assigned | immutable | Used as path parameter in all employee endpoints |
| firstName | string | Employee first name | required | optional | |
| lastName | string | Employee last name | required | optional | |
| workEmail | string | Work email address | optional | optional | Used for login and SSO matching |
| homeEmail | string | Personal email address | optional | optional | |
| jobTitle | string | Employee job title | optional | optional | |
| department | string | Department name | optional | optional | |
| location | string | Work location | optional | optional | |
| division | string | Division name | optional | optional | |
| employmentHistoryStatus | string | Employment status (e.g., Full-Time, Part-Time) | optional | optional | |
| hireDate | date (YYYY-MM-DD) | Employee hire date | optional | optional | |
| terminationDate | date (YYYY-MM-DD) | Date of termination | optional | optional | Set when offboarding |
| supervisor | string | Name of direct supervisor | optional | optional | |
| mobilePhone | string | Mobile phone number | optional | optional | |
| workPhone | string | Work phone number | optional | optional | |
| gender | string | Gender | optional | optional | |
| dateOfBirth | date (YYYY-MM-DD) | Date of birth | optional | optional | Sensitive field; access controlled by permissions |
| country | string | Country of work location | optional | optional | |
| state | string | State/province of work location | optional | optional | |
| status | string | Active or Inactive employee status | auto | read-only via this field | Controlled by employment status history, not directly settable |
Core endpoints
List all employees (directory)
- Method: GET
- URL:
https://api.bamboohr.com/api/gateway.php/{companyDomain}/v1/employees/directory - Watch out for: Returns only fields visible to the API key's user. Does not return all fields by default; use the /employees/{id} endpoint with a fields parameter for full detail.
Request example
GET /api/gateway.php/acme/v1/employees/directory
Authorization: Basic {base64(apiKey:x)}
Accept: application/json
Response example
{
"fields": [{"id":"displayName","type":"text","name":"Display Name"}],
"employees": [
{"id":"123","displayName":"Jane Doe","workEmail":"jane@acme.com"}
]
}
Get employee by ID
- Method: GET
- URL:
https://api.bamboohr.com/api/gateway.php/{companyDomain}/v1/employees/{id} - Watch out for: You must explicitly specify the 'fields' query parameter. Without it, only a minimal default set is returned.
Request example
GET /api/gateway.php/acme/v1/employees/123?fields=firstName,lastName,workEmail,jobTitle
Authorization: Basic {base64(apiKey:x)}
Accept: application/json
Response example
{
"id": "123",
"firstName": "Jane",
"lastName": "Doe",
"workEmail": "jane@acme.com",
"jobTitle": "Engineer"
}
Create employee
- Method: POST
- URL:
https://api.bamboohr.com/api/gateway.php/{companyDomain}/v1/employees - Watch out for: The new employee ID is returned in the Location response header, not the body. Parse the header to get the ID.
Request example
POST /api/gateway.php/acme/v1/employees
Authorization: Basic {base64(apiKey:x)}
Content-Type: application/json
{"firstName":"John","lastName":"Smith","hireDate":"2024-01-15","workEmail":"john@acme.com"}
Response example
HTTP 201 Created
Location: /api/gateway.php/acme/v1/employees/456
(empty body; new employee ID in Location header)
Update employee
- Method: POST
- URL:
https://api.bamboohr.com/api/gateway.php/{companyDomain}/v1/employees/{id} - Watch out for: BambooHR uses POST (not PATCH/PUT) for updates to existing employees. Only include fields you want to change.
Request example
POST /api/gateway.php/acme/v1/employees/123
Authorization: Basic {base64(apiKey:x)}
Content-Type: application/json
{"jobTitle":"Senior Engineer","department":"Engineering"}
Response example
HTTP 200 OK
(empty body on success)
Get employee fields (metadata)
- Method: GET
- URL:
https://api.bamboohr.com/api/gateway.php/{companyDomain}/v1/meta/fields - Watch out for: Use this endpoint to discover all available field aliases and IDs before constructing employee queries.
Request example
GET /api/gateway.php/acme/v1/meta/fields
Authorization: Basic {base64(apiKey:x)}
Accept: application/json
Response example
[
{"id":"1","name":"First Name","type":"text","alias":"firstName"},
{"id":"4","name":"Last Name","type":"text","alias":"lastName"}
]
Get employee table (e.g., employment status history)
- Method: GET
- URL:
https://api.bamboohr.com/api/gateway.php/{companyDomain}/v1/employees/{id}/tables/{tableName} - Watch out for: Termination and status changes are tracked in table rows, not flat fields. To terminate an employee, add a row to the employmentStatus table with status 'Terminated'.
Request example
GET /api/gateway.php/acme/v1/employees/123/tables/employmentStatus
Authorization: Basic {base64(apiKey:x)}
Accept: application/json
Response example
{
"title": "Employment Status",
"fields": [{"id":"date"},{"id":"employmentStatus"}],
"rows": [{"id":"1","date":"2024-01-15","employmentStatus":"Full-Time"}]
}
Add employee table row (e.g., terminate employee)
- Method: POST
- URL:
https://api.bamboohr.com/api/gateway.php/{companyDomain}/v1/employees/{id}/tables/{tableName} - Watch out for: This is the correct way to terminate an employee in BambooHR via API. There is no dedicated 'deactivate' endpoint.
Request example
POST /api/gateway.php/acme/v1/employees/123/tables/employmentStatus
Content-Type: application/json
{"date":"2024-06-01","employmentStatus":"Terminated"}
Response example
HTTP 201 Created
(empty body)
Get changed employees (since timestamp)
- Method: GET
- URL:
https://api.bamboohr.com/api/gateway.php/{companyDomain}/v1/employees/changed - Watch out for: The 'type' parameter accepts 'inserted', 'updated', or 'deleted'. Use this for incremental sync instead of polling the full directory repeatedly.
Request example
GET /api/gateway.php/acme/v1/employees/changed?since=2024-01-01T00:00:00Z&type=inserted
Authorization: Basic {base64(apiKey:x)}
Accept: application/json
Response example
{
"employees": {
"123": {"id":"123","action":"inserted","lastChanged":"2024-01-15T10:00:00+00:00"}
}
}
Rate limits, pagination, and events
- Rate limits: BambooHR enforces rate limiting per API key. Requests exceeding the limit receive a 429 Too Many Requests response. The limit is approximately 100 requests per minute per API key, but BambooHR does not publish an exact number officially.
- Rate-limit headers: No
- Retry-After header: No
- Rate-limit notes: BambooHR does not document specific rate limit headers. On 429, implement exponential backoff. Bulk operations (e.g., fetching all employees at once) are preferred over many individual calls.
- Pagination method: none
- Default page size: 0
- Max page size: 0
- Pagination pointer: Not documented
| Plan | Limit | Concurrent |
|---|---|---|
| All plans | ~100 requests/minute per API key (unofficial; BambooHR advises implementing exponential backoff) | 0 |
- Webhooks available: Yes
- Webhook notes: BambooHR supports webhooks that fire on employee data changes. Webhooks are configured in the BambooHR UI under Settings > Integrations > Webhooks. They send HTTP POST payloads to a configured URL when monitored fields change.
- Alternative event strategy: Use the GET /employees/changed endpoint with a 'since' timestamp for polling-based incremental sync if webhooks are not suitable.
- Webhook events: Employee field change (any monitored field), Employee added, Employee status change
SCIM API status
- SCIM available: No
- SCIM version: Not documented
- Plan required: Custom
- Endpoint: Not documented
Limitations:
- BambooHR does not expose an inbound SCIM endpoint. It acts as an HR source of truth and provisions outbound to other apps via IdP integrations (Okta, Entra ID, OneLogin).
- SCIM provisioning from BambooHR to downstream apps requires a supported IdP integration and is outbound only.
- No inbound SCIM provisioning into BambooHR is supported.
Common scenarios
Three scenarios require careful sequencing.
For onboarding: POST to /employees, then parse the new employee ID from the Location response header (it is not in the body), then POST to /employees/{id}/tables/employmentStatus to set employment status - skipping this step leaves the employee without a status in the directory.
For incremental sync: use GET /employees/changed? since={timestamp}&type=updated|inserted|deleted rather than polling the full directory; note that deleted covers hard-deleted records only - terminated employees remain in BambooHR and must be detected by checking the employmentStatus table.
For termination: POST a row to /employees/{id}/tables/employmentStatus with employmentStatus: Terminated - there is no dedicated deactivation endpoint, and BambooHR does not directly revoke app access.
downstream deprovisioning only occurs if an IdP (Okta, Entra ID, OneLogin) is configured to sync status changes from BambooHR.
Onboard a new employee
- POST to /employees with firstName, lastName, hireDate, workEmail, and any other required fields.
- Parse the Location header from the 201 response to get the new employee ID.
- POST to /employees/{id}/tables/employmentStatus to set initial employment status (e.g., Full-Time).
- Optionally POST to /employees/{id} to set additional fields like jobTitle, department, location.
Watch out for: If you do not set an employmentStatus table row, the employee may appear without a status. The employee will not appear in the directory until their hireDate is reached or status is set.
Sync employee changes incrementally
- Store the timestamp of your last successful sync.
- GET /employees/changed?since={lastSyncTimestamp}&type=updated to retrieve changed employee IDs.
- For each changed employee ID, GET /employees/{id}?fields={fieldList} to fetch updated data.
- Repeat with type=inserted for new employees and type=deleted for removed employees.
- Update your downstream system and store the new sync timestamp.
Watch out for: The 'deleted' type returns employees who were deleted from BambooHR entirely (rare). Terminated employees still exist in BambooHR; detect termination by checking the employmentStatus table.
Terminate (offboard) an employee
- POST to /employees/{id}/tables/employmentStatus with body: {"date": "YYYY-MM-DD", "employmentStatus": "Terminated"}.
- Optionally POST to /employees/{id} to set terminationDate field.
- Downstream deprovisioning (e.g., revoking SSO/app access) must be handled via your IdP (Okta, Entra) which reads BambooHR status changes via its integration.
Watch out for: BambooHR does not directly deprovision app access. Termination in BambooHR triggers downstream deprovisioning only if an IdP integration (Okta, Entra, OneLogin) is configured to sync employee status.
Why building this yourself is a trap
Several API behaviors diverge from REST conventions and will cause silent failures if not handled explicitly. Employee updates use POST to /employees/{id}, not PATCH or PUT; sending unrecognized fields returns a 400 with no partial success.
The /employees/directory endpoint returns a limited default field set - always specify the fields query parameter on /employees/{id} calls, and use /meta/fields first to discover valid field aliases. BambooHR has no inbound SCIM endpoint; it is outbound-only, provisioning to downstream apps via IdP integrations.
Webhooks are available and fire on field changes, employee additions, and status changes, but polling via /employees/changed is the more reliable fallback if webhook delivery cannot be guaranteed.
The employmentHistoryStatus field returned by the Zapier 'Updated Employee' trigger has been reported to return blank data in some configurations - validate this field explicitly in any automated offboarding workflow.
Automate BambooHR 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.