Summary and recommendation
Ashby exposes a REST-style API at https://api.ashbyhq.com using HTTP Basic Auth (API key as username, blank password). All endpoints - including reads - use POST; there are no GET endpoints in the public API.
Key scopes are module-level (e.g., organizationRead, organizationWrite, candidatesWrite) and are assigned per key at creation time in Admin > Integrations > API Keys. No official SDKs are provided, and no public sandbox environment exists - Ashby recommends a test workspace or restricted-key production setup for development.
Stitchflow connects to Ashby through an MCP server with ~100 deep IT/identity integrations, handling auth, pagination, and sync-token lifecycle without custom plumbing.
API quick reference
| Has user API | Yes |
| Auth method | HTTP Basic Auth (API key as username, password blank) |
| Base URL | Official docs |
| SCIM available | Yes |
| SCIM plan required | Legacy Plus, Plus, or Enterprise (not available on Foundations plan) |
Authentication
Auth method: HTTP Basic Auth (API key as username, password blank)
Setup steps
- Log in to Ashby as an Admin and navigate to Admin > Integrations > API Keys.
- Create a new API key and assign module-level read/write permissions (e.g., organizationRead for user endpoints).
- Optionally enable 'Allow access to confidential jobs and projects?' and 'Allow access to non-offer private fields?' flags on the key.
- Send the API key as the HTTP Basic Auth username with every request; leave the password field blank.
- Set Content-Type: application/json on all requests (except multipart endpoints like applicationForm.submit).
Required scopes
| Scope | Description | Required for |
|---|---|---|
| organizationRead | Read access to organization/user data including user.list, user.info, user.search. | Listing, fetching, and searching Ashby users |
| organizationWrite | Write access to organization data including adding members to hiring teams. | hiringTeam.addMember, application.addHiringTeamMember |
| candidatesRead | Read access to candidate and application data. | candidate.list, candidate.info, application.list, application.info |
| candidatesWrite | Write access to candidate and application data. | candidate.create, candidate.update, application.create, application.update |
| apiKeysRead | Read access to API key metadata. | apiKey.info |
| apiKeysWrite | Write access to API key and webhook settings. | webhook.create, webhook.delete |
| hiringProcessMetadataRead | Read access to hiring process metadata including tags, archive reasons, custom fields. | candidateTag.list, archiveReason.list, customField.list |
User object / data model
| Field | Type | Description | On create | On update | Notes |
|---|---|---|---|---|---|
| id | string (UUID) | Unique identifier for the Ashby user. | system-generated | immutable | Used as teamMemberId in hiring team endpoints. |
| firstName | string | User's first name. | required | updatable via UI | |
| lastName | string | User's last name. | required | updatable via UI | |
| string | User's email address; used for login and search. | required | editable in UI | Used as the lookup key in user.search endpoint. | |
| globalRole | string (enum) | User's global access level in Ashby (e.g., limitedAccess, elevatedAccess, organizationAdmin). | defaults to limitedAccess | updatable via UI or SCIM provisioning | SCIM provisioning always sets new users to limitedAccess; role must be changed manually in Ashby after sync. |
| isEnabled | boolean | Whether the user account is active. | set on activation | toggled via deactivate/activate actions | Deactivated users are removed from interviewer pools but remain on already-scheduled interviews. |
| updatedAt | string (ISO 8601 datetime) | Timestamp of last update to the user record. | system-generated | system-generated | Returned in user.search and user.list responses. |
| includeDeactivated | boolean (request param) | Request parameter on user.list to include deactivated users in results. | Not a field on the user object itself; a filter param for user.list. |
Core endpoints
List all users
- Method: POST
- URL:
https://api.ashbyhq.com/user.list - Watch out for: Requires organizationRead permission. Pass includeDeactivated: true to see deactivated/terminated users. Paginated via cursor + syncToken.
Request example
curl -u API_KEY: -X POST https://api.ashbyhq.com/user.list \
-H 'Content-Type: application/json' \
-d '{"cursor": null, "limit": 50, "includeDeactivated": false}'
Response example
{
"success": true,
"results": [{"id": "uuid", "firstName": "Jane", "lastName": "Doe",
"email": "jane@example.com", "globalRole": "elevatedAccess",
"isEnabled": true, "updatedAt": "2025-01-01T00:00:00Z"}],
"moreDataAvailable": false,
"nextCursor": null
}
Get user by ID
- Method: POST
- URL:
https://api.ashbyhq.com/user.info - Watch out for: Requires organizationRead permission. All Ashby API calls use POST even for lookups.
Request example
curl -u API_KEY: -X POST https://api.ashbyhq.com/user.info \
-H 'Content-Type: application/json' \
-d '{"userId": "<uuid>"}'
Response example
{
"success": true,
"results": {"id": "uuid", "firstName": "Jane",
"lastName": "Doe", "email": "jane@example.com",
"globalRole": "elevatedAccess", "isEnabled": true}
}
Search user by email
- Method: POST
- URL:
https://api.ashbyhq.com/user.search - Watch out for: Requires organizationRead permission. .search endpoints are not paginated; intended for specific lookups only.
Request example
curl -u API_KEY: -X POST https://api.ashbyhq.com/user.search \
-H 'Content-Type: application/json' \
-d '{"email": "jane@example.com"}'
Response example
{
"success": true,
"results": {"id": "uuid", "firstName": "Jane",
"lastName": "Doe", "email": "jane@example.com",
"globalRole": "limitedAccess", "isEnabled": true,
"updatedAt": "2025-01-01T00:00:00Z"}
}
Add user to hiring team (job or application level)
- Method: POST
- URL:
https://api.ashbyhq.com/hiringTeam.addMember - Watch out for: Requires organizationWrite permission. Must supply either applicationId or jobId (not both). roleId must be a valid UUID from the org's role list.
Request example
curl -u API_KEY: -X POST https://api.ashbyhq.com/hiringTeam.addMember \
-H 'Content-Type: application/json' \
-d '{"jobId": "<uuid>", "teamMemberId": "<uuid>", "roleId": "<uuid>"}'
Response example
{
"success": true,
"results": {"teamMemberId": "uuid", "roleId": "uuid",
"jobId": "uuid"}
}
Add user to hiring team at application level
- Method: POST
- URL:
https://api.ashbyhq.com/application.addHiringTeamMember - Watch out for: Requires candidateWrite permission (note: singular, not candidatesWrite). applicationId, teamMemberId, and roleId are all required UUIDs.
Request example
curl -u API_KEY: -X POST https://api.ashbyhq.com/application.addHiringTeamMember \
-H 'Content-Type: application/json' \
-d '{"applicationId": "<uuid>", "teamMemberId": "<uuid>", "roleId": "<uuid>"}'
Response example
{
"success": true,
"results": {"teamMemberId": "uuid", "roleId": "uuid",
"applicationId": "uuid"}
}
Get API key info
- Method: POST
- URL:
https://api.ashbyhq.com/apiKey.info - Watch out for: Requires apiKeysRead permission. Useful for verifying key identity and creation date in automated setups.
Request example
curl -u API_KEY: -X POST https://api.ashbyhq.com/apiKey.info \
-H 'Content-Type: application/json' \
-d '{}'
Response example
{
"success": true,
"results": {"title": "My Integration Key",
"createdAt": "2024-06-01T00:00:00Z"}
}
Create webhook
- Method: POST
- URL:
https://api.ashbyhq.com/webhook.create - Watch out for: Requires apiKeysWrite scope. A 'ping' webhook is sent on creation; if your endpoint returns >400 or times out, the webhook is created in a disabled state.
Request example
curl -u API_KEY: -X POST https://api.ashbyhq.com/webhook.create \
-H 'Content-Type: application/json' \
-d '{"webhookType": "candidateStageChange", "requestUrl": "https://your.server/hook", "secretToken": "s3cr3t"}'
Response example
{
"success": true,
"results": {"id": "uuid", "enabled": true,
"requestUrl": "https://your.server/hook",
"webhookType": "candidateStageChange"}
}
Delete webhook
- Method: POST
- URL:
https://api.ashbyhq.com/webhook.delete - Watch out for: Requires apiKeysWrite permission. webhookId must be a valid UUID of an existing webhook setting.
Request example
curl -u API_KEY: -X POST https://api.ashbyhq.com/webhook.delete \
-H 'Content-Type: application/json' \
-d '{"webhookId": "<uuid>"}'
Response example
{
"success": true,
"results": {"webhookId": "uuid"}
}
Rate limits, pagination, and events
- Rate limits: Ashby enforces rate limits per API key. Specific numeric thresholds are not publicly documented. Rate limiting is applied at the request-handling layer. Contact Ashby support for limit increases if high-volume syncs are needed.
- Rate-limit headers: No
- Retry-After header: No
- Rate-limit notes: No official documentation of specific rate limit numbers or rate-limit response headers. Ashby's security overview confirms rate limiting exists at the request-handling layer. Reach out to support@ashbyhq.com for high-volume use cases.
- Pagination method: cursor
- Default page size: Not documented
- Max page size: Not documented
- Pagination pointer: cursor
| Plan | Limit | Concurrent |
|---|---|---|
| All plans (per API key) | Not publicly specified; described as 'generous' by Ashby |
- Webhooks available: Yes
- Webhook notes: Webhooks are configured in the Ashby Admin panel (Admin > Integrations > Webhooks) or via the webhook.create API endpoint. Each webhook targets a single event type and a payload URL. An optional secret token enables HMAC signature verification via the Ashby-Signature HTTP header on incoming payloads. Some events trigger cascading related webhooks (e.g., candidateStageChange also fires applicationUpdate).
- Alternative event strategy: Poll user.list with syncToken for incremental user sync if webhooks are not available for user-specific events.
- Webhook events: candidateStageChange, applicationUpdate, jobPostingPublish, jobPostingUnpublish, candidateUpdate, interviewScheduleCreate, interviewScheduleUpdate, offerCreate, offerUpdate
SCIM API status
SCIM available: Yes
SCIM version: 2.0
Plan required: Legacy Plus, Plus, or Enterprise (not available on Foundations plan)
Endpoint: Delivered via WorkOS; specific SCIM base URL is provided by Ashby support upon setup request.
Supported operations: User provisioning (create), User deprovisioning (deactivate/remove), Directory sync from Okta, Microsoft Entra ID (Azure AD), Google Workspace
Limitations:
- Permission/role mapping is not supported via SCIM; all SCIM-provisioned users are created as 'limitedAccess' and must have roles assigned manually in Ashby.
- Log streams and domain verification are not supported.
- SCIM setup requires contacting Ashby support (support@ashbyhq.com) to receive the WorkOS setup link; it cannot be self-served.
- SSO must be configured alongside or before SCIM; both are delivered through WorkOS.
- No public SCIM endpoint URL documented; provided per-customer by Ashby support.
Common scenarios
Three primary automation scenarios are well-supported by the API. First, full and incremental user sync via user.
list using cursor pagination and syncToken - note that syncToken can expire, requiring a full re-sync on sync_token_expired errors. Second, targeted user lookup by email via user.
search (non-paginated, single-record intent) followed by hiring team assignment via hiringTeam. addMember, which requires valid UUIDs for jobId, teamMemberId, and roleId.
Third, SCIM-based provisioning from Okta, Entra ID, or Google Workspace on Legacy Plus, Plus, or Enterprise plans - delivered via WorkOS, initiated by contacting support@ashbyhq. com, and limited to user create/deactivate operations with no group-to-role mapping.
Sync all active Ashby users to an external directory
- Create an API key with organizationRead permission in Admin > Integrations > API Keys.
- POST to https://api.ashbyhq.com/user.list with an empty body to retrieve the first page.
- If moreDataAvailable is true, pass nextCursor in subsequent requests until moreDataAvailable is false.
- Store the final syncToken from the last response.
- On subsequent syncs, pass the stored syncToken to retrieve only updated records since the last sync.
- If sync_token_expired error is returned, discard the token and perform a full re-sync from step 2.
Watch out for: syncToken can expire between syncs. Always handle the sync_token_expired error code and fall back to a full sync.
Look up an Ashby user by email and add them to a job's hiring team
- POST to https://api.ashbyhq.com/user.search with {"email": "user@example.com"} using an API key with organizationRead.
- Extract the id (UUID) from the response as teamMemberId.
- Obtain the target jobId and the desired roleId (from your org's role configuration).
- POST to https://api.ashbyhq.com/hiringTeam.addMember with {"jobId": "
", "teamMemberId": " ", "roleId": " "} using an API key with organizationWrite.
Watch out for: user.search is not paginated and is intended for specific lookups only. The roleId must be a valid UUID from the organization's configured roles, not a role name string.
Provision users via SCIM from Okta or Entra ID
- Confirm the Ashby account is on Legacy Plus, Plus, or Enterprise plan.
- Email support@ashbyhq.com requesting SCIM setup; provide the IT service account email for error notifications.
- Ashby support will respond with a WorkOS setup link.
- Complete the WorkOS SCIM configuration in your IdP (Okta, Entra ID, or Google Workspace).
- Assign users/groups in the IdP to push to Ashby.
- After provisioning, log into Ashby and manually update each new user's globalRole from limitedAccess to the appropriate permission level.
Watch out for: SCIM does not map IdP groups to Ashby permission roles. Every SCIM-provisioned user is created as limitedAccess regardless of IdP group membership. Role assignment must be done manually in Ashby after provisioning.
Why building this yourself is a trap
The most significant API caveat is that SCIM does not support permission or role mapping: every SCIM-provisioned user is created as limitedAccess regardless of IdP group assignments, and role elevation must be done manually in the Ashby admin panel after provisioning.
Rate limit thresholds are not publicly documented - Ashby confirms rate limiting exists at the request-handling layer but does not expose rate-limit response headers or retry-after values, making backoff logic implementation speculative without direct support engagement.
Confidential job and private field data are excluded from API responses by default and must be explicitly enabled per API key, meaning integrations built without these flags will silently return incomplete records.
Automate Ashby 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.