Candid Health SCIM 2.0 API
Candid Health implements SCIM 2.0 for programmatic user and organization provisioning. This document is the external reference for the SCIM endpoints.
Contents
- Quickstart
- Conventions
- Authentication
- Scoping
- Discovery endpoints
- Users
- Groups (Organizations)
- Role assignment
- PATCH operations
- Filtering and pagination
- Errors
- IdP compatibility notes
- Idempotency
- Future features
Quickstart
Note on terminology: in this API, Groups represent organizations. The SCIM standard names the resource
Group, so the endpoints are/Groups, but everywhere in this document “Group” and “organization” refer to the same thing.
1. Obtain a SCIM token
Contact Candid Health to provision a SCIM token for your account. You will be given:
- A bearer token (sent in the
Authorizationheader) - The base URL for the API (production and any non-production environments)
- The UUID of the organization the token is scoped to
The token grants access to that organization and all of its descendants in the org hierarchy. See Scoping.
2. Discover the organizations you can manage
The response lists every organization in your token’s scope. Record the id of any organization you’ll need to reference when creating users or child organizations.
3. (Optional) Create child organizations
If your token is scoped to an enterprise organization, you can create child organizations under it:
4. Provision users
5. Verify
Fetch the organization to see the user in its members list:
Conventions
- Base URL: all SCIM endpoints are mounted at
/scim/v2. - Content type: SCIM requires
application/scim+json. Standardapplication/jsonis also accepted on requests for convenience; responses always setapplication/scim+json. - IDs: all resource identifiers (
id,parentId,organizationId) are UUIDs. - Emails: stored lowercase and compared case-insensitively. Mixed-case input is normalized on write.
- Time: ISO-8601 timestamps in UTC.
- Request body size limit: 100 KB. Larger bodies are rejected with
413 Payload Too Large. - Rate limiting: requests are rate-limited per source IP. Standard
RateLimit-*response headers indicate remaining quota; exceeding the limit returns429 Too Many Requests. - Read-only attributes:
id,meta,active(User), andexternalId(User) are server-managed. They are returned in responses but ignored on writes. UserexternalIdis populated by the IdP (Auth0) on first login; it cannot be set by the provisioning client. schemasarray: every request body must include aschemasarray per RFC 7643. The array is not strictly validated — you may list only the resource’s core schema (e.g.["urn:ietf:params:scim:schemas:core:2.0:User"]) even when using the Candid extension. We recommend listing every schema you reference for clarity and forward compatibility.- Unknown attributes are silently stripped. Attributes that aren’t part of the supported schemas (e.g. SCIM enterprise extensions, vendor-specific fields sent by your IdP) are silently dropped from request bodies — they will not appear on the resource and will not cause an error. This means typos in attribute names also go undetected; double-check spelling.
The full set of supported features is advertised by GET /scim/v2/ServiceProviderConfig.
Authentication
All SCIM requests must include a Bearer token issued by Candid Health:
Tokens are scoped to a single organization. The scope of operations a token can perform is described in Scoping.
Authentication failure modes:
Scoping
Each SCIM token is bound to a single Candid organization. When the token is used, the server resolves that organization plus all of its descendants in the organization hierarchy as the token’s allowed scope.
The scope is enforced on every endpoint:
- Users — a user is in scope if they have an active membership in any organization in the allowed scope.
GET /Usersreturns only in-scope users;GET /Users/:id,PUT /Users/:id,PATCH /Users/:id, andDELETE /Users/:idreturn403for out-of-scope users. - Groups — a group is in scope if its ID is in the allowed scope.
GET /Groupsfilters to in-scope groups; the per-ID operations return403for out-of-scope groups. - Role assignments — any
organizationIdreferenced in a User create/update body must be in scope. The same applies to organization IDs embedded in legacy role values (PRIMARY_ORG,ADDITIONAL_ORGS). - Group parent —
parentIdon create/replace must be in scope.
Out-of-scope references return 403 Forbidden with detail Organization is not within the scope of this SCIM token. (or the User equivalent).
Discovery endpoints
Standard SCIM discovery endpoints per RFC 7644 §4.
GET /scim/v2/ServiceProviderConfig
Returns the provider’s capability advertisement.
GET /scim/v2/ResourceTypes
Lists the supported resource types: User and Group. Each declares its core schema and any extension schemas.
GET /scim/v2/Schemas
Returns full attribute definitions for:
urn:ietf:params:scim:schemas:core:2.0:Userurn:ietf:params:scim:schemas:core:2.0:Groupurn:candid:scim:1.0:User— Candid User extensionurn:candid:scim:1.0:Organization— Candid Group/Organization extension
Users
The User resource represents a person who can sign in. Schema URI: urn:ietf:params:scim:schemas:core:2.0:User.
Attributes
Candid User extension
organizationId— UUID of an organization the user should be a member of.roles— list of role names (display name or normalized name accepted) for that organization.
The extension is always included in User responses (
GET /Users,GET /Users/:id, and in the response bodies ofPOST/PUT/PATCH). You do not need to opt in viaattributes.On
GET /Users, roles are fetched per user in the returned page. Listing latency therefore scales withcount— paginate with smaller page sizes if you’re hitting wall-clock limits.See Future features for the current limitation on per-organization role differentiation.
POST /scim/v2/Users — create a user
Response: 201 Created, Location: /scim/v2/Users/{id}, body is the full User resource. externalId is omitted until the user completes their first login.
Error cases:
GET /scim/v2/Users/:id — fetch a user
Returns the User resource, or 404 if the user does not exist or 403 if the user is not in the token’s scope.
GET /scim/v2/Users — list users
Supports filtering and pagination. See Filtering and pagination.
Filterable fields: userName, externalId.
userName co "..." (contains) is special-cased: it matches against the email or against the concatenated "{firstName} {lastName}", so it can be used as a free-text search.
Response:
PUT /scim/v2/Users/:id — replace a user
Replaces the user’s mutable attributes wholesale. Roles passed in the body fully replace the user’s existing role assignments — anything not listed is removed.
PATCH /scim/v2/Users/:id — partial update
See PATCH operations for the general format. Allowed paths on User:
userNamename.givenNamename.familyNamedisplayNameroles
Example — rename a user:
Example — change role assignments (legacy format):
Notes:
opis normalized to lowercase, soAdd/add/ADDare equivalent.addandreplacebehave the same for scalar paths.removeis not supported on any User path.- PATCH operations without a
pathare not supported — use explicit paths.
DELETE /scim/v2/Users/:id — delete a user
Permanently deactivates the user.
- Marks the user record as deleted. Subsequent
GET /Users/:idandGET /Usersreturn404/ omit the user. - Deactivates all of the user’s organization memberships.
- Removes the user’s role tuples from the authorization model.
- The associated Auth0 account is hard-deleted.
Response: 204 No Content.
The deletion is permanent — the user’s UUID becomes unreachable forever. If you need to re-onboard the same person later, issue a new POST /Users with the same email; you’ll receive a new UUID. Update your IdP’s mapping accordingly.
Deactivation
The active attribute is server-managed.
- It always returns
truefor users that exist (deleted users return404, notactive: false). - On
POST /UsersandPUT /Users/:id, sendingactivein the body is silently ignored. - On
PATCH /Users/:id, sendingpath: "active"is rejected with400 invalidValue(the path is not in the allowed list).
The only way to deactivate a user is DELETE /scim/v2/Users/:id. There is no soft-pause; deletion is the only off-switch.
Email-domain restriction
An organization may be configured with a list of allowed email domains. When a user is added to such an organization (via create, replace, or patch with role assignments), the user’s email domain must match one of the configured domains. Mismatches return:
Organizations without any configured email domains accept users from any domain.
Groups (Organizations)
In Candid Health, SCIM Groups represent organizations. Schema URI: urn:ietf:params:scim:schemas:core:2.0:Group.
Candid supports a two-level organization hierarchy:
- Enterprise organizations are top-level groups that can contain children. Flagged with
isEnterprise: trueon creation. - Child organizations are singletons that reference an enterprise organization via
parentId.
Attributes
Candid Organization extension
POST /scim/v2/Groups — create an organization
Create an enterprise organization:
Create a child organization under an enterprise organization:
Response: 201 Created, Location: /scim/v2/Groups/{id}, body is the full Group resource. The extension object always contains parentId (or omits it for top-level orgs) and includes isSso only for enterprise organizations.
Child (singleton) organization response — isSso is omitted:
Enterprise organization response — isSso is present:
Error cases:
GET /scim/v2/Groups/:id — fetch an organization
Returns the Group resource, including its current members array (computed from active User memberships). The display field is "{givenName} {familyName}", not displayName.
GET /scim/v2/Groups — list organizations
Supports filtering and pagination. Filterable fields: displayName, externalId.
PUT /scim/v2/Groups/:id — replace an organization
Updates displayName, externalId, and parentId. isEnterprise and isSso in the extension are silently ignored — these are fixed at creation time.
PATCH /scim/v2/Groups/:id — partial update
Allowed paths:
displayNameexternalId
remove is only supported for externalId.
Operations on members are explicitly rejected — group membership is managed via User role assignments:
DELETE /scim/v2/Groups/:id — delete an organization
Soft-deletes the organization (deactivatedAt is set; reads return 404 afterward). If the organization is an enterprise organization, its enterprise marker is removed as well.
Returns 400 if the organization has any active children. Reassign or delete the children first.
Response: 204 No Content.
Role assignment
A user can have one or more roles in each organization they belong to. Roles control what actions the user is allowed to take. Permissions accumulate across all roles assigned to a user in a given organization.
Roles are assigned through the User resource on create (POST /Users), replace (PUT /Users/:id), and PATCH (PATCH /Users/:id with path: "roles"). There are two supported formats; pick one and use it consistently.
Modern format (preferred)
The Candid User extension allows per-organization role assignments:
Legacy format
Use the standard SCIM roles attribute with four entry types:
PRIMARY_ORGandADDITIONAL_ORGSenumerate the organization IDs the user belongs to. They are treated identically — both contribute organization memberships. MultiplePRIMARY_ORGentries are allowed.- ⚠️
ADDITIONAL_ORGS.valueis a comma-separated list of UUIDs in a single string, not a JSON array. This is the convention from our legacy provisioning system. ROLEentries are role names. MultipleROLEentries are allowed; each role is applied to every organization in the user’s set (there’s no way to differentiate per-org assignments with the legacy format — use the modern format if you need that).- ⚠️
ORGANIZATION_ADMIN.valueis a string ("true"/"false"), not a boolean. Anything other than the literal string"true"is treated asfalse.
Because the legacy format applies every ROLE to every org uniformly, the modern format is strongly preferred for any new integration.
Mixing formats
Sending both organizationRoles (modern) and roles (legacy) in the same request is rejected:
Role names
roles (modern) and ROLE (legacy) entries accept any name or alias listed in the role catalog below. Unknown role names — or known roles that are not on the SCIM-assignable allowlist — are rejected with 400 invalidValue (detail: Role "<name>" is not assignable via SCIM.).
Role catalog
The roles below can be assigned via SCIM. The same list is advertised as canonicalValues on the modern organizationRoles[].roles attribute in GET /Schemas, so SCIM clients that codegen from the schema can populate role pickers automatically.
Roles outside this list exist in Candid Health (e.g., internal Candid staff roles) but are intentionally not assignable through the SCIM API.
Users can hold multiple roles in the same organization — for example, Editor plus Remits App to give an editor access to the Remits application. Permissions accumulate across all assigned roles.
The legacy
{ "type": "ORGANIZATION_ADMIN", "value": "true" }entry is preserved for backwards compatibility with the previous provisioning system but is not a SCIM-assignable role and is not listed incanonicalValues. New integrations should not rely on it; useOrganization Admininstead.
PUT vs. PATCH semantics
POST /Usersadds the user to all listed organizations with the listed roles.PUT /Users/:idandPATCH /Users/:idwithpath: "roles"replace the user’s role assignments wholesale:- Roles within an organization are replaced — any role not listed for an org is removed from that org.
- Organization memberships are reconciled — orgs newly listed are added, and orgs the user currently belongs to but that are omitted from the request are removed (the membership is deactivated, its OpenFGA role tuples are cleared, and the per-org removal is forwarded to candid-api).
To change roles within an existing org, list the org with its new role set. To grant access to a new org, add it to the list. To remove a user from a specific org, omit that org from a PUT/PATCH (path: "roles") request. To deactivate the user across all orgs, use DELETE /scim/v2/Users/:id.
PATCH operations
PATCH bodies follow RFC 7644 §3.5.2:
Notes:
Operations(note the capitalO) must contain at least one entry.opis case-insensitive (add,Add,ADDall accepted).- A
pathis required on every operation; path-less PATCH is not supported. - Unknown paths are rejected with
400. removeis only meaningful for a small subset of paths (currentlyexternalIdon Group only).- See the per-resource sections for the list of allowed paths.
Filtering and pagination
List endpoints (GET /Users, GET /Groups) accept:
Filter syntax
Only single-clause filters are supported. The grammar is:
eq— exact match.co— substring match (case-insensitive). For/Users,userName co "..."additionally searches"{firstName} {lastName}".
Supported fields:
/Users:userName,externalId/Groups:displayName,externalId
Examples:
Unsupported syntax (logical operators and/or, parenthesized expressions, other operators like sw/ew/gt) returns 400 invalidFilter.
Response format
Errors
All errors follow RFC 7644 §3.12:
The Content-Type of error responses is always application/scim+json.
IdP compatibility notes
Common provisioning systems (Okta, Microsoft Entra/Azure AD, OneLogin, etc.) emit SCIM payloads that don’t always match our supported surface. The most frequent friction points:
Deactivation via active: false
Most IdPs send PATCH /Users/:id with { "op": "replace", "path": "active", "value": false } when a user is suspended. Our API rejects this with 400 invalidValue because active is not in the allowed PATCH paths.
Equivalent on PUT (sending active: false in a full resource body) is silently ignored — the user stays active.
To deprovision a user, send DELETE /scim/v2/Users/:id. If your IdP cannot be configured to issue DELETE on suspend, see your IdP’s documentation for “delete on unassign” or an equivalent setting.
Path-less PATCH
Some IdPs (notably Microsoft Entra ID) issue PATCH operations without a path, e.g.:
This is rejected with 400:
You must rewrite these to use explicit paths, one operation per attribute:
PATCH filter expressions in paths
Bracket-filter PATCH paths like emails[type eq "work"].value are rejected because the path doesn’t match an allowed value. Use the bare attribute path (userName to change email).
Group membership management
Many IdPs assign users to groups by issuing PATCH /Groups/:id with operations on members. We reject this with 400:
Group membership in our model is derived from User role assignments, not directly managed. Configure your IdP to manage organization access on the User side, using the modern organizationRoles extension (preferred) or the legacy roles attribute.
Unsupported standard extensions
The SCIM enterprise extension (urn:ietf:params:scim:schemas:extension:enterprise:2.0:User) and any other custom attributes your IdP sends are silently stripped — no error, but no effect. Use the Candid extensions (urn:candid:scim:1.0:User, urn:candid:scim:1.0:Organization) for any custom data the API can act on.
Unsupported filter syntax
Compound filters (and, or, parenthesization) and operators beyond eq/co (sw, ew, gt, pr, etc.) return 400 invalidFilter. If your IdP issues these for sync, you may need to disable filtering and accept a full sync, or replicate the filtering on your side.
Idempotency
Retries are common in IdP provisioning. The behavior of each verb under retry:
Future features
The following capabilities are not yet implemented. Requests that depend on them will return errors as described.
Per-organization role differentiation
The modern urn:candid:scim:1.0:User.organizationRoles extension accepts a roles array per organization, but all organizations assigned in a single request must currently resolve to the same set of role assignments. The model allows distinct per-organization roles in the wire format today, and per-organization role differentiation is planned but not yet active.
Bulk operations
POST /scim/v2/Bulk is not supported. The ServiceProviderConfig advertises bulk.supported: false. Issue one request per resource.
Sorting
The sortBy and sortOrder parameters on list endpoints are not implemented. ServiceProviderConfig advertises sort.supported: false. The current default ordering on list endpoints is undefined; clients should not rely on a specific order.
Password changes
Candid Health does not store user passwords — authentication is delegated to Auth0 (or to the customer’s IdP for SSO organizations). PUT /scim/v2/Me/password is not supported and ServiceProviderConfig advertises changePassword.supported: false.
ETag / optimistic concurrency
Resource versioning via ETag / If-Match is not implemented. ServiceProviderConfig advertises etag.supported: false. Concurrent writes are last-write-wins.
Compound filter expressions
Only single-clause eq and co filters are supported. Compound expressions (and, or, parenthesization) and additional operators (sw, ew, gt, ge, lt, le, pr, ne) return 400 invalidFilter.