SenseCrypt Docs
Concepts

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:

SCIM POST /Users user's first interactive sign-in SCIM PATCH active:false / DELETE Your IdP directoryOkta / Entra / Ping Pending shelluser record, no biometrics yet Device enrollmentface proof + hardware key, on the phone Active user Eager revocationfails closed on next token / refresh

Base URL and authentication

The SCIM base path is served on the tenant's issuer host:

{issuer}/v1/idp/scim/v2

Authenticate 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. changePassword is false — SenseCrypt is passwordless, so there is nothing to change (and there is no /Me endpoint).
  • 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:

EndpointMethodsAPI reference
/UsersGET (list/filter), POST (create)list, create
/Users/.searchPOSTsearch
/Users/{id}GET, PUT, PATCH, DELETEget
/BulkPOSTbulk
  • Creating a user requires a userName and 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.version is 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), logical and/or/not, parenthesised grouping, and emails[...] value paths (e.g. emails[type eq "work"].value). Resolvable paths include userName, externalId, emails, active, and the SCIM core attributes backed by the tenant's schema. An unknown attribute path or a malformed filter returns 400 with scimType: "invalidFilter"never a silent full-directory listing.
  • Attribute mapping: SCIM fields whose key matches one of the tenant's attribute definitions are stored (with administered write authority); the name cluster (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 a path. Unknown paths are ignored (forward-compatible per SCIM).

Groups

EndpointMethodsAPI reference
/GroupsGET (filter), POSTlist, create
/Groups/.searchPOSTsearch
/Groups/{id}GET, PUT, PATCH, DELETEget

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/remove on members), including the filtered form members[value eq "<id>"]. A target-less remove is 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, and meta.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 Group PATCH/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: false is 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.

On this page