# How SCIM 2.0 Works, Message by Message

Source: https://startwithidentity.com/guides/protocols/scim-message-flow/
Last updated: 2026-11-05
License: content by Start with Identity. Cite the source URL.

---

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](https://www.rfc-editor.org/rfc/rfc7643) (schema) or [RFC 7644](https://www.rfc-editor.org/rfc/rfc7644) (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](https://startwithidentity.com/standards/scim-2-0/). Both RFCs were updated by [RFC 9865](https://www.rfc-editor.org/rfc/rfc9865) (cursor-based pagination, October 2025) and [RFC 9967](https://www.rfc-editor.org/rfc/rfc9967) (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](https://www.rfc-editor.org/rfc/rfc7644#section-4), [RFC 7643 §5](https://www.rfc-editor.org/rfc/rfc7643#section-5) |
| 1 | Before create | `GET /Users?filter=userName eq "..."` | [RFC 7644 §3.4.2.2](https://www.rfc-editor.org/rfc/rfc7644#section-3.4.2.2) |
| 2 | Joiner | `POST /Users` | [RFC 7644 §3.3](https://www.rfc-editor.org/rfc/rfc7644#section-3.3) |
| 3 | Mover | `PATCH /Users/{id}` | [RFC 7644 §3.5.2](https://www.rfc-editor.org/rfc/rfc7644#section-3.5.2) |
| 4 | Access change | `PATCH /Groups/{id}` add or remove `members` | [RFC 7644 §3.5.2.1–3.5.2.2](https://www.rfc-editor.org/rfc/rfc7644#section-3.5.2.2) |
| 5 | Leaver | `PATCH active=false`, later `DELETE /Users/{id}` | [RFC 7643 §4.1.1](https://www.rfc-editor.org/rfc/rfc7643#section-4.1.1), [RFC 7644 §3.6](https://www.rfc-editor.org/rfc/rfc7644#section-3.6) |

Every request carries a bearer token or other credential agreed out of band; RFC 7644 [§2](https://www.rfc-editor.org/rfc/rfc7644#section-2) leaves the mechanism open and requires TLS ([§7.2](https://www.rfc-editor.org/rfc/rfc7644#section-7.2)).

## Step 0: discovery

```http
GET /scim/v2/ServiceProviderConfig HTTP/1.1
Host: app.example.com
Authorization: Bearer scim-token-example-not-real
```

```json
{
  "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](https://www.rfc-editor.org/rfc/rfc7643#section-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

```http
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
```

```json
{
  "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](https://www.rfc-editor.org/rfc/rfc7644#section-3.4.2.2) |
| `attributes=` | Return only these attributes. | [RFC 7644 §3.4.2.5](https://www.rfc-editor.org/rfc/rfc7644#section-3.4.2.5) |
| `ListResponse`, `totalResults`, `startIndex` | Paged result envelope; index-based paging, or cursors under RFC 9865. | [RFC 7644 §3.4.2.4](https://www.rfc-editor.org/rfc/rfc7644#section-3.4.2.4) |

`userName` is case-insensitive and unique across the service provider ([RFC 7643 §4.1.1](https://www.rfc-editor.org/rfc/rfc7643#section-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](https://www.rfc-editor.org/rfc/rfc7644#section-7.5.2) suggests `POST /Users/.search` ([§3.4.3](https://www.rfc-editor.org/rfc/rfc7644#section-3.4.3)) for that reason.

## Step 2: create the user

```http
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
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](https://www.rfc-editor.org/rfc/rfc7643#section-3) |
| `id` | Assigned by the service provider; immutable; used in every later URL. Never sent by the client on create. | [RFC 7643 §3.1](https://www.rfc-editor.org/rfc/rfc7643#section-3.1) |
| `externalId` | The client's own identifier for this person. The join key the IdP should store. | [RFC 7643 §3.1](https://www.rfc-editor.org/rfc/rfc7643#section-3.1) |
| `userName` | Required, unique, case-insensitive. Collisions return 409 with `scimType: uniqueness`. | [RFC 7643 §4.1.1](https://www.rfc-editor.org/rfc/rfc7643#section-4.1.1), [RFC 7644 §3.3](https://www.rfc-editor.org/rfc/rfc7644#section-3.3) |
| `name`, `emails` | Complex and multi-valued attributes; `primary` marks one value. | [RFC 7643 §4.1.1–4.1.2](https://www.rfc-editor.org/rfc/rfc7643#section-4.1.2), [§2.4](https://www.rfc-editor.org/rfc/rfc7643#section-2.4) |
| `active` | Administrative status. `false` typically means suspended; exact meaning is up to the service provider. | [RFC 7643 §4.1.1](https://www.rfc-editor.org/rfc/rfc7643#section-4.1.1) |
| Enterprise extension | `employeeNumber`, `department`, `manager` and others, namespaced by URN. | [RFC 7643 §4.3](https://www.rfc-editor.org/rfc/rfc7643#section-4.3) |
| `meta` | `resourceType`, `created`, `lastModified`, `location`, `version`. Read-only. | [RFC 7643 §3.1](https://www.rfc-editor.org/rfc/rfc7643#section-3.1) |
| `ETag` / `meta.version` | Resource version for optimistic concurrency with `If-Match`. | [RFC 7644 §3.14](https://www.rfc-editor.org/rfc/rfc7644#section-3.14) |

The `id` values are the illustrative ones used throughout RFC 7644.

## Step 3: change attributes with PATCH

```http
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](https://www.rfc-editor.org/rfc/rfc7644#section-3.5.2)). `path` follows the attribute notation of [§3.10](https://www.rfc-editor.org/rfc/rfc7644#section-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](https://www.rfc-editor.org/rfc/rfc7644#section-3.5.1)), which replaces the whole resource and is easy to get wrong.

## Step 4: group membership

```json
{
  "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](https://www.rfc-editor.org/rfc/rfc7644#section-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](https://www.rfc-editor.org/rfc/rfc7644#section-3.5.2.2)). That one paragraph of the RFC is where most deprovisioning bugs live.

## Step 5: deactivate, then delete

```json
{ "schemas": ["urn:ietf:params:scim:api:messages:2.0:PatchOp"],
  "Operations": [{ "op": "replace", "path": "active", "value": false }] }
```

```http
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](https://www.rfc-editor.org/rfc/rfc7644#section-3.6)). Whether the application also kills active sessions and API tokens is outside SCIM; ask.

### Errors

```json
{
  "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](https://www.rfc-editor.org/rfc/rfc7644#section-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](https://www.rfc-editor.org/rfc/rfc7644#section-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](https://www.rfc-editor.org/rfc/rfc7643#section-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)**

7. **Read `ServiceProviderConfig` first** and adapt to what is supported (RFC 7644 §4).
8. **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](https://startwithidentity.com/techniques/mover-entitlement-accumulation/) |
| Deactivation stops at `active=false` in the app, sessions and API keys survive | A leaver keeps working access | [orphaned account abuse](https://startwithidentity.com/techniques/orphaned-account-abuse/) |
| SCIM token shared across tenants or never rotated | One leaked token rewrites every customer's directory | [secrets in repositories and CI](https://startwithidentity.com/techniques/secrets-in-repositories-and-ci/), [static API key abuse](https://startwithidentity.com/techniques/static-api-key-abuse/) |
| Connector code with weak input handling | The provisioning plane itself becomes the target | [CVE-2024-3318](https://startwithidentity.com/cves/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](https://startwithidentity.com/tools/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](https://startwithidentity.com/recipes/set-up-scim-provisioning/) walks through a full connector.

## Related

[SCIM 2.0 standard page](https://startwithidentity.com/standards/scim-2-0/), [what is SCIM](https://startwithidentity.com/guides/fundamentals/what-is-scim/), [SCIM provisioning implementation guide](https://startwithidentity.com/guides/implementation/scim-provisioning-implementation-guide/), [user lifecycle management](https://startwithidentity.com/guides/implementation/user-lifecycle-management-guide/), [SAML message flow](https://startwithidentity.com/guides/protocols/saml-message-flow/). Glossary: [SCIM](https://startwithidentity.com/glossary/scim/), [provisioning](https://startwithidentity.com/glossary/provisioning/), [deprovisioning](https://startwithidentity.com/glossary/deprovisioning/), [joiner-mover-leaver](https://startwithidentity.com/glossary/joiner-mover-leaver/), [security event token](https://startwithidentity.com/glossary/security-event-token/).
