In this article
September 9, 2025
September 9, 2025

Implementing a generic SCIM client: A practical guide

If you’ve built a custom Identity Provider, you’ll need to implement SCIM client functionality yourself. This guide shows you how to build a standards-compliant SCIM 2.0 client that can provision users and groups using WorkOS as the SCIM service provider.

User provisioning is one of the core building blocks of enterprise-ready applications. When companies adopt your product, IT admins want a seamless way to manage user accounts and group memberships directly from their Identity Provider (IdP), rather than manually handling them. This is where SCIM (System for Cross-domain Identity Management) comes in.

SCIM standardizes how IdPs and applications exchange user and group data. Most enterprise IdPs (Okta, Azure AD/Entra ID, OneLogin, Ping, etc.) already ship with SCIM clients built in. But if you’re building your own homegrown IdP or custom provisioning system, you’ll need to implement a SCIM client yourself.

In this guide, you’ll learn how to build a generic SCIM client that can integrate with WorkOS as the SCIM service provider, including authentication, supported endpoints, and example requests.

What is SCIM

SCIM (System for Cross-domain Identity Management) is an IETF standard for provisioning and managing users and groups over HTTP using predictable REST endpoints and JSON schemas. The protocol is defined in RFC 7644 and the core resource schema in RFC 7643.

In a SCIM integration:

  • The Identity Provider (IdP) acts as the SCIM client, sending create, update, and delete requests.
  • WorkOS acts as the SCIM service provider, receiving those requests and applying them to the application you’re provisioning into.

Why would you need to build a SCIM client

Most enterprise IdPs like Okta, Azure AD (Entra ID), OneLogin, and Ping ship with a SCIM client built in. In those cases, integrating with WorkOS is straightforward—you just plug in the Base URL and Bearer Token.

But if you’ve built a homegrown IdP or custom provisioning system, you don’t get that out-of-the-box functionality. Instead, you need to implement the SCIM client yourself: making authenticated HTTP requests, formatting payloads correctly, and handling responses from the WorkOS SCIM server.

This guide is designed specifically for that scenario. We’ll walk step-by-step through:

  • Authenticating requests against the WorkOS SCIM service.
  • Understanding the key SCIM endpoints you’ll need.
  • Crafting user and group provisioning calls (create, update, deactivate, delete).
  • Handling edge cases like idempotency, pagination, and error responses.

By the end, you’ll have a working blueprint for a generic SCIM client that can sync your homegrown IdP with WorkOS.

How this works

When a user or group is created, updated, or deactivated in your IdP, you send a SCIM request to the WorkOS SCIM Base URL.

  • Base URL: Available from the WorkOS Dashboard (provided by the application admin) or through the Admin Portal Workflow (a link generated by the application admin).
  • Bearer Token: Also provided by the admin. This token is tied to a specific directory connection in WorkOS.

Authentication

Every request to the WorkOS SCIM server must include:

	
Authorization: Bearer {YOUR_DIRECTORY_BEARER_TOKEN}
Accept: application/scim+json
Content-Type: application/scim+json
	

Without this token, you’ll get a 401 Unauthorized response.

Endpoints you’ll use

These paths are relative to the Base URL you’ve been given. WorkOS implements SCIM 2.0 (RFC 7644) conventions.

Discovery

  • GET /ServiceProviderConfig – See supported features (PATCH, filter, bulk, etc.).
  • GET /Schemas – Lists schemas supported by WorkOS.
  • GET /ResourceTypes – Lists available resource types (Users, Groups).

Users

  • GET /Users – List users (supports startIndex, count, filter).
  • POST /Users – Create a new user.
  • GET /Users/{id} – Retrieve a user by ID.
  • PUT /Users/{id} – Replace a user’s attributes completely.
  • PATCH /Users/{id} – Update specific attributes.
  • DELETE /Users/{id} – Remove or deactivate a user (often done by setting active to false instead).

Groups

  • GET /Groups – List groups.
  • POST /Groups – Create a group.
  • GET /Groups/{id} – Retrieve a group.
  • PUT /Groups/{id} – Replace a group.
  • PATCH /Groups/{id} – Modify group membership or attributes.
  • DELETE /Groups/{id} – Remove a group.

Example requests

Create a user

When creating a user, you’ll notice three critical fields:

  • userName: The primary login identifier (must be unique).
  • externalId: A stable reference from your IdP that ensures idempotency.
  • id: The identifier returned by WorkOS, which you must store for all future requests.

Think of externalId as your source-of-truth key, while the WorkOS id is the handle you’ll use when making API calls. Without persisting this mapping, you won’t be able to update or delete users later.

Request (IdP → WorkOS):

	
POST {BASE_URL}/Users
Content-Type: application/scim+json

{
  "schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"],
  "userName": "jane.doe@example.com",
  "externalId": "00u2abcXYZ",
  "name": {
    "givenName": "Jane",
    "familyName": "Doe"
  },
  "displayName": "Jane Doe",
  "emails": [
    { "value": "jane.doe@example.com", "type": "work", "primary": true }
  ],
  "active": true
}
	

Response (WorkOS → IdP):

	
{
    "name": {
        "givenName": "Jane",
        "familyName": "Doe"
    },
    "active": true,
    "emails": [
        {
            "type": "work",
            "value": "jane.doe@example.com",
            "primary": true
        }
    ],
    "schemas": [
        "urn:ietf:params:scim:schemas:core:2.0:User",
        "urn:ietf:params:scim:schemas:extension:enterprise:2.0:User"
    ],
    "userName": "jane.doe@example.com",
    "externalId": "00u2abcXYZ",
    "displayName": "Jane Doe",
    "id": "directory_user_01K31NSNCTAYMZ6KCGDVXWVYFK",
    "meta": {
        "resourceType": "User",
        "created": "2025-08-19T21:24:57.874Z",
        "lastModified": "2025-08-19T21:24:57.874Z"
    }
}
	

Update specific user attributes

The PATCH example illustrates two SCIM fundamentals:

  1. Replace: Swap out an attribute (e.g., change displayName to "Jane D.").
  2. Add: Append new attributes without overwriting existing ones (e.g., add a secondary email).

This fine-grained approach avoids full object replacements and helps prevent race conditions in distributed systems.

Request (IdP → WorkOS):

	
PATCH {BASE_URL}/Users/{id}
Content-Type: application/scim+json

{
  "schemas": ["urn:ietf:params:scim:api:messages:2.0:PatchOp"],
  "Operations": [
    { "op": "replace", "path": "displayName", "value": "Jane D." },
    { "op": "add", "path": "emails", "value": [
         { "value": "jane.d@example.com", "type": "other" }
      ]
    }
  ]
}
	

Response (WorkOS → IdP):

	
{
    "name": {
        "givenName": "Jane",
        "familyName": "Doe"
    },
    "active": true,
    "emails": [
        {
            "type": "work",
            "value": "jane.doe@example.com",
            "primary": true
        },
        {
            "type": "other",
            "value": "jane.d@example.com"
        }
    ],
    "schemas": [
        "urn:ietf:params:scim:schemas:core:2.0:User",
        "urn:ietf:params:scim:schemas:extension:enterprise:2.0:User"
    ],
    "userName": "jane.doe@example.com",
    "externalId": "00u2abcXYZ",
    "displayName": "Jane D.",
    "id": "directory_user_01K31NSNCTAYMZ6KCGDVXWVYFK",
    "meta": {
        "resourceType": "User",
        "created": "2025-08-19T21:24:57.874Z",
        "lastModified": "2025-08-19T22:35:39.947Z"
    }
}
	

Deactivate a user

Deactivation (PATCH active: false) is usually safer than deletion, since it preserves historical references while preventing login. Deletion (DELETE /Users/{id}) should only be used when you’re certain the account should be removed entirely.

Enterprise IdPs often default to deactivation to ensure audit logs and references remain intact.

For more on this see Handling Inactive Users.

Request (IdP → WorkOS):

	
PATCH {BASE_URL}/Users/{id}
Content-Type: application/scim+json

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

Response (WorkOS → IdP):

	
# Secure flow returns an empty response
# Custom flow returns updated user
{
    "name": {
        "givenName": "Jane",
        "familyName": "Doe"
    },
    "active": false,
    "emails": [
        {
            "type": "work",
            "value": "jane.doe@example.com",
            "primary": true
        }
    ],
    "schemas": [
        "urn:ietf:params:scim:schemas:core:2.0:User",
        "urn:ietf:params:scim:schemas:extension:enterprise:2.0:User"
    ],
    "userName": "jane.doe@example.com",
    "externalId": "00u2abcXYZ",
    "displayName": "Jane Doe",
    "id": "directory_user_01K31T287N5KQC3XB2JS2KQQ7T",
    "meta": {
        "resourceType": "User",
        "created": "2025-08-19T22:39:33.562Z",
        "lastModified": "2025-08-19T22:40:18.813Z"
    }
}
	

Permanently delete a user

Deleting a user is the most destructive operation in SCIM. Unlike deactivation, which simply sets active: false, deletion removes the user record entirely from the WorkOS SCIM directory connection.

Key things to know:

  • Use only when necessary: Many IdPs prefer deactivation instead, to preserve audit trails.
  • Group memberships are removed: Any groups the user belonged to will lose this reference.
  • Error handling: If the user doesn’t exist, the SCIM server will respond with a 404 Not Found.
  • Irreversible: Once deleted, the WorkOS id for that user cannot be reused.

Request (IdP → WorkOS):

	
DELETE {BASE_URL}/Users/{userId}
Authorization: Bearer {YOUR_DIRECTORY_BEARER_TOKEN}
Accept: application/scim+json
	

Response (WorkOS → IdP):

	
204 No Content
	

Create a group

Groups are containers for users that represent roles, teams, or departments. When creating one:

  • displayName: This is the friendly name shown in WorkOS (e.g., “Engineering”).
  • externalId: Just like with users, this should be a stable reference from your IdP to ensure idempotency on retries.
  • id: Returned by WorkOS; store it for future update or delete operations.

Groups are typically created empty, and members are added afterward using PATCH.

Request (IdP → WorkOS):

	
POST {BASE_URL}/Groups
Content-Type: application/scim+json

{
  "schemas": ["urn:ietf:params:scim:schemas:core:2.0:Group"],
  "displayName": "Engineering",
  "externalId": "00u2abcLMN",
  "members": []
}
	

Response (WorkOS → IdP):

	
{
    "schemas": [
        "urn:ietf:params:scim:schemas:core:2.0:Group"
    ],
    "externalId": "00u2abcLMN",
    "displayName": "Engineering",
    "members": [],
    "id": "directory_group_01K31TPK8HT5KFP1EJ0CPJCT84",
    "meta": {
        "resourceType": "Group",
        "created": "2025-08-19T22:50:40.271Z",
        "lastModified": "2025-08-19T22:50:40.271Z"
    }
}
	

Add members to a group

Once you’ve created a group, you can add users to it incrementally. This is done with a PATCH request using the add operation.

Important points:

  • Reference by WorkOS IDs: Memberships are tied to the WorkOS id for each user, not your internal IDs.
  • Batching: You can add multiple members in a single request by including several user IDs in the value array.
  • Atomicity: All members listed in one request are added together. If you need to add a large set, consider chunking the requests to avoid hitting payload limits.

Request (IdP → WorkOS):

	
PATCH {BASE_URL}/Groups/{groupId}
Content-Type: application/scim+json
Authorization: Bearer {YOUR_DIRECTORY_BEARER_TOKEN}

{
  "schemas": ["urn:ietf:params:scim:api:messages:2.0:PatchOp"],
  "Operations": [
    {
      "op": "add",
      "path": "members",
      "value": [
        { "value": "directory_user_01K31TVGN88YP152228P59P3TA" }
      ]
    }
  ]
}
	

Response (WorkOS → IdP):

	
{
    "schemas": [
        "urn:ietf:params:scim:schemas:core:2.0:Group"
    ],
    "externalId": "00u2abcLMN",
    "displayName": "Engineering",
    "members": [
        {
            "value": "directory_user_01K31TVGN88YP152228P59P3TA",
            "display": "jane.doe@example.com"
        }
    ],
    "id": "directory_group_01K31TPK8HT5KFP1EJ0CPJCT84",
    "meta": {
        "resourceType": "Group",
        "created": "2025-08-19T22:50:40.271Z",
        "lastModified": "2025-08-19T22:50:40.271Z"
    }
}
	

Remove members from a group

Removing members also uses a PATCH request, but with the remove operation.

Key details:

  • Filter syntax: The path specifies which member to remove using the user’s WorkOS id.
  • Selective removal: You can target a single user without affecting the rest of the group.
  • Consistency: Always make sure the user exists and is still active in WorkOS before attempting removal.

Request (IdP → WorkOS):

	
PATCH {BASE_URL}/Groups/{groupId}
Content-Type: application/scim+json
Authorization: Bearer {YOUR_DIRECTORY_BEARER_TOKEN}

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

Response (WorkOS → IdP):

	
{
    "schemas": [
        "urn:ietf:params:scim:schemas:core:2.0:Group"
    ],
    "externalId": "00u2abcLMN",
    "displayName": "Engineering",
    "members": [],
    "id": "directory_group_01K31TPK8HT5KFP1EJ0CPJCT84",
    "meta": {
        "resourceType": "Group",
        "created": "2025-08-19T22:50:40.271Z",
        "lastModified": "2025-08-19T22:50:40.271Z"
    }
}
	

Permanently delete a group

Deleting a group is similar to deleting a user—once removed, it’s gone permanently.

Things to keep in mind:

  • Memberships cleared: All user associations are deleted along with the group.
  • Error handling: If the group doesn’t exist, the server returns 404 Not Found.
  • Idempotency: Ensure you only delete groups that you’re certain should be removed, as there’s no recovery step.

Request (IdP → WorkOS):

	
DELETE {BASE_URL}/Groups/{groupId}
Authorization: Bearer {YOUR_DIRECTORY_BEARER_TOKEN}
Accept: application/scim+json
	

Response (WorkOS → IdP):

	
204 No Content
	

Operational tips

  • Always set externalId on create requests for safe retries. externalId acts as the idempotency key between your IdP and WorkOS.
  • Gracefully handle rate limit errors (HTTP 429). Check the WorkOS Rate-limit reference guide for more details
  • Set monitoring, alerts & triage workflows for relevant 4xx and 5xx responses.
  • Payload size considerations
    • Only send needed attributes — reduces payload size and improves performance.
    • Respect payload size limits (413 Payload Too Large response) by breaking up operations if possible. For example, if adding a large number of members to a group, the add members PATCH request can be broken up into separate requests.
  • Serialize updates per user or group to prevent race conditions. For example
    • Only send User update, deactivation, or deletion requests after confirming the corresponding Create User request was successfully processed by the SCIM server.
    • Similarly, only add or remove group members after verifying that all Create User requests for those members have been successfully acknowledged by the SCIM server. You can use the GET endpoints (GET /Users, GET /Groups) to verify a resource exists.
  • Group membership updates for a de-activated user
    • When a user is deactivated in the IdP, it's important to clean up their group memberships by issuing requests to remove them from groups. IdPs should not assume that service providers will maintain references to a deactivated user's previous group memberships. Not doing so can lead to security vulnerabilities where the service provider's view of the state would diverge from the IdP.
  • Group membership updates for a re-activated user
    • When a user is reactivated in the IdP, it's essential to republish all relevant group memberships. Never assume the service provider has retained group memberships for previously deactivated or deleted users. Not doing so can lead to security vulnerabilities where the service provider's view of the state would diverge from the IdP.
  • Use filtering on username or externalId to avoid duplicates.
	
GET /Users?filter=userName eq "jane.doe@example.com"

GET /Users?filter=externalId eq "00u2abcXYZ"
	
  • Support pagination on GET /Users and GET /Groups using startIndex and count.
  • Test small batches first before enabling full provisioning syncs.
  • Conduct periodic full synchronization of all users, groups, and group memberships. This ensures consistency across all systems.
  • If the Base URL is changed, ensure all user and group entities are still properly linked, or perform a full sync of the directory state. Not doing so can result in data integrity issues between the IdP and service provider.

Going live checklist

  1. Get Base URL and Bearer Token from the application admin.
  2. Implement all necessary SCIM calls in your IdP.
  3. Use correct headers and JSON schema.
  4. Handle 4xx and 5xx responses gracefully.
  5. Confirm you’re sending required attributes for Users and Groups.
  6. Run a provisioning test with a few users before enabling all users and groups sync.

Final thoughts

Implementing a SCIM client isn’t just about making API calls—it’s about aligning your IdP with the enterprise standard for identity management. By following the patterns above, you’ll ensure your provisioning logic is:

  • Standards-compliant with RFC 7644 and RFC 7643.
  • Resilient against retries, race conditions, and failures.
  • Interoperable with enterprise IdPs and service providers.

Before going live, use the Going Live Checklist to validate your implementation. Test in small batches, monitor for errors, and set up synchronization workflows to keep systems consistent.

Once in production, your SCIM client will make it easy for IT admins to manage users and groups at scale—removing friction for adoption and making your application enterprise-ready.

This site uses cookies to improve your experience. Please accept the use of cookies on this site. You can review our cookie policy here and our privacy policy here. If you choose to refuse, functionality of this site will be limited.