Skip to content

SCIM 2.0

SCIM keeps the tenant’s user roster in sync with the customer’s directory, so joiners and leavers arrive without a support ticket. Provisioning is inbound: their directory is the source of truth.

Mint a token, scoped to a connection so that revoking directory sync does not disturb the customer’s SSO:

POST /v1/organizations/org_01J…/directory/scim_token
Authorization: Bearer sk_live_…
{ "connection_id": "conn_…" }
{
"token": "scim_…",
"scim_base_url": "https://acme.oauth.work/scim/v2"
}

Both values go into the customer’s provisioning configuration. The token is shown once, and authorizes the SCIM endpoints only — it cannot call /v1.

The customer’s IT admin can mint this themselves through an Admin Portal link with intent: "dsync".

GET /scim/v2/Users HTTP/1.1
Host: acme.oauth.work
Authorization: Bearer scim_…

A connection-scoped token may only mutate resources belonging to its own connection. Two directories syncing into one organization cannot overwrite each other’s users or groups.

GET /scim/v2/ServiceProviderConfig
{
"patch": { "supported": true },
"bulk": { "supported": false },
"filter": { "supported": true, "maxResults": 200 },
"changePassword": { "supported": false },
"sort": { "supported": false },
"etag": { "supported": false },
"authenticationSchemes": [{ "type": "oauthbearertoken", "name": "OAuth Bearer Token" }]
}
ResourceOperations
/scim/v2/UsersGET (list, filter), POST, GET/PUT/PATCH/DELETE by id
/scim/v2/GroupsGET (list, filter), POST, GET/PUT/PATCH/DELETE by id

A supported filter is one or more attribute eq value terms joined by and:

GET /scim/v2/Users?filter=userName eq "ada@acme.com"
GET /scim/v2/Users?filter=externalId eq "ext-42" and active eq true
GET /scim/v2/Groups?filter=displayName eq "Engineering"
ResourceFilterable attributes
UsersuserName, externalId, id, active
GroupsdisplayName, externalId, id

String values are quoted; active takes the bare booleans true or false. Attribute names and the eq operator are case-insensitive.

Anything outside that grammar — or, ne, co, sw, pr, grouping, negation, or an attribute not in the table — is rejected, not ignored:

GET /scim/v2/Users?filter=emails.value eq "ada@acme.com"
{
"schemas": ["urn:ietf:params:scim:api:messages:2.0:Error"],
"status": "400",
"scimType": "invalidFilter",
"detail": "filtering on 'emails.value' is not supported (supported: userName, externalId, id, active)"
}

That is deliberate. A filter the server cannot fully evaluate is refused rather than partially applied, because answering a narrower question than the one you asked is indistinguishable from success — a reconciliation sweep would quietly draw the wrong conclusion about who exists and who is deactivated. Branch on scimType (RFC 7644 §3.12): invalidFilter means the query was rejected, not that the resource is missing.

Results are capped at 200, matching the maxResults in the configuration document. Bulk operations, sorting, and ETags are not supported, as that document also states.

Setting active: false — usually through a PATCH when someone leaves — deactivates the user, and their sessions are killed on the next request rather than at token expiry. That is the property that matters on a termination: revocation is immediate, not eventual.

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

DELETE is also supported. Prefer deactivation: it preserves the audit trail and the user’s history, and it is what most directories send anyway.

Groups sync as groups — created, listed, patched for membership, and deleted.

SCIM provisioning, SAML SSO, and OIDC federation all converge on the same user records, keyed by external id and connection. A user who is synced by the directory and then signs in through SSO is one account, not two — which is what makes deprovisioning actually cut off access rather than leaving a second, SSO-created record behind.

scim_token_created, scim_user_provisioned, scim_user_deprovisioned, and scim_group_updated. Watching scim_user_deprovisioned is a straightforward way to confirm that offboarding is reaching you. See audit logs.

600 requests per minute, keyed on the token rather than the IP — so a large initial sync is not throttled by unrelated traffic from the same network. See errors and rate limits.