SCIM 2.0
Provision users and groups into a SenseCrypt tenant from your identity provider using the SCIM 2.0 protocol, with pending-shell enrollment for biometrics, filtering, bulk, and eager revocation.
SenseCrypt exposes a SCIM 2.0 provisioning endpoint so your identity provider (Okta, Entra ID, Ping, and others) can create, update, deactivate, and delete users and groups in a tenant automatically. The directory your IdP manages becomes the source of truth; SenseCrypt keeps its user records in sync — and, uniquely, defers biometric enrollment to first use (see pending shells).
The provisioning-to-enrollment lifecycle:
Base URL and authentication
The SCIM base path is served on the tenant's issuer host:
{issuer}/v1/idp/scim/v2Authenticate every request with a bearer token. Two credential types work:
- An opaque SCIM token — a "Secret Token" style credential (prefixed
ssct_) minted in the admin console. This is the mode most external IdP SCIM connectors expect (Entra ID custom SCIM, Okta header auth). It grants exactly the SCIM capabilities. - An M2M access token whose role grants
scim:read/scim:write. See Machine-to-machine.
The token is tenant-scoped — it can only read and write within the tenant it was issued for, and fails closed against any other tenant.
Minting an ssct_ token
Create a token in the console (or via POST /v1/admin/scim-tokens). The plaintext is returned once and only its hash is stored — capture it immediately:
POST /v1/admin/scim-tokens
{ "label": "Okta production", "expires_at": null }
// → 201
{
"id": "…",
"label": "Okta production",
"token": "ssct_…", // shown once — store it now
"scim_base_url": "https://acme.example.com/v1/idp/scim/v2",
"expires_at": null, // null = non-expiring (Entra static-token model)
"created_at": "…",
"last_used_at": null
}Tokens can be listed and revoked but not updated. See the SCIM tokens API reference.
Resources and feature set
The three discovery endpoints (/ServiceProviderConfig, /ResourceTypes, /Schemas) are anonymous (they are static metadata); every data route requires the bearer token. The advertised ServiceProviderConfig is:
{
"schemas": ["urn:ietf:params:scim:schemas:core:2.0:ServiceProviderConfig"],
"patch": { "supported": true },
"bulk": { "supported": true, "maxOperations": 1000, "maxPayloadSize": 1048576 },
"filter": { "supported": true, "maxResults": 200 },
"sort": { "supported": true },
"etag": { "supported": true },
"changePassword": { "supported": false },
"authenticationSchemes": [
{ "type": "oauthbearertoken", "name": "OAuth Bearer Token", "description": "M2M client_credentials access token with scim:* capability." }
]
}- Users and Groups are the supported resource types.
changePasswordisfalse— SenseCrypt is passwordless, so there is nothing to change (and there is no/Meendpoint). - The enterprise-user extension is tolerated on input (inbound bodies allow extra fields) but its fields are not mapped and it is never emitted on output.
Users
Standard CRUD plus search and bulk:
| Endpoint | Methods | API reference |
|---|---|---|
/Users | GET (list/filter), POST (create) | list, create |
/Users/.search | POST | search |
/Users/{id} | GET, PUT, PATCH, DELETE | get |
/Bulk | POST | bulk |
- Creating a user requires a
userNameand a primary email. The new user is created as a pending shell (enrollment_status = "pending"). - A returned User resource looks like this:
{
"schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"],
"id": "3f2a…",
"userName": "ada@acme.com",
"active": true,
"externalId": "okta-00u…",
"displayName": "Ada Lovelace",
"name": { "formatted": "Ada Lovelace", "givenName": "Ada", "familyName": "Lovelace" },
"emails": [ { "value": "ada@acme.com", "primary": true, "type": "work" } ],
"meta": {
"resourceType": "User",
"created": "2026-01-01T00:00:00Z",
"lastModified": "2026-01-02T00:00:00Z",
"location": "https://acme.example.com/v1/idp/scim/v2/Users/3f2a…",
"version": "W/\"a1b2c3d4e5f6a7b8\""
}
}meta.versionis a weak ETag; bulk operations and conditional requests are ETag-guarded.- Filtering implements the full SCIM filter grammar: the complete operator set (
eq ne co sw ew gt ge lt le pr), logicaland/or/not, parenthesised grouping, andemails[...]value paths (e.g.emails[type eq "work"].value). Resolvable paths includeuserName,externalId,emails,active, and the SCIM core attributes backed by the tenant's schema. An unknown attribute path or a malformed filter returns400withscimType: "invalidFilter"— never a silent full-directory listing. - Attribute mapping: SCIM fields whose key matches one of the tenant's attribute definitions are stored (with
administeredwrite authority); thenamecluster (givenName/familyName/middleName) maps to the corresponding attributes. Unknown SCIM attributes are ignored — definitions are never auto-created. - PATCH supports
add/replace/remove(case-insensitive verbs, since Entra capitalizes them), with or without apath. Unknown paths are ignored (forward-compatible per SCIM).
Groups
| Endpoint | Methods | API reference |
|---|---|---|
/Groups | GET (filter), POST | list, create |
/Groups/.search | POST | search |
/Groups/{id} | GET, PUT, PATCH, DELETE | get |
A returned Group resource:
{
"schemas": ["urn:ietf:params:scim:schemas:core:2.0:Group"],
"id": "9c1d…",
"displayName": "Engineering",
"members": [ { "value": "3f2a…", "$ref": "https://acme.example.com/v1/idp/scim/v2/Users/3f2a…" } ],
"meta": { "resourceType": "Group", "location": "https://acme.example.com/v1/idp/scim/v2/Groups/9c1d…", "version": "W/\"…\"" }
}- Group membership is managed with SCIM PATCH (
add/replace/removeonmembers), including the filtered formmembers[value eq "<id>"]. A target-lessremoveis rejected (invalidValue), never treated as clear-all — this matches how Entra sends single-member removals. - Filtering uses the full grammar over
displayName,externalId,id,meta.created, andmeta.lastModified. - SCIM-managed groups are read-only in the admin console so your external IdP stays the source of truth, and they participate in access gating like any other group.
Pending shells and biometric enrollment
SCIM-provisioned users are created as pending shells: they have a profile and group memberships owned by your IdP, but no face binding yet. This is how SenseCrypt reconciles directory provisioning with an on-device biometric. A pending shell:
- cannot sign in — the token grant requires an enrolled user, so a shell fails the gate; and
- binds its biometric later, when the person first uses the Authenticator app. The app's onboarding claims the shell in place, preserving the SCIM-assigned
sub, the SCIM-set name/attributes, and the SCIM group memberships (the device cannot clobber IdP-owned values).
This keeps provisioning transparent: your IdP manages the account lifecycle, and the biometric is added by the user on first use — no separate enrollment step for you to orchestrate.
Lifecycle: suspend and delete
active: false(via PATCH/PUT) is a reversible suspend. It immediately calls the eager-revocation path: the user's device keys are revoked and their refresh-token families are killed. Re-activating does not restore credentials — the device must re-enroll.- DELETE is a permanent removal, with the same full credential revocation.
- Removing a user from a group (via SCIM
GroupPATCH/PUT, or the console) also eagerly revokes that user's device keys and refresh families — access is severed immediately, not left to token expiry. A user still in other groups must re-authenticate.
This eager-revocation behavior is why suspend/delete/group-removal take effect right away rather than at the next token's exp. See Tokens & sessions.
Errors
SCIM errors use the SCIM error envelope, with status as a string:
{
"schemas": ["urn:ietf:params:scim:api:messages:2.0:Error"],
"status": "409",
"scimType": "uniqueness",
"detail": "userName already exists"
}Common scimType values you'll encounter: uniqueness (409, duplicate userName/email/displayName), invalidFilter (400, bad filter), invalidValue (400, missing required field or an illegal removal), invalidSyntax (400, unsupported PATCH op), invalidPath / noTarget (400/…, bad value-path). A 404 is a plain not-found. Responses use the application/scim+json media type.
Gotchas
- Provisioning does not enroll biometrics. A freshly provisioned user is a pending shell and cannot sign in until they enroll on the phone. This is by design, not a failure.
active: falseis not reversible for credentials. Re-activating restores the account but the user must re-enroll their device.- Bad filters fail loudly. An unknown attribute path returns
invalidFilter— it never degrades to returning the whole directory. - Group removal is not a soft "leave". It eagerly revokes the user's device keys and refresh families across the tenant.
Set it up
Follow the SCIM provisioning guide for the base URL, minting a token, and connecting your IdP. Full REST detail is in the SCIM API reference.
SAML 2.0
SenseCrypt as a SAML 2.0 identity provider — metadata, bindings, NameID, request signatures, assertion signing and encryption, attribute release, and how the biometric ceremony fits behind SP-initiated SSO.
CIBA
Client-Initiated Backchannel Authentication — decoupled, no-browser sign-in where SenseCrypt emails the user a sign-in QR, they approve on their phone, and your backend collects the tokens.