Skip to content

Set Up SCIM Provisioning for Enterprise Customers

IAM · practitioner · 8 min read · last reviewed 2026-07-07

How to implement SCIM 2.0: the User and Group schema, the endpoints you must build, deactivate-not-delete flows, group-to-role mapping, and testing against Okta and Entra.

TL;DR

  • SCIM 2.0 gives enterprise IdPs an API to provision and, critically, deprovision users in your app automatically.
  • The schema (RFC 7643) defines two core resources, User and Group; the protocol (RFC 7644) defines the /Users and /Groups CRUD endpoints plus ServiceProviderConfig, ResourceTypes, and Schemas discovery.
  • Offboarding is the security control that sells SCIM: a PATCH setting active to false must revoke sessions and tokens immediately.
  • Deactivate over delete: flip the active flag, keep the record for audit and rehire, and reserve DELETE for a deliberate purge policy.
  • Okta and Microsoft Entra interpret PATCH, filtering, and attribute defaults differently, so per-IdP testing and ongoing maintenance are required.

Enterprises want SCIM because it turns user provisioning into an API contract their identity provider drives automatically. When someone joins, moves, or leaves, the IdP pushes the change to your app in seconds. To support it you implement a small REST surface defined by SCIM 2.0: /Users and /Groups endpoints that create, read, update, and deactivate accounts. This guide covers the schema, the endpoints, and the parts the specs do not warn you about.

Why enterprises want automated user lifecycle

They want it because manual provisioning does not scale and, more importantly, manual deprovisioning fails as a security control. Onboarding is the friendly half: a new hire lands in the IdP, and within a minute your app has an account with the right role, no ticket, no shared spreadsheet. The half that actually sells the deal is offboarding. When someone is terminated, the IdP flips their status and your app must revoke access immediately, not whenever an admin remembers.

That offboarding path is why security and IT teams gate purchases on SCIM support. SSO alone stops a departed employee from logging in through the front door, but it does not delete the local session, the API token, or the account itself. SCIM does. If your product handles anything sensitive, "does it support automated deprovisioning" is a checkbox in the procurement review, and a missing checkbox is a lost enterprise contract. For the wider vendor landscape, see top user provisioning and governance tools.

SCIM 2.0 core schema: Users and Groups

SCIM 2.0 defines two core resources, User and Group, in RFC 7643 (datatracker.ietf.org/doc/html/rfc7643). Every resource carries an id you assign, an externalId the IdP assigns, a schemas array naming which schemas apply, and a meta object with resourceType, timestamps, and a location URL.

The User schema uses userName as the unique login handle, plus name (a sub-object with givenName and familyName), emails, and the boolean active that drives deactivation. Group has a displayName and a members array of references back to users.

{
  "schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"],
  "userName": "asha@acme.com",
  "name": { "givenName": "Asha", "familyName": "Rao" },
  "emails": [{ "value": "asha@acme.com", "primary": true }],
  "active": true,
  "externalId": "00u1a2b3c4"
}

You do not have to store every attribute. Persist what your product uses and echo the rest back on read. Map the SCIM attributes you keep to your own model deliberately:

SCIM attributeYour model fieldNotes
userNamelogin_emailUnique key. Do not treat as mutable id.
externalIdidp_external_idStable IdP handle. Join on this, not email.
name.givenNamefirst_name
name.familyNamelast_name
emails[primary]email
activestatusfalse means deactivate, never delete.
idscim_idYou mint it. Return it in meta.location.

The single most common integration bug is joining on userName or email. People change both. Join on externalId, which the IdP keeps stable across a rename.

The endpoints you must implement

You must implement the CRUD surface for /Users and /Groups plus a service discovery trio, all defined in RFC 7644 (datatracker.ietf.org/doc/html/rfc7644). Base everything under a versioned path such as /scim/v2.

Method and pathPurpose
POST /UsersCreate a user. Return 201 with the full resource.
GET /Users/{id}Read one user.
GET /Users?filter=...Query, usually by userName eq "x".
PUT /Users/{id}Full replace of a user.
PATCH /Users/{id}Partial update, commonly toggling active.
DELETE /Users/{id}Optional. Most IdPs deactivate instead.
POST /GroupsCreate a group.
GET /Groups/{id}Read one group.
PATCH /Groups/{id}Add or remove members.
GET /ServiceProviderConfigAdvertise supported features.
GET /ResourceTypesList User and Group types.
GET /SchemasPublish your attribute definitions.

Do not skip the discovery three. Okta and Microsoft Entra call ServiceProviderConfig during setup to learn whether you support PATCH and filtering, and they change their behavior based on the answer. Support at least filter with the eq operator on userName, because that is how an IdP checks whether a user exists before deciding to create or update.

Create, update, and deactivate flows

The flow is create on first sync, update on change, and deactivate (not delete) on offboarding. On POST /Users, first run a filter for the userName. If it exists, return 409 Conflict; the IdP will then switch to PATCH. If not, create the record, generate your id, and return 201 with the complete resource and a meta.location header.

For deactivation, the IdP sends a PATCH that sets active to false:

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

Treat this as the security event it is. Revoke sessions, invalidate API tokens, and drop any cached authorization for that user, then return the user with active: false. Do not hard delete on active: false. Deactivate over delete matters for three reasons: audit trails need the record to survive, reactivation (the rehire or the fixed typo) must restore the same account and its history, and a DELETE that cascades can orphan data the user created. Keep the row, flip the flag, cut off access. Reserve real DELETE for a deliberate purge policy, not for routine offboarding.

Mapping SCIM groups to your roles

Map SCIM groups to your roles by treating group membership as the input to your own RBAC engine, never as the roles themselves. The IdP owns the groups. Your app owns what a role can do. Keep that boundary clean.

Do not invent your own group-to-role UI inside your product. Let admins name groups in the IdP (app-admins, app-readonly), then maintain a mapping table that resolves a group to a role. When a PATCH adds a user to app-admins, look up the mapped role and apply it. If a group has no mapping, assign your safest default, usually read-only, and log it. That prevents an unmapped group from silently granting more than intended.

Push authorization decisions past group names when you can. Groups are coarse. If you need conditions like resource ownership or time of day, resolve the group to a role and let a finer model such as ABAC make the call. SCIM gets identity and coarse grouping into your app; your own policy layer does the rest.

Testing against Okta and Microsoft Entra

Test against both Okta and Microsoft Entra directly, because passing one does not mean passing the other. Okta ships a SCIM SPEC test suite and a runner (Okta's developer docs at developer.okta.com) that hammers your CRUD and filtering. Start there because the errors are specific. Then add the app in Entra and run a real provisioning cycle, because Entra behaves differently in ways that will surprise you.

The per-IdP quirks that cost the most time:

  • Entra sends fewer attributes than you expect. It relies on default attribute mappings the admin can edit, so name.givenName may simply not arrive. Handle missing fields gracefully.
  • Entra PATCH payloads vary. It sometimes wraps value differently and sometimes omits path. Parse defensively.
  • Filtering syntax differs at the margins. Both send userName eq "x", but escaping and case sensitivity are inconsistent. Compare case-insensitively.
  • Entra provisions on a cycle, not instantly. Its incremental sync can run every 40 minutes, so "it did not work" often means "it has not run yet."

Microsoft's SCIM guidance (learn.microsoft.com) documents its interpretation; read it alongside the RFC, because where they disagree, Entra follows Microsoft. For which providers your customers actually run, see new IAM providers 2025 and top IAM solutions.

Idempotency and rate limits

Make every write idempotent and expect the IdP to retry aggressively. SCIM clients retry on timeouts and 5xx, so the same create can arrive twice. If you generate a new record on the retry, you get duplicate users. Guard creates with a uniqueness check on externalId (and userName), and return the existing resource rather than a second one.

On rate limits, IdPs can burst hundreds of operations during an initial sync or a large group change. Return 429 with a Retry-After header when you need to shed load; both Okta and Entra honor it and back off. Do not silently drop requests, because a dropped deactivation is exactly the security gap SCIM was bought to close. Log every operation with its externalId and outcome so you can prove, during an audit, that a specific offboarding actually landed.

What this costs you: ongoing schema drift

The real cost of SCIM is not the initial build, it is maintaining a slightly different integration per IdP forever. The spec is one document. The implementations are not. Okta, Entra, OneLogin, and Google each interpret PATCH, filtering, and attribute defaults with their own conventions, and each ships changes on its own schedule without asking you.

So budget for the long tail. Every enterprise customer may map a custom attribute (employee number, cost center, department) into your /Schemas, and each mapping is a small contract you now support. Keep a per-IdP conformance test in CI, version your SCIM endpoint from day one so you can evolve without breaking live syncs, and assign an owner, because a SCIM integration that no one maintains quietly rots until an offboarding fails. Authentication and provisioning stay separate concerns here: SCIM never carries a login. That runs over SAML or OIDC, often alongside MFA and a JWT session, and is worth building first per Add SSO to Your B2B SaaS.

Key takeaways

  • Join records on externalId, not userName or email, because people change both and the IdP keeps externalId stable.
  • Never hard delete on active:false. Deactivation preserves audit trails and lets a rehire restore the same account.
  • Treat group membership as input to your own RBAC engine; the IdP owns groups, your app owns what roles can do.
  • Make every write idempotent and honor 429 with Retry-After, because IdPs retry hard and burst during initial sync.
  • The ongoing cost is schema drift: budget a per-IdP conformance test in CI and an owner, or offboarding quietly breaks.
  • SCIM handles provisioning only; authentication still runs over SAML or OIDC, so build SSO first.

Frequently asked questions

What is SCIM?
SCIM (System for Cross-domain Identity Management) is an open standard, defined in RFC 7643 and RFC 7644, for automatically provisioning and deprovisioning user accounts between an identity provider and your application over a REST API.
SCIM vs SSO, what is the difference?
SSO authenticates a user at login, usually over SAML or OIDC. SCIM manages the account lifecycle: creating, updating, and deactivating users independently of any login event. Enterprises typically want both.
Should I delete or deactivate users?
Deactivate. Set active to false, revoke access, and keep the record so audit history survives and a rehire restores the same account. Reserve hard delete for a deliberate data purge policy.
Which endpoints must I implement?
At minimum the /Users CRUD endpoints, /Groups with PATCH for membership, and the ServiceProviderConfig, ResourceTypes, and Schemas discovery endpoints so IdPs can detect your supported features.
Do Okta and Entra behave the same?
No. They differ on PATCH payload shape, filtering syntax, attribute defaults, and sync timing. Test against both directly rather than assuming one passing means the other works.

Related

← All How-To & Implementation guides