SCIM (System for Cross-domain Identity Management)

SCIM is a REST API standard that lets an Identity Provider (IdP)—for example Okta, Entra ID (Azure AD), or OneLogin—automatically provision and sync user and group accounts in your application. Without SCIM, when a company with hundreds of employees adopts your SaaS product, an admin may need to create accounts manually. With SCIM, the IdP pushes accounts to your app and keeps them in sync.

Roles:

Role Who Responsibility
SCIM Service Provider Your application Exposes REST endpoints the IdP calls
SCIM Client The IdP Initiates all requests

Endpoints your app exposes

Typical base URL: https://yourapp.com/scim/v2.

Users

Path Methods
/Users GET, POST
/Users/{id} GET, PUT, PATCH, DELETE

Groups

Path Methods
/Groups GET, POST
/Groups/{id} GET, PUT, PATCH, DELETE

Discovery

These tell the IdP what your SCIM implementation supports:

  • /ServiceProviderConfig
  • /ResourceTypes
  • /Schemas

Authenticating SCIM requests

The IdP must authenticate when calling your endpoints.

  • Bearer token (most common): Your app issues a long-lived API token; the admin configures it in the IdP. Every request includes Authorization: Bearer <token>.
  • OAuth 2.0: Some deployments use OAuth instead of a static bearer token.

User lifecycle

1. Check if the user exists

When an admin assigns a user to your app in the IdP, the IdP often issues a filtered GET first:

GET /scim/v2/Users?filter=userName eq "john@company.com"

If the user does not exist, return an empty list:

{
  "schemas": ["urn:ietf:params:scim:api:messages:2.0:ListResponse"],
  "totalResults": 0,
  "Resources": []
}

2. Create the user

The IdP POSTs to /Users:

POST /scim/v2/Users
Content-Type: application/json
{
  "schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"],
  "userName": "john@company.com",
  "name": {
    "givenName": "John",
    "familyName": "Doe"
  },
  "emails": [
    {
      "primary": true,
      "value": "john@company.com",
      "type": "work"
    }
  ],
  "displayName": "John Doe",
  "active": true
}

Bulk onboarding is implemented as one HTTP request per user (each user is a separate POST). There is no standard batch “create a million users in one call” operation in core SCIM—large directories imply many individual creates (often paced by the IdP).

3. Respond after create

Create the user in your database and respond with 201 Created, returning the full user resource including the id your system generated:

{
  "schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"],
  "id": "abc-123-def",
  "userName": "john@company.com",
  "name": {
    "givenName": "John",
    "familyName": "Doe"
  },
  "emails": [
    {
      "primary": true,
      "value": "john@company.com",
      "type": "work"
    }
  ],
  "displayName": "John Doe",
  "active": true,
  "meta": {
    "resourceType": "User",
    "created": "2026-05-10T10:00:00Z",
    "lastModified": "2026-05-10T10:00:00Z",
    "location": "https://yourapp.com/scim/v2/Users/abc-123-def"
  }
}

The id is critical: the IdP stores it and uses it for later updates. It must be stable, unique, and immutable in your system.


Updating a user

Example: last name changes from “Doe” to “Smith”. The IdP may send PATCH (partial) or PUT (full replacement), depending on product and configuration.

PATCH /scim/v2/Users/abc-123-def
Content-Type: application/json
{
  "schemas": ["urn:ietf:params:scim:api:messages:2.0:PatchOp"],
  "Operations": [
    {
      "op": "replace",
      "path": "name.familyName",
      "value": "Smith"
    }
  ]
}

Respond with 200 OK and the updated user resource where applicable.

Interop note: Okta often favors PATCH; Entra ID may use PUT or PATCH. Supporting both PATCH and PUT improves compatibility.


Deactivating (de-provisioning) a user

Many IdPs do not send DELETE when someone leaves or is unassigned. They PATCH active to false:

PATCH /scim/v2/Users/abc-123-def
Content-Type: application/json
{
  "schemas": ["urn:ietf:params:scim:api:messages:2.0:PatchOp"],
  "Operations": [
    {
      "op": "replace",
      "path": "active",
      "value": false
    }
  ]
}

Recommended: Soft-disable the user—revoke sessions, block login, retain data. Some IdPs later set active back to true if the user is reassigned.

Hard delete: Less common; when supported, the IdP calls DELETE /scim/v2/Users/{id} and you return 204 No Content.


Groups

Creating a group

Groups often map to roles, teams, or permission sets.

POST /scim/v2/Groups
Content-Type: application/json
{
  "schemas": ["urn:ietf:params:scim:schemas:core:2.0:Group"],
  "displayName": "Engineering",
  "members": [
    { "value": "abc-123-def", "display": "John Doe" },
    { "value": "xyz-789-ghi", "display": "Jane Roe" }
  ]
}

members[].value uses the id values your app returned when those users were provisioned. Respond with 201 Created and your generated group id.

Updating group membership

Membership changes usually arrive as PATCH. Example—add a member:

PATCH /scim/v2/Groups/grp-456
Content-Type: application/json
{
  "schemas": ["urn:ietf:params:scim:api:messages:2.0:PatchOp"],
  "Operations": [
    {
      "op": "add",
      "path": "members",
      "value": [{ "value": "new-user-id-here" }]
    }
  ]
}

Example—remove a member:

{
  "schemas": ["urn:ietf:params:scim:api:messages:2.0:PatchOp"],
  "Operations": [
    {
      "op": "remove",
      "path": "members[value eq \"abc-123-def\"]"
    }
  ]
}

Filter syntax on path varies slightly between IdPs—this is a common source of integration friction.


Filters your implementation should support

IdPs use filter query parameters for lookups. Minimum useful support:

Example filter Typical use
userName eq "john@company.com" Existence check before create
displayName eq "Engineering" Group lookup
externalId eq "<idp-user-id>" Some IdPs prefer this over userName

The eq operator is the most important. Supporting and, or, contains, startsWith, etc., improves interoperability but is not always required.


externalId

Some IdPs send externalId—the user’s identifier in the IdP. Store it next to your own id for reconciliation. Lookups may filter by externalId instead of userName, so supporting both matters.


Pagination

For GET /scim/v2/Users (initial sync, reconciliation), large directories require pagination:

GET /scim/v2/Users?startIndex=1&count=100

Include totalResults, startIndex, itemsPerPage, and the Resources array. The IdP pages until it has retrieved everything.


Error responses

Return consistent HTTP status codes:

Status Meaning
400 Malformed request
401 Invalid or missing auth
404 User or group id not found
409 Conflict (e.g., duplicate create)

Body should follow the SCIM error schema:

{
  "schemas": ["urn:ietf:params:scim:api:messages:2.0:Error"],
  "detail": "User already exists",
  "status": "409"
}

Practical implementation tips

  1. IdP variance: Despite the standard, Okta, Entra ID, OneLogin, etc., differ in PATCH vs PUT, group paths, and retries. Test each IdP you support.

  2. Idempotency: Retries are normal. A duplicate POST for the same logical user should not create two accounts—detect duplicates and return the existing resource where appropriate.

  3. Initial sync load: First connection may create hundreds or thousands of users quickly—rate limits, DB efficiency, and queueing matter.

  4. Libraries: Prefer a maintained SCIM library when possible (e.g. Java scim2, Python django-scim2, Ruby scim-kit) for parsing, filtering, and schema validation.

  5. Discovery endpoints: /ServiceProviderConfig, /Schemas, and /ResourceTypes are sometimes queried during setup—return accurate capability declarations (supported operations, filters).


Summary

Implementing SCIM means exposing a REST API the IdP calls to push user and group lifecycle into your app. Your service is passive: it receives requests. Typical flow: check existence → create → patch updates → deactivate via active, with groups and membership carried by PATCH operations.