Guides

SCIM provisioning

AdminUpdated Sep 19, 2026

SCIM provisioning

Auto-create, update, and de-provision Chatly seats from your IdP via SCIM v2.

SCIM 2.0 is how identity providers automate the seat lifecycle. New hire → Chatly seat appears on the next push. Employee departs → membership deactivated and every session revoked.

Chatly implements the Users half of SCIM v2, plus the three discovery documents a connector reads at setup. It is built against the shapes Okta, Microsoft Entra, and OneLogin actually send.

Info — SSO + SCIM solve different problems

SSO is how users sign in (authentication). SCIM is who can sign in (provisioning). You almost always want both — SSO without SCIM means admins manually invite every new hire. See SSO setup first.

Warning — Groups are not implemented

There is no /Groups resource. GET /ResourceTypes advertises User and nothing else, and a PATCH whose path is groups is refused with invalidPath. If your connector is configured to push group memberships, turn that off — it will error rather than no-op.

Enable SCIM

  1. Workspace → SSO → SCIM → Generate token.

  2. Copy the token immediately — it is hashed server-side and never shown again.

  3. In your IdP, add a SCIM connection with:

Parameters

Name

Type

Description

SCIM v2 endpoint URL (required)

url

{API_PUBLIC_URL}/v1/scim/v2. Read the exact value from GET /v1/sso/setup (scim.baseUrl) or the copy button in the dashboard.

Unique identifier field (required)

enum

userName, which must be an email address — the schema validates it as one.

Authentication (required)

enum

HTTP Header — Authorization: Bearer {token}. The SCIM token is the only credential these routes accept; a workspace API key (ck_…) and a dashboard bearer token are both rejected.

Managing tokens

POST /v1/sso/scim/tokens — Bearer token

GET /v1/sso/scim/tokens — Bearer token

DELETE /v1/sso/scim/tokens/{id} — Bearer token

These three are dashboard-scoped (workspace:write), not SCIM-scoped. POST takes an optional description (1–200 chars) so an operator can tell tokens apart in the list, and returns the token exactly once. GET lists tokens without their hashes. DELETE revokes one — a revoked token is refused with the same error as an unknown one.

Supported endpoints

Implemented

Name

Type

Description

GET /Users

SCIM v2

List, with startIndex / count paging (count capped at 200) and a filter. Deactivated members are included, so re-hiring reactivates the original membership.

GET /Users/{id}

SCIM v2

Fetch one.

POST /Users

SCIM v2

Create. 201 on success; 409 uniqueness when a live membership already exists for that email or externalId.

PUT /Users/{id}

SCIM v2

Full replace — how connectors that do not implement PATCH deprovision.

PATCH /Users/{id}

SCIM v2

Sparse update. Toggling active is the operation that matters.

DELETE /Users/{id}

SCIM v2

Deactivate, do not erase — identical to active: false. Answers 204.

GET /ServiceProviderConfig

SCIM v2

Capability discovery — your connector calls this once at setup.

GET /ResourceTypes

SCIM v2

Lists User only.

GET /Schemas

SCIM v2

The User schema, trimmed to the attributes we actually store.

ServiceProviderConfig advertises exactly what is true: patch supported, filter supported with maxResults: 200, and bulk, sort, etag, changePassword all unsupported.

The id is a membership id

SCIM resource ids are membership ids, not user ids. A person can be in several workspaces; your IdP is provisioning into exactly one, and the membership is the row that gets created, deactivated, and reactivated.

Filters

Only two filter shapes are accepted, because they are the only two Okta, Entra, and OneLogin send:

userName eq "[email protected]"
externalId eq "00u1a2b3c4"

Anything else is refused with 400 invalidFilter. That refusal is deliberate: a filter we do not understand returning the whole workspace reads to the IdP as "none of these users are new".

Attribute mappings

Attributes we store

Name

Type

Description

userName (required)

string

Must be an email address. Stored lowercased on users.email and used as the identity.

name.givenName

string

Combined with familyName into the agent's display name.

name.familyName

string

See above.

name.formatted

string

Used ahead of the assembled given/family pair.

displayName

string

Preferred over everything else. Order is displayNamename.formattedgivenName + familyName → the email.

active

bool

true → membership live. false → soft-deleted and every session revoked. Accepted on create too, so a not-yet-started employee can be staged without consuming a seat.

externalId

string

Persisted on memberships.scim_external_id (max 255 chars) and matched ahead of email on create.

emails[]

array

Accepted by the schema for connector compatibility. The identity always comes from userName.

Attributes we accept and knowingly drop

Name

Type

Description

Ignored on PATCH

no-op

title, phoneNumbers, locale, timezone, preferredLanguage, userType, nickName, profileUrl, addresses, photos, roles, entitlements, ims, x509Certificates, password. A connector sending one of these alongside active should not have the whole sync fail over the part we do not keep.

Anything else

error

An unrecognised PATCH path is 400 invalidPath, not a silent drop — a silently dropped operation is how a deprovision goes missing.

Roles

Warning — SCIM always provisions the agent role

Every membership SCIM creates gets the role agent. There is no group mapping, no role attribute, and no Chatly SCIM schema extension. Change someone's role from the Team page after they are provisioned; a later SCIM push will not overwrite it.

If you need role-on-provision, the SSO connection's jitRole sets the role for users provisioned by an SSO login instead — see SSO setup. It is also a single role per connection, not a group mapping.

De-provisioning

When the IdP sends active: false (via PATCH, PUT, or DELETE), inside one transaction we:

  1. Set memberships.deleted_at = now() — the membership stops being live and stops consuming a seat.

  2. Revoke every unrevoked session row for that user in that workspace, so nothing can refresh into a new access token.

  3. Revoke the user's push registrations, so their phone stops receiving customer messages.

  4. Write an audit row (actor: SCIM).

An already-issued access token keeps working until it expires — JWT_ACCESS_TTL_SECONDS, 15 minutes by default. Nothing force-closes an open WebSocket.

Re-activation (active: true after a false) restores the same membership — conversations, macros, and report history stay attached — subject to the workspace's seat limit. The role is unchanged by the restore.

Name, email, and externalId changes are applied but deliberately not audited: they are the highest-volume thing SCIM does and none of them move access.

Push immediately, not on schedule

Most IdPs default to event-driven SCIM. If yours polls, the worst case from "user leaves" to "seat revoked" is one poll interval.

Warning — No polling on our side

Chatly is the SCIM server — we act only when your provider calls us. There is no pull, no reconciliation job, and no "re-sync" button in the dashboard. If a user is missing, the fix is a full sync from the IdP.

Token hygiene

The SCIM bearer token is functionally an admin credential — anyone with it can create or deactivate members of the workspace.

Danger — Treat SCIM tokens like AWS root keys

Store the token in your IdP's secret manager, not in code. Mint one per provider so you can revoke a single connector. If you suspect leakage, Revoke in the dashboard and mint a new one — a revoked token stops working immediately.

Troubleshooting

Info — 409 on POST /Users

A live membership already exists for that userName or externalId. That is the RFC 7644 §3.3 answer and it is what tells Okta and Entra to switch to an update — most connectors handle it automatically. A 201 here would make the provider keep two records for one person.

Info — 400 invalidPath on PATCH

Your connector sent an attribute path we do not store and have not listed as ignorable — groups is the usual one. Remove it from the connector's attribute mappings.

Info — 403 on every SCIM call

The Authorization header is missing, is not Bearer …, is duplicated (a repeated header arrives as an array and is treated as malformed), or names a token that was revoked. Revoked and unknown tokens return the same error on purpose.

Info — Errors read strangely in the IdP admin UI

They should not — SCIM responses use the RFC 7644 §3.12 error body, whose detail string is written for the person configuring the connector. If you are seeing the platform's own JSON error envelope, you are calling a non-SCIM route.

Was this page helpful?