Skip to content
Start with Identity
Protocols · Intermediate

How SCIM 2.0 Works, Message by Message

By SWI Community Team · Updated 2026-11-05 · 13 min

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)

  1. 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.
  2. Enforce userName uniqueness case-insensitively and return 409 uniqueness on conflict (RFC 7643 §4.1.1, RFC 7644 §3.3).
  3. Respect mutability: reject writes to id, meta and other read-only attributes with mutability (RFC 7643 §2.2, RFC 7644 §3.12).
  4. Implement PATCH remove with filters exactly as §3.5.2.2 says, including the no-op success when the member is already absent.
  5. Honour If-Match when you advertise ETags, returning 412 (Precondition Failed) on a version mismatch (RFC 7644 §3.14, status table in §3.12).
  6. Never return passwords, hashed or not (RFC 7643 §4.1.1, password), and treat active=false as an immediate session and token revocation event.

Client (the identity provider or IGA connector)

  1. Read ServiceProviderConfig first and adapt to what is supported (RFC 7644 §4).
  2. Store id against your externalId, 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.

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.
Last reviewed By SWI Community TeamSuggest a correctionHow we research