Summary and recommendation
Bullhorn's REST API authenticates via OAuth 2.0 but adds a second exchange step: the access token must be traded for a session-scoped BhRestToken via GET /rest-services/login, and that token expires after a configurable idle timeout (default ~10 minutes).
The base restUrl is dynamic and returned at login time - it must never be hardcoded.
All user data is modeled under the CorporateUser entity;
key fields for identity graph construction include id, username, email, enabled, userType (to-one relation), department (to-one relation), externalID, dateLastModified, and isDeleted.
Integrating Bullhorn into an identity graph alongside other workforce systems requires resolving UserType and Department relation ids before writes, and treating externalID as the stable cross-system join key.
API quick reference
| Has user API | Yes |
| Auth method | OAuth 2.0 with session token (BhRestToken). OAuth 2.0 issues an access token; a separate /login call exchanges it for a BhRestToken used on all subsequent requests. |
| Base URL | Official docs |
| SCIM available | Yes |
| SCIM plan required | Corporate/Enterprise |
Authentication
Auth method: OAuth 2.0 with session token (BhRestToken). OAuth 2.0 issues an access token; a separate /login call exchanges it for a BhRestToken used on all subsequent requests.
Setup steps
- Register your application with Bullhorn to obtain a client_id and client_secret via the Bullhorn developer portal.
- Direct the user to the Bullhorn authorization endpoint: https://auth.bullhornstaffing.com/oauth/authorize?client_id=...&response_type=code&redirect_uri=...
- Exchange the returned authorization code for an access_token via POST to https://auth.bullhornstaffing.com/oauth/token.
- Call GET https://rest.bullhornstaffing.com/rest-services/login?version=2.0&access_token={access_token} to obtain a BhRestToken and the corp-specific REST URL.
- Include BhRestToken as a query parameter (BhRestToken={token}) or header on all subsequent API calls to the returned restUrl.
Required scopes
| Scope | Description | Required for |
|---|---|---|
| api | General REST API access scope required for all API operations. | All REST API calls including user management |
User object / data model
| Field | Type | Description | On create | On update | Notes |
|---|---|---|---|---|---|
| id | Integer | Unique identifier for the CorporateUser. | auto-assigned | read-only | System-generated; cannot be set. |
| username | String | Login username for the user. | required | optional | Must be unique within the corporation. |
| firstName | String | User's first name. | required | optional | |
| lastName | String | User's last name. | required | optional | |
| String | Primary email address. | required | optional | ||
| enabled | Boolean | Whether the user account is active. | optional | optional | Set to false to deactivate without deleting. |
| userType | To-one (UserType) | Role/type assigned to the user. | required | optional | References a UserType entity by id. |
| department | To-one (Department) | Department the user belongs to. | optional | optional | |
| mobile | String | Mobile phone number. | optional | optional | |
| phone | String | Office phone number. | optional | optional | |
| timeZoneOffsetEST | Integer | User's timezone offset from EST. | optional | optional | |
| dateAdded | Timestamp | Date the user record was created. | auto-assigned | read-only | |
| dateLastModified | Timestamp | Date the user record was last modified. | auto-assigned | auto-assigned | |
| isDeleted | Boolean | Soft-delete flag. | auto-assigned (false) | optional | Bullhorn uses soft deletes; records are not physically removed. |
| externalID | String | External system identifier for the user. | optional | optional | Useful for mapping to external IdP or HR system IDs. |
| loginRestrictions | Object | IP or time-based login restriction settings. | optional | optional | |
| occupation | String | Job title or occupation. | optional | optional | |
| ssoEnabled | Boolean | Whether SSO is enabled for this user. | optional | optional | Requires SSO configuration at the corp level. |
Core endpoints
Search/List Users
- Method: GET
- URL:
{restUrl}/search/CorporateUser?query=isDeleted:0&fields=id,username,firstName,lastName,email,enabled&count=20&start=0&BhRestToken={token} - Watch out for: The search endpoint uses Lucene query syntax. Always include isDeleted:0 to exclude soft-deleted users.
Request example
GET /rest-services/{corpToken}/search/CorporateUser
?query=isDeleted:0
&fields=id,username,firstName,lastName,email,enabled
&count=20&start=0
&BhRestToken={token}
Response example
{
"total": 45,
"start": 0,
"count": 20,
"data": [
{"id":101,"username":"jdoe","firstName":"Jane","lastName":"Doe","email":"jdoe@corp.com","enabled":true}
]
}
Get Single User
- Method: GET
- URL:
{restUrl}/entity/CorporateUser/{id}?fields=id,username,firstName,lastName,email,enabled,userType,department&BhRestToken={token} - Watch out for: You must explicitly specify the fields parameter; omitting it returns only the id field by default.
Request example
GET /rest-services/{corpToken}/entity/CorporateUser/101
?fields=id,username,firstName,lastName,email,enabled,userType
&BhRestToken={token}
Response example
{
"data": {
"id": 101,
"username": "jdoe",
"firstName": "Jane",
"lastName": "Doe",
"email": "jdoe@corp.com",
"enabled": true,
"userType": {"id": 5}
}
}
Create User
- Method: PUT
- URL:
{restUrl}/entity/CorporateUser?BhRestToken={token} - Watch out for: Bullhorn uses PUT (not POST) for entity creation. The response returns the new entity id in changedEntityId.
Request example
PUT /rest-services/{corpToken}/entity/CorporateUser
?BhRestToken={token}
Body: {
"username": "newuser",
"firstName": "New",
"lastName": "User",
"email": "newuser@corp.com",
"userType": {"id": 5}
}
Response example
{
"changedEntityType": "CorporateUser",
"changedEntityId": 202,
"changeType": "INSERT"
}
Update User
- Method: POST
- URL:
{restUrl}/entity/CorporateUser/{id}?BhRestToken={token} - Watch out for: Bullhorn uses POST (not PATCH) for updates to existing entities. Only include fields you want to change.
Request example
POST /rest-services/{corpToken}/entity/CorporateUser/202
?BhRestToken={token}
Body: {
"email": "updated@corp.com",
"enabled": true
}
Response example
{
"changedEntityType": "CorporateUser",
"changedEntityId": 202,
"changeType": "UPDATE"
}
Deactivate User (Soft Delete)
- Method: POST
- URL:
{restUrl}/entity/CorporateUser/{id}?BhRestToken={token} - Watch out for: Bullhorn does not support hard deletion of CorporateUser records via the REST API. Set enabled:false or isDeleted:true to deactivate.
Request example
POST /rest-services/{corpToken}/entity/CorporateUser/202
?BhRestToken={token}
Body: {
"enabled": false
}
Response example
{
"changedEntityType": "CorporateUser",
"changedEntityId": 202,
"changeType": "UPDATE"
}
Query Users (alternative list)
- Method: GET
- URL:
{restUrl}/query/CorporateUser?where=isDeleted=0&fields=id,username,firstName,lastName,email&count=20&start=0&BhRestToken={token} - Watch out for: The /query endpoint uses SQL-like WHERE syntax, while /search uses Lucene. /search is generally preferred for performance.
Request example
GET /rest-services/{corpToken}/query/CorporateUser
?where=isDeleted=0
&fields=id,username,firstName,lastName,email
&count=20&start=0
&BhRestToken={token}
Response example
{
"start": 0,
"count": 20,
"data": [
{"id":101,"username":"jdoe","firstName":"Jane","lastName":"Doe"}
]
}
Get Current User (Session Info)
- Method: GET
- URL:
{restUrl}/settings/userId,corporationId?BhRestToken={token} - Watch out for: Returns the userId of the authenticated session's user. Use this to resolve the current user's CorporateUser record.
Request example
GET /rest-services/{corpToken}/settings/userId,corporationId
?BhRestToken={token}
Response example
{
"userId": 101,
"corporationId": 9999
}
List User Types
- Method: GET
- URL:
{restUrl}/query/UserType?where=isDeleted=0&fields=id,name&count=100&BhRestToken={token} - Watch out for: UserType ids are required when creating or updating a CorporateUser's role. Retrieve them before provisioning.
Request example
GET /rest-services/{corpToken}/query/UserType
?where=isDeleted=0
&fields=id,name
&count=100
&BhRestToken={token}
Response example
{
"start": 0,
"count": 5,
"data": [
{"id":1,"name":"Administrator"},
{"id":5,"name":"Recruiter"}
]
}
Rate limits, pagination, and events
Rate limits: Bullhorn does not publicly document specific rate limit thresholds in its official REST API docs. Rate limiting is enforced at the platform level and varies by contract/plan.
Rate-limit headers: No
Retry-After header: No
Rate-limit notes: No publicly documented rate limit values, headers, or Retry-After behavior found in official docs. Contact Bullhorn support for contract-specific limits.
Pagination method: offset
Default page size: 20
Max page size: 500
Pagination pointer: start / count
Webhooks available: Yes
Webhook notes: Bullhorn supports event-based subscriptions via its Event Subscription API (also called the Events API). Consumers poll a subscription endpoint to retrieve queued entity change events rather than receiving push callbacks.
Alternative event strategy: Because the Events API is poll-based rather than push-based, consumers must periodically GET /event/subscription/{subscriptionId} to retrieve new events. True outbound HTTP webhooks are not documented in the official REST API docs.
Webhook events: INSERTED (entity created), UPDATED (entity updated), DELETED (entity soft-deleted)
SCIM API status
SCIM available: Yes
SCIM version: 2.0
Plan required: Corporate/Enterprise
Endpoint: Not documented
Supported operations: GET /Users, GET /Users/{id}, POST /Users, PUT /Users/{id}, PATCH /Users/{id}, DELETE /Users/{id}
Limitations:
- SCIM 2.0 support is gated to Corporate/Enterprise plan tiers; not available on lower-tier plans.
- Specific SCIM base URL is tenant/environment-specific and not publicly documented; must be obtained from Bullhorn support or account team.
- No publicly documented list of supported SCIM schema extensions or Group provisioning details in official docs.
- IdP connector configuration (e.g., Okta, Entra) details are not publicly documented by Bullhorn.
Common scenarios
Three automation scenarios cover the primary provisioning lifecycle.
To provision a new recruiter: authenticate, retrieve available UserType ids via GET /query/UserType, then issue a PUT to /entity/CorporateUser
note that Bullhorn inverts standard REST verb semantics, using PUT for creation and POST for updates.
To deactivate a departed employee: locate the user via /search/CorporateUser using Lucene syntax, then POST {"enabled": false} to the entity endpoint;
hard deletion is not supported and deactivated records persist unless filtered with isDeleted:0.
To sync user changes into a downstream identity graph or HR system: create an event subscription scoped to CorporateUser INSERTED/UPDATED/DELETED events, then poll GET /event/subscription/{subscriptionId} on a schedule
the Events API is poll-based, not push-based, and events may be lost if the subscription lapses beyond the retention window.
Provision a new recruiter user
- Authenticate via OAuth 2.0 to obtain an access_token.
- Call GET /rest-services/login?access_token={token} to obtain BhRestToken and restUrl.
- Call GET {restUrl}/query/UserType?where=isDeleted=0&fields=id,name to retrieve available UserType ids.
- Call PUT {restUrl}/entity/CorporateUser with JSON body containing username, firstName, lastName, email, and userType:{id}.
- Capture the changedEntityId from the response as the new user's id.
- Optionally call POST {restUrl}/entity/CorporateUser/{id} to set additional fields such as department or externalID.
Watch out for: Use PUT for creation (not POST). Missing the userType field will cause a validation error.
Deactivate a departed employee
- Authenticate and obtain BhRestToken.
- Call GET {restUrl}/search/CorporateUser?query=username:{username}&fields=id,enabled to locate the user's id.
- Call POST {restUrl}/entity/CorporateUser/{id} with body {"enabled": false} to deactivate the account.
- Optionally set isDeleted:true if the record should be excluded from standard queries.
Watch out for: Hard deletion is not supported. Deactivated users remain in the system and will appear in queries unless isDeleted:0 filter is applied.
Sync user changes via event polling
- Create an event subscription via PUT {restUrl}/event/subscription/{subscriptionId}?type=entity&names=CorporateUser&eventTypes=UPDATED,INSERTED,DELETED.
- Periodically poll GET {restUrl}/event/subscription/{subscriptionId} to retrieve queued events.
- For each event, extract the entityId and call GET {restUrl}/entity/CorporateUser/{entityId}?fields=... to fetch the current state.
- Process changes (e.g., sync to downstream IdP or HR system) and acknowledge by advancing the subscription.
Watch out for: The Events API is poll-based, not push-based. Events may be lost if the subscription is not polled within the retention window. Confirm retention period with Bullhorn support.
Why building this yourself is a trap
Several API behaviors diverge from common REST conventions and will cause silent failures if not handled explicitly. The fields parameter is mandatory on all entity and search endpoints; omitting it returns only the id field with no error.
The /search endpoint uses Lucene syntax while /query uses SQL-like WHERE clauses - they are not interchangeable and mixing them produces unexpected results. Rate limit thresholds are not publicly documented and vary by contract; no Retry-After headers are emitted, so retry logic must be implemented conservatively without platform guidance.
SCIM 2.0 is available on Corporate/Enterprise tiers but the endpoint URL is not discoverable - it must be provisioned by Bullhorn support, making it unsuitable for self-service IdP connector setup without account team involvement.
Automate Bullhorn 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.