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

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 Authorization header)
  • 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

GET /scim/v2/Groups
Authorization: Bearer <token>

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:

POST /scim/v2/Groups
{
"schemas": ["urn:ietf:params:scim:schemas:core:2.0:Group", "urn:candid:scim:1.0:Organization"],
"displayName": "Acme Cardiology Clinic",
"urn:candid:scim:1.0:Organization": {
"parentId": "<enterprise-org-id-from-step-2>"
}
}

4. Provision users

POST /scim/v2/Users
{
"schemas": ["urn:ietf:params:scim:schemas:core:2.0:User", "urn:candid:scim:1.0:User"],
"userName": "alex.morgan@example.com",
"name": { "givenName": "Alex", "familyName": "Morgan" },
"urn:candid:scim:1.0:User": {
"organizationRoles": [
{
"organizationId": "<org-id-from-step-2-or-3>",
"roles": ["Biller"]
}
]
}
}

5. Verify

Fetch the organization to see the user in its members list:

GET /scim/v2/Groups/<org-id>

Conventions

  • Base URL: all SCIM endpoints are mounted at /scim/v2.
  • Content type: SCIM requires application/scim+json. Standard application/json is also accepted on requests for convenience; responses always set application/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 returns 429 Too Many Requests.
  • Read-only attributes: id, meta, active (User), and externalId (User) are server-managed. They are returned in responses but ignored on writes. User externalId is populated by the IdP (Auth0) on first login; it cannot be set by the provisioning client.
  • schemas array: every request body must include a schemas array 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:

Authorization: Bearer <scim-token>

Tokens are scoped to a single organization. The scope of operations a token can perform is described in Scoping.

Authentication failure modes:

ConditionStatusDetail
Missing or malformed Authorization header401Authorization header with Bearer token is required
Token does not match a known active token401Invalid bearer token
Token is expired (expires_at in the past)401Token has expired

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 /Users returns only in-scope users; GET /Users/:id, PUT /Users/:id, PATCH /Users/:id, and DELETE /Users/:id return 403 for out-of-scope users.
  • Groups — a group is in scope if its ID is in the allowed scope. GET /Groups filters to in-scope groups; the per-ID operations return 403 for out-of-scope groups.
  • Role assignments — any organizationId referenced 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 parentparentId on 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.

{
"schemas": ["urn:ietf:params:scim:schemas:core:2.0:ServiceProviderConfig"],
"documentationUri": "https://datatracker.ietf.org/doc/html/rfc7644",
"patch": { "supported": true },
"bulk": { "supported": false, "maxOperations": 0, "maxPayloadSize": 0 },
"filter": { "supported": true, "maxResults": 100 },
"changePassword": { "supported": false },
"sort": { "supported": false },
"etag": { "supported": false },
"authenticationSchemes": [
{
"type": "oauthbearertoken",
"name": "OAuth Bearer Token",
"description": "Authentication scheme using the OAuth Bearer Token Standard",
"specUri": "https://www.rfc-editor.org/info/rfc6750"
}
]
}

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:User
  • urn:ietf:params:scim:schemas:core:2.0:Group
  • urn:candid:scim:1.0:User — Candid User extension
  • urn: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

AttributeTypeRequiredNotes
userNamestringyesEmail address. Must match emails[0].value when emails is present. Stored lowercase.
externalIdstringread-onlyThe user’s Auth0 ID. Populated automatically on first login. Ignored if sent on create/replace/patch. Returned in GET responses once set; omitted before the user has logged in.
name.givenNamestringyesFirst name.
name.familyNamestringyesLast name.
displayNamestringnoDefaults to "{givenName} {familyName}" if omitted.
emailsarraynoAt most one entry; emails[0].value must equal userName. The type and primary fields are accepted but ignored — the address from emails[0] is always the primary.
activebooleanread-onlytrue for active users. See Deactivation.
rolesarraynoLegacy role assignment format. See Role assignment.
urn:candid:scim:1.0:User.organizationRolesarraynoModern role assignment format. Preferred.

Candid User extension

urn:candid:scim:1.0:User
{
"organizationRoles": [
{
"organizationId": "11111111-1111-1111-1111-111111111111",
"roles": ["Biller"]
}
]
}
  • 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 of POST / PUT / PATCH). You do not need to opt in via attributes.

On GET /Users, roles are fetched per user in the returned page. Listing latency therefore scales with count — 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

POST /scim/v2/Users
Content-Type: application/scim+json
Authorization: Bearer <token>
{
"schemas": ["urn:ietf:params:scim:schemas:core:2.0:User", "urn:candid:scim:1.0:User"],
"userName": "alex.morgan@example.com",
"name": {
"givenName": "Alex",
"familyName": "Morgan"
},
"displayName": "Alex Morgan",
"emails": [{ "value": "alex.morgan@example.com", "primary": true }],
"urn:candid:scim:1.0:User": {
"organizationRoles": [
{
"organizationId": "11111111-1111-1111-1111-111111111111",
"roles": ["Biller"]
}
]
}
}

Response: 201 Created, Location: /scim/v2/Users/{id}, body is the full User resource. externalId is omitted until the user completes their first login.

{
"schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"],
"id": "9c8b1a3e-5e2f-4b6e-9d8c-1f2a3b4c5d6e",
"userName": "alex.morgan@example.com",
"name": { "givenName": "Alex", "familyName": "Morgan" },
"displayName": "Alex Morgan",
"emails": [{ "value": "alex.morgan@example.com", "primary": true }],
"active": true,
"meta": {
"resourceType": "User",
"created": "2026-05-26T14:00:00.000Z",
"lastModified": "2026-05-26T14:00:00.000Z",
"location": "https://auth.example.com/scim/v2/Users/9c8b1a3e-5e2f-4b6e-9d8c-1f2a3b4c5d6e"
}
}

Error cases:

ConditionStatusscimType
emails[0].value does not match userName400invalidValue
emails contains more than one entry400invalidValue
userName already in use by an active user409uniqueness
Referenced organizationId is not in token scope403
Referenced organization does not exist400invalidValue
organizationId is not a valid UUID400invalidValue
Role name does not resolve to a known role400invalidValue
Both legacy roles and modern organizationRoles provided400invalidValue
User’s email domain is not allowed by the organization (see Email-domain restriction)400invalidValue

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.

GET /scim/v2/Users?filter=userName%20eq%20%22alex.morgan%40example.com%22&count=50

Response:

{
"schemas": ["urn:ietf:params:scim:api:messages:2.0:ListResponse"],
"totalResults": 1,
"startIndex": 1,
"itemsPerPage": 50,
"Resources": [
{
/* ... User resource ... */
}
]
}

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.

PUT /scim/v2/Users/9c8b1a3e-5e2f-4b6e-9d8c-1f2a3b4c5d6e
{
"schemas": ["urn:ietf:params:scim:schemas:core:2.0:User", "urn:candid:scim:1.0:User"],
"userName": "alex.morgan@example.com",
"name": { "givenName": "Alex", "familyName": "Morgan" },
"displayName": "Alex Morgan",
"urn:candid:scim:1.0:User": {
"organizationRoles": [
{
"organizationId": "11111111-1111-1111-1111-111111111111",
"roles": ["Organization Admin"]
},
{
"organizationId": "22222222-2222-2222-2222-222222222222",
"roles": ["Organization Admin"]
}
]
}
}

PATCH /scim/v2/Users/:id — partial update

See PATCH operations for the general format. Allowed paths on User:

  • userName
  • name.givenName
  • name.familyName
  • displayName
  • roles

Example — rename a user:

{
"schemas": ["urn:ietf:params:scim:api:messages:2.0:PatchOp"],
"Operations": [
{ "op": "replace", "path": "name.givenName", "value": "Alexandra" },
{ "op": "replace", "path": "displayName", "value": "Alexandra Morgan" }
]
}

Example — change role assignments (legacy format):

{
"schemas": ["urn:ietf:params:scim:api:messages:2.0:PatchOp"],
"Operations": [
{
"op": "replace",
"path": "roles",
"value": [
{ "type": "PRIMARY_ORG", "value": "11111111-1111-1111-1111-111111111111" },
{ "type": "ROLE", "value": "Biller" }
]
}
]
}

Notes:

  • op is normalized to lowercase, so Add / add / ADD are equivalent.
  • add and replace behave the same for scalar paths.
  • remove is not supported on any User path.
  • PATCH operations without a path are 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/:id and GET /Users return 404 / 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 true for users that exist (deleted users return 404, not active: false).
  • On POST /Users and PUT /Users/:id, sending active in the body is silently ignored.
  • On PATCH /Users/:id, sending path: "active" is rejected with 400 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:

{
"schemas": ["urn:ietf:params:scim:api:messages:2.0:Error"],
"detail": "Email domain \"@example.com\" is not allowed for organization 11111111-1111-1111-1111-111111111111. Allowed: @acme-health.com, @acme.org.",
"status": "400",
"scimType": "invalidValue"
}

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: true on creation.
  • Child organizations are singletons that reference an enterprise organization via parentId.

Attributes

AttributeTypeRequiredNotes
displayNamestringyesHuman-readable organization name.
externalIdstringnoStable identifier from your IdP. Globally unique when set.
membersarrayread-onlyComputed from User role assignments. Cannot be set via Group endpoints.
urn:candid:scim:1.0:Organization.parentIdstringnoUUID of the parent enterprise organization. Echoed back in responses.
urn:candid:scim:1.0:Organization.isEnterprisebooleannoMark as an enterprise organization (can contain children). Default false. Write-only: accepted on create, not echoed in responses, and not modifiable via PUT/PATCH. Infer enterprise status from the presence of isSso in the response.
urn:candid:scim:1.0:Organization.isSsobooleannoIf true, this enterprise organization uses SSO. Only meaningful when isEnterprise: true. Returned in responses only for enterprise organizations (omitted entirely for singleton orgs). Not modifiable via PUT/PATCH.

Candid Organization extension

urn:candid:scim:1.0:Organization
{
"parentId": "00000000-0000-0000-0000-000000000001",
"isEnterprise": true,
"isSso": false
}

POST /scim/v2/Groups — create an organization

Create an enterprise organization:

POST /scim/v2/Groups
{
"schemas": ["urn:ietf:params:scim:schemas:core:2.0:Group", "urn:candid:scim:1.0:Organization"],
"displayName": "Acme Health Network",
"externalId": "acme-health-network",
"urn:candid:scim:1.0:Organization": {
"isEnterprise": true,
"isSso": false
}
}

Create a child organization under an enterprise organization:

{
"schemas": ["urn:ietf:params:scim:schemas:core:2.0:Group", "urn:candid:scim:1.0:Organization"],
"displayName": "Acme Cardiology Clinic",
"externalId": "acme-cardiology",
"urn:candid:scim:1.0:Organization": {
"parentId": "00000000-0000-0000-0000-000000000001"
}
}

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:

{
"schemas": ["urn:ietf:params:scim:schemas:core:2.0:Group", "urn:candid:scim:1.0:Organization"],
"id": "22222222-2222-2222-2222-222222222222",
"externalId": "acme-cardiology",
"displayName": "Acme Cardiology Clinic",
"members": [],
"urn:candid:scim:1.0:Organization": {
"parentId": "00000000-0000-0000-0000-000000000001"
},
"meta": {
"resourceType": "Group",
"created": "2026-05-26T14:00:00.000Z",
"lastModified": "2026-05-26T14:00:00.000Z",
"location": "https://auth.example.com/scim/v2/Groups/22222222-2222-2222-2222-222222222222"
}
}

Enterprise organization response — isSso is present:

{
"schemas": ["urn:ietf:params:scim:schemas:core:2.0:Group", "urn:candid:scim:1.0:Organization"],
"id": "00000000-0000-0000-0000-000000000001",
"externalId": "acme-health-network",
"displayName": "Acme Health Network",
"members": [],
"urn:candid:scim:1.0:Organization": {
"isSso": false
},
"meta": {
"resourceType": "Group",
"created": "2026-05-26T14:00:00.000Z",
"lastModified": "2026-05-26T14:00:00.000Z",
"location": "https://auth.example.com/scim/v2/Groups/00000000-0000-0000-0000-000000000001"
}
}

Error cases:

ConditionStatusscimType
parentId is malformed400invalidValue
parentId refers to a non-enterprise organization or unknown ID400invalidValue
parentId is outside the token’s scope403
externalId already in use409uniqueness

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.

{
"members": [
{
"value": "9c8b1a3e-5e2f-4b6e-9d8c-1f2a3b4c5d6e",
"display": "Alex Morgan",
"type": "User",
"$ref": "https://auth.example.com/scim/v2/Users/9c8b1a3e-5e2f-4b6e-9d8c-1f2a3b4c5d6e"
}
]
}

GET /scim/v2/Groups — list organizations

Supports filtering and pagination. Filterable fields: displayName, externalId.

GET /scim/v2/Groups?filter=displayName%20co%20%22Acme%22&startIndex=1&count=50

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.

{
"schemas": ["urn:ietf:params:scim:schemas:core:2.0:Group", "urn:candid:scim:1.0:Organization"],
"displayName": "Acme Cardiology",
"externalId": "acme-cardiology",
"urn:candid:scim:1.0:Organization": {
"parentId": "00000000-0000-0000-0000-000000000001"
}
}

PATCH /scim/v2/Groups/:id — partial update

Allowed paths:

  • displayName
  • externalId

remove is only supported for externalId.

{
"schemas": ["urn:ietf:params:scim:api:messages:2.0:PatchOp"],
"Operations": [{ "op": "replace", "path": "displayName", "value": "Acme Cardiology" }]
}

Operations on members are explicitly rejected — group membership is managed via User role assignments:

{
"schemas": ["urn:ietf:params:scim:api:messages:2.0:Error"],
"detail": "Member operations are not supported. Assign users to organizations via the User roles attribute.",
"status": "400",
"scimType": "invalidValue"
}

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:

{
"urn:candid:scim:1.0:User": {
"organizationRoles": [
{
"organizationId": "11111111-1111-1111-1111-111111111111",
"roles": ["Biller"]
},
{
"organizationId": "22222222-2222-2222-2222-222222222222",
"roles": ["Viewer"]
}
]
}
}

Legacy format

Use the standard SCIM roles attribute with four entry types:

{
"roles": [
{ "type": "PRIMARY_ORG", "value": "11111111-1111-1111-1111-111111111111" },
{
"type": "ADDITIONAL_ORGS",
"value": "22222222-2222-2222-2222-222222222222,33333333-3333-3333-3333-333333333333"
},
{ "type": "ROLE", "value": "Biller" },
{ "type": "ROLE", "value": "Viewer" },
{ "type": "ORGANIZATION_ADMIN", "value": "true" }
]
}
  • PRIMARY_ORG and ADDITIONAL_ORGS enumerate the organization IDs the user belongs to. They are treated identically — both contribute organization memberships. Multiple PRIMARY_ORG entries are allowed.
  • ⚠️ ADDITIONAL_ORGS.value is a comma-separated list of UUIDs in a single string, not a JSON array. This is the convention from our legacy provisioning system.
  • ROLE entries are role names. Multiple ROLE entries 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.value is a string ("true"/"false"), not a boolean. Anything other than the literal string "true" is treated as false.

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:

{
"schemas": ["urn:ietf:params:scim:api:messages:2.0:Error"],
"detail": "Cannot mix legacy roles (PRIMARY_ORG/ADDITIONAL_ORGS/ROLE) with modern organizationRoles. Use one format or the other.",
"status": "400",
"scimType": "invalidValue"
}

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.

Role nameNotes
Organization AdminCustomer organization administrator. Typical choice for IT admins who manage other users.
BillerCustomer-side biller.
EditorEditor-level access.
ViewerRead-only access.
Customer (API)Programmatic / machine-to-machine integration role.
Front OfficeFront-office staff.
Front Office AdminFront-office administrator.
AnalyticsAnalytics and reporting access.
DocsDocumentation access.
Remits AppFeature-flag role. Layer on top of a base role to grant access to the Remits application.
Configurable Rules V2Feature-flag role. Layer on top of a base role to grant access to Configurable Rules V2.

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 in canonicalValues. New integrations should not rely on it; use Organization Admin instead.

PUT vs. PATCH semantics

  • POST /Users adds the user to all listed organizations with the listed roles.
  • PUT /Users/:id and PATCH /Users/:id with path: "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:

{
"schemas": ["urn:ietf:params:scim:api:messages:2.0:PatchOp"],
"Operations": [
{ "op": "<add|remove|replace>", "path": "<attribute>", "value": <any> }
]
}

Notes:

  • Operations (note the capital O) must contain at least one entry.
  • op is case-insensitive (add, Add, ADD all accepted).
  • A path is required on every operation; path-less PATCH is not supported.
  • Unknown paths are rejected with 400.
  • remove is only meaningful for a small subset of paths (currently externalId on Group only).
  • See the per-resource sections for the list of allowed paths.

Filtering and pagination

List endpoints (GET /Users, GET /Groups) accept:

ParameterDefaultNotes
filternoneSee below.
startIndex11-based per SCIM spec.
count100Page size. Maximum 100.

Filter syntax

Only single-clause filters are supported. The grammar is:

<field> (eq|co) "<value>"
  • 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:

GET /scim/v2/Users?filter=externalId%20eq%20%22auth0%7C65f0c1234567890abcdef111%22
GET /scim/v2/Groups?filter=displayName%20co%20%22Acme%22

Unsupported syntax (logical operators and/or, parenthesized expressions, other operators like sw/ew/gt) returns 400 invalidFilter.

Response format

{
"schemas": ["urn:ietf:params:scim:api:messages:2.0:ListResponse"],
"totalResults": 137,
"startIndex": 1,
"itemsPerPage": 50,
"Resources": [
/* ... */
]
}

Errors

All errors follow RFC 7644 §3.12:

{
"schemas": ["urn:ietf:params:scim:api:messages:2.0:Error"],
"detail": "<human-readable description>",
"status": "<HTTP status as string>",
"scimType": "<SCIM error subtype, when applicable>"
}

The Content-Type of error responses is always application/scim+json.

HTTP statusscimTypeWhen
400invalidValueRequest body or an attribute value failed validation.
400invalidFilterFilter syntax is unsupported or malformed.
401Missing, invalid, or expired bearer token.
403Resource is outside the token’s organization scope.
404Resource does not exist (or has been deleted).
409uniquenessEmail (User) or externalId (Group) already in use.
500Unhandled server error.
501notImplementedOperation is not yet supported — see Future features.

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.:

{
"Operations": [{ "op": "replace", "value": { "name": { "givenName": "Alex" } } }]
}

This is rejected with 400:

Patch operations without a path are not supported. Use explicit paths (e.g., "active", "userName").

You must rewrite these to use explicit paths, one operation per attribute:

{
"Operations": [{ "op": "replace", "path": "name.givenName", "value": "Alex" }]
}

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:

Member operations are not supported. Assign users to organizations via the User roles attribute.

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:

VerbIdempotent?Notes
GETyesSafe to retry indefinitely.
POST /UsersnoA successful create whose response is lost will cause the retry to fail with 409 uniqueness on email. Handle 409 by issuing GET /Users?filter=userName eq "..." to recover the existing resource.
POST /GroupsnoSame pattern: retry on lost response → 409 on externalId (when set). If externalId is not set, the retry will create a duplicate organization with a new UUID. Always set externalId on POST /Groups if you intend to be retry-safe.
PUTyesFull replacement is naturally idempotent.
PATCHdependsreplace is idempotent; repeated add of the same value is a no-op; remove of an already-cleared value is a no-op.
DELETEeffectively yesA retry returns 404 (the user/group no longer exists), which your client should treat as success-equivalent.

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.