How SCIM 2.0 Works, Message by Message
SCIM 2.0 is a REST API with a fixed JSON schema for users and groups. The identity provider is the client and pushes changes; the application is the service provider and stores them. A full lifecycle is about six request types: discovery, a filtered lookup, create, PATCH, group PATCH, and delete. This page shows each one with fake but realistic JSON, explains every field with its section in RFC 7643 (schema) or RFC 7644 (protocol), and lists the checks both sides should run.
For what SCIM is, why it matters and where implementations break at a high level, see the SCIM 2.0 standard page. Both RFCs were updated by RFC 9865 (cursor-based pagination, October 2025) and RFC 9967 (SCIM profile for security event tokens, May 2026); the core messages below are unchanged by them.
The sequence at a glance
| # | Event | Request | Spec |
|---|---|---|---|
| 0 | Connector set-up | GET /ServiceProviderConfig, /ResourceTypes, /Schemas |
RFC 7644 §4, RFC 7643 §5 |
| 1 | Before create | GET /Users?filter=userName eq "..." |
RFC 7644 §3.4.2.2 |
| 2 | Joiner | POST /Users |
RFC 7644 §3.3 |
| 3 | Mover | PATCH /Users/{id} |
RFC 7644 §3.5.2 |
| 4 | Access change | PATCH /Groups/{id} add or remove members |
RFC 7644 §3.5.2.1–3.5.2.2 |
| 5 | Leaver | PATCH active=false, later DELETE /Users/{id} |
RFC 7643 §4.1.1, RFC 7644 §3.6 |
Every request carries a bearer token or other credential agreed out of band; RFC 7644 §2 leaves the mechanism open and requires TLS (§7.2).
Step 0: discovery
GET /scim/v2/ServiceProviderConfig HTTP/1.1
Host: app.example.com
Authorization: Bearer scim-token-example-not-real
{
"schemas": ["urn:ietf:params:scim:schemas:core:2.0:ServiceProviderConfig"],
"patch": { "supported": true },
"bulk": { "supported": false, "maxOperations": 0, "maxPayloadSize": 0 },
"filter": { "supported": true, "maxResults": 200 },
"changePassword": { "supported": false },
"sort": { "supported": false },
"etag": { "supported": true },
"authenticationSchemes": [
{ "type": "oauthbearertoken", "name": "OAuth Bearer Token",
"description": "Authentication using an OAuth 2.0 bearer token" }
]
}
This tells the client whether PATCH, filtering and ETags exist before it relies on them (RFC 7643 §5). A connector that skips this step is the usual cause of "works in one app, fails in the next".
Step 1: look before you create
GET /scim/v2/Users?filter=userName%20eq%20%22ada%40example.com%22&attributes=id,userName,active HTTP/1.1
Host: app.example.com
Authorization: Bearer scim-token-example-not-real
{
"schemas": ["urn:ietf:params:scim:api:messages:2.0:ListResponse"],
"totalResults": 0,
"startIndex": 1,
"itemsPerPage": 0,
"Resources": []
}
| Element | Meaning | Spec |
|---|---|---|
filter=userName eq "..." |
Attribute, operator, quoted value. Operators include eq, ne, co, sw, pr, gt, and, or, not. |
RFC 7644 §3.4.2.2 |
attributes= |
Return only these attributes. | RFC 7644 §3.4.2.5 |
ListResponse, totalResults, startIndex |
Paged result envelope; index-based paging, or cursors under RFC 9865. | RFC 7644 §3.4.2.4 |
userName is case-insensitive and unique across the service provider (RFC 7643 §4.1.1), so the server must match ADA@example.com here too. Filters that contain personal data end up in access logs; RFC 7644 §7.5.2 suggests POST /Users/.search (§3.4.3) for that reason.
Step 2: create the user
POST /scim/v2/Users HTTP/1.1
Host: app.example.com
Authorization: Bearer scim-token-example-not-real
Content-Type: application/scim+json
{
"schemas": [
"urn:ietf:params:scim:schemas:core:2.0:User",
"urn:ietf:params:scim:schemas:extension:enterprise:2.0:User"
],
"externalId": "idp-obj-4f2a9c1e-example",
"userName": "ada@example.com",
"name": { "givenName": "Ada", "familyName": "Example" },
"emails": [{ "value": "ada@example.com", "type": "work", "primary": true }],
"active": true,
"urn:ietf:params:scim:schemas:extension:enterprise:2.0:User": {
"employeeNumber": "100234",
"department": "Finance",
"manager": { "value": "26118915-6090-4610-87e4-49d8ca9f808d" }
}
}
HTTP/1.1 201 Created
Location: https://app.example.com/scim/v2/Users/2819c223-7f76-453a-919d-413861904646
ETag: W/"e180ee84f0671b1"
{
"schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"],
"id": "2819c223-7f76-453a-919d-413861904646",
"externalId": "idp-obj-4f2a9c1e-example",
"userName": "ada@example.com",
"active": true,
"meta": {
"resourceType": "User",
"created": "2026-11-05T14:02:11Z",
"lastModified": "2026-11-05T14:02:11Z",
"location": "https://app.example.com/scim/v2/Users/2819c223-7f76-453a-919d-413861904646",
"version": "W/\"e180ee84f0671b1\""
}
}
| Field | Meaning | Spec |
|---|---|---|
schemas |
Which schemas the body uses; required on every resource. | RFC 7643 §3 |
id |
Assigned by the service provider; immutable; used in every later URL. Never sent by the client on create. | RFC 7643 §3.1 |
externalId |
The client's own identifier for this person. The join key the IdP should store. | RFC 7643 §3.1 |
userName |
Required, unique, case-insensitive. Collisions return 409 with scimType: uniqueness. |
RFC 7643 §4.1.1, RFC 7644 §3.3 |
name, emails |
Complex and multi-valued attributes; primary marks one value. |
RFC 7643 §4.1.1–4.1.2, §2.4 |
active |
Administrative status. false typically means suspended; exact meaning is up to the service provider. |
RFC 7643 §4.1.1 |
| Enterprise extension | employeeNumber, department, manager and others, namespaced by URN. |
RFC 7643 §4.3 |
meta |
resourceType, created, lastModified, location, version. Read-only. |
RFC 7643 §3.1 |
ETag / meta.version |
Resource version for optimistic concurrency with If-Match. |
RFC 7644 §3.14 |
The id values are the illustrative ones used throughout RFC 7644.
Step 3: change attributes with PATCH
PATCH /scim/v2/Users/2819c223-7f76-453a-919d-413861904646 HTTP/1.1
Host: app.example.com
Authorization: Bearer scim-token-example-not-real
Content-Type: application/scim+json
If-Match: W/"e180ee84f0671b1"
{
"schemas": ["urn:ietf:params:scim:api:messages:2.0:PatchOp"],
"Operations": [
{ "op": "replace",
"path": "urn:ietf:params:scim:schemas:extension:enterprise:2.0:User:department",
"value": "Treasury" },
{ "op": "replace", "path": "emails[type eq \"work\"].value", "value": "ada.example@example.com" }
]
}
PATCH operations are add, remove and replace, applied in order and atomically: if one fails, the server must restore the original resource (RFC 7644 §3.5.2). path follows the attribute notation of §3.10 and may include a value filter in brackets. A server that does not support PATCH says so in ServiceProviderConfig; the client then falls back to PUT (§3.5.1), which replaces the whole resource and is easy to get wrong.
Step 4: group membership
{
"schemas": ["urn:ietf:params:scim:api:messages:2.0:PatchOp"],
"Operations": [
{ "op": "add", "path": "members",
"value": [{ "value": "2819c223-7f76-453a-919d-413861904646" }] },
{ "op": "remove",
"path": "members[value eq \"902c246b-6245-4190-8e05-00816be7344a\"]" }
]
}
Sent as PATCH /scim/v2/Groups/e9e30dba-f08f-4109-8486-d5c6a331660a. add on a multi-valued attribute appends (§3.5.2.1). remove with a filter removes only the matching member; remove on members with no filter removes all members; a remove with no path at all must fail with noTarget (§3.5.2.2). That one paragraph of the RFC is where most deprovisioning bugs live.
Step 5: deactivate, then delete
{ "schemas": ["urn:ietf:params:scim:api:messages:2.0:PatchOp"],
"Operations": [{ "op": "replace", "path": "active", "value": false }] }
DELETE /scim/v2/Users/2819c223-7f76-453a-919d-413861904646 HTTP/1.1
Host: app.example.com
Authorization: Bearer scim-token-example-not-real
HTTP/1.1 204 No Content
After a DELETE the server may keep the record but must return 404 for it and omit it from queries, and should not count it in uniqueness checks (RFC 7644 §3.6). Whether the application also kills active sessions and API tokens is outside SCIM; ask.
Errors
{
"schemas": ["urn:ietf:params:scim:api:messages:2.0:Error"],
"status": "409",
"scimType": "uniqueness",
"detail": "userName already exists"
}
scimType values include invalidFilter, tooMany, uniqueness, mutability, invalidSyntax, invalidPath, noTarget, invalidValue, invalidVers and sensitive (RFC 7644 §3.12).
The checks each side should run
Service provider (the application)
- Authenticate every request and scope the token to SCIM for one tenant; RFC 7644 §6 leaves multi-tenancy to you, so map the credential to a tenant server-side, never from a request field.
- Enforce
userNameuniqueness case-insensitively and return 409uniquenesson conflict (RFC 7643 §4.1.1, RFC 7644 §3.3). - Respect mutability: reject writes to
id,metaand other read-only attributes withmutability(RFC 7643 §2.2, RFC 7644 §3.12). - Implement PATCH remove with filters exactly as §3.5.2.2 says, including the no-op success when the member is already absent.
- Honour
If-Matchwhen you advertise ETags, returning 412 (Precondition Failed) on a version mismatch (RFC 7644 §3.14, status table in §3.12). - Never return passwords, hashed or not (RFC 7643 §4.1.1,
password), and treatactive=falseas an immediate session and token revocation event.
Client (the identity provider or IGA connector)
- Read
ServiceProviderConfigfirst and adapt to what is supported (RFC 7644 §4). - Store
idagainst yourexternalId, look up by filter before creating, and escape double quotes and backslashes in filter values, which are JSON strings inside the filter grammar (§3.4.2.2); unescaped input is filter injection.
Common implementation mistakes
| Mistake | What goes wrong | Related |
|---|---|---|
| Group removals reported as success but not applied | Users keep access after they move or leave | mover entitlement accumulation |
Deactivation stops at active=false in the app, sessions and API keys survive |
A leaver keeps working access | orphaned account abuse |
| SCIM token shared across tenants or never rotated | One leaked token rewrites every customer's directory | secrets in repositories and CI, static API key abuse |
| Connector code with weak input handling | The provisioning plane itself becomes the target | CVE-2024-3318 (SailPoint connector path traversal) |
Joining on email or userName |
A rename creates a duplicate; a reused address inherits someone's account | Use externalId + id |
The SWI CVE catalog has only one SCIM-tagged entry, and its own note says the risk sat in IdP and IGA connectors rather than the protocol. In practice SCIM failures are silent logic errors, not exploits, which is why testing deprovisioning end to end matters more than anything in the table above.
Try it
Build and inspect each request on this page in the SCIM request builder: set the base URL, choose create, PATCH or group membership, and copy the generated JSON. Then run the one test that catches most real bugs: remove a user from a group through your IdP and read the group back with GET /Groups/{id}. The set up SCIM provisioning recipe walks through a full connector.
Related
SCIM 2.0 standard page, what is SCIM, SCIM provisioning implementation guide, user lifecycle management, SAML message flow. Glossary: SCIM, provisioning, deprovisioning, joiner-mover-leaver, security event token.
Frequently asked questions
- What requests does an identity provider send over SCIM?
- A typical provisioning connector reads GET /ServiceProviderConfig once, then for each user runs a filtered GET /Users?filter=userName eq "..." to check for an existing account, POST /Users to create it, PATCH /Users/{id} to change attributes or set active to false, PATCH /Groups/{id} to add and remove members, and sometimes DELETE /Users/{id}. All bodies are JSON with a schemas array, defined in RFC 7643 and RFC 7644.
- What is the difference between id and externalId in SCIM?
- id is assigned by the service provider (the application) when the resource is created and is the value used in the URL of every later request. externalId is set by the client (the identity provider) and holds the client's own identifier for the person, such as the IdP's object ID. Both are defined in RFC 7643 section 3.1. Store the mapping; never use the email address or userName as the join key, because they change.
- Does SCIM DELETE remove the user?
- RFC 7644 section 3.6 lets the service provider choose. It may keep the record, but it must return 404 for every later operation on that resource and must omit it from queries. Many identity providers send PATCH active=false instead of DELETE when a user is unassigned or disabled, and only delete later. Check what each application does, because it decides whether a license is freed and whether an auditor sees the account as gone.
- Why does SCIM group removal fail silently?
- Usually because the client and server disagree on PATCH semantics for multi-valued attributes. RFC 7644 section 3.5.2.2 defines removal of one member with a path filter such as members[value eq "id"]. Servers that do not support that filter, or treat a replace on members as additive, return success while the member stays. Test removal explicitly, then read the group back.
Related on Start with Identity
- GuideHow Kerberos Works, Message by Message
Kerberos V5 on the wire: AS-REQ, pre-authentication, AS-REP, TGS-REQ and TGS-REP, AP-REQ and AP-REP, with each ASN.1 field tied to its RFC 4120 section, the che
- GuideHow LDAP Works, Message by Message
LDAP on the wire: StartTLS, the service-account bind, the user search, the user bind and the result codes, with each LDAPMessage field tied to RFC 4511, RFC 451
- GuideHow OpenID Connect Works, Message by Message
OpenID Connect on the wire: discovery, the authentication request, the callback, the token request with PKCE, the token response and a decoded ID token, with ev
- RankingBest Enterprise SSO & SCIM Platforms for B2B SaaS: Top 5
The best enterprise SSO and SCIM platforms for B2B SaaS in 2026: WorkOS, SSOJet, Auth0, FusionAuth, and Keycloak, ranked for teams that already have core auth a
- ExpertEditor, SCIM Protocol (RFC 7644)
Phil Hunt is the editor of RFC 7644, the SCIM protocol, published in 2015 with co-authors from SailPoint, Cisco, Nexus, and Salesforce while he was at Oracle. H
- ArticleEnterprise Readiness for B2B SaaS: SSO, SCIM, and Audit Logs
The identity features that turn a B2B SaaS product into one enterprises will buy: SAML and OIDC single sign-on, SCIM provisioning, audit logs, role-based access