Stitchflow
BambooHR logo

BambooHR User Management API Guide

API workflow

How to automate user lifecycle operations through APIs with caveats that matter in production.

UpdatedFeb 27, 2026

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 APIYes
Auth methodHTTP Basic Auth (API key as username, any string as password)
Base URLOfficial docs
SCIM availableNo
SCIM plan requiredCustom

Authentication

Auth method: HTTP Basic Auth (API key as username, any string as password)

Setup steps

  1. Log in to BambooHR as an admin.
  2. Navigate to your account icon (top right) > API Keys.
  3. Click 'Add New Key', give it a name, and copy the generated API key.
  4. Use the API key as the username in HTTP Basic Auth. The password field can be any non-empty string (e.g., 'x').
  5. 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

  1. POST to /employees with firstName, lastName, hireDate, workEmail, and any other required fields.
  2. Parse the Location header from the 201 response to get the new employee ID.
  3. POST to /employees/{id}/tables/employmentStatus to set initial employment status (e.g., Full-Time).
  4. 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

  1. Store the timestamp of your last successful sync.
  2. GET /employees/changed?since={lastSyncTimestamp}&type=updated to retrieve changed employee IDs.
  3. For each changed employee ID, GET /employees/{id}?fields={fieldList} to fetch updated data.
  4. Repeat with type=inserted for new employees and type=deleted for removed employees.
  5. 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

  1. POST to /employees/{id}/tables/employmentStatus with body: {"date": "YYYY-MM-DD", "employmentStatus": "Terminated"}.
  2. Optionally POST to /employees/{id} to set terminationDate field.
  3. 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.

Every app coverage, including apps without APIs
60+ app integrations plus browser automation for apps without APIs
IT graph reconciliation across apps and your IdP
Less than a week to launch, maintained as APIs and admin consoles change
SOC 2 Type II. ~2 hours of your team's time

UpdatedFeb 27, 2026

* Details sourced from official product documentation and admin references.

Keep exploring

Related apps

Abnormal Security logo

Abnormal Security

API Only
AutomationAPI only
Last updatedMar 2026

Abnormal Security is an enterprise email security platform focused on detecting and investigating threats such as phishing, account takeover (ATO), and vendor email compromise. It does not support SCIM provisioning, which means every app in your stack

ActiveCampaign logo

ActiveCampaign

API Only
AutomationAPI only
Last updatedFeb 2026

ActiveCampaign uses a group-based permission model: every user belongs to exactly one group, and all feature-area access (Contacts, Campaigns, Automations, Deals, Reports, Templates) is configured at the group level, not per individual. The default Adm

ADP logo

ADP

API Only
AutomationAPI only
Last updatedFeb 2026

ADP Workforce Now is a mid-market to enterprise HCM platform that serves as the HR source of record for employee data — payroll, benefits, time, and talent. User access is governed by a hybrid permission model: predefined security roles (Security Maste