How to Implement SCIM 2.0 Provisioning in a Multi-Tenant SaaS Product
SCIM 2.0 provisioning is required to sell into enterprises. This guide covers the protocol, tenant isolation, IdP quirks, and reconciliation to get it right.
SCIM 2.0 provisioning is the mechanism enterprise customers use to automatically create, update, suspend, and delete user accounts in your SaaS product as their workforce changes. Defined by RFC 7643 (schema) and RFC 7644 (protocol), SCIM has become a procurement gate: every major enterprise identity provider — Okta, Microsoft Entra ID, Google Workspace, OneLogin, and Ping Identity — supports SCIM 2.0, and enterprise IT teams require SCIM support before committing to a vendor. A missing SCIM integration stalls enterprise deals, forces manual provisioning workflows, and produces the orphaned account problem that surfaces as a finding in every SOC 2 audit.
The protocol itself is not the hard part. RFC 7644 is clearly specified and the endpoints are straightforward REST. The hard part is implementing SCIM correctly inside a multi-tenant SaaS architecture: isolating each customer's provisioning context so one tenant's IdP cannot read or write another's user records, handling the large initial directory sync without overwhelming your database, absorbing the behavioural differences between Okta and Entra ID that the RFC does not specify, and building the reconciliation job that catches provisioning events that were never delivered. These are the details that break naive implementations during the first enterprise customer onboarding. They are also exactly the concerns covered in the broader multi-tenant SaaS architecture guide on this blog, which covers tenant isolation patterns, connection pooling, and the database design decisions that determine whether your SCIM implementation is structurally sound.
This guide is for engineering leads and architects building SCIM support in-house on an existing multi-tenant SaaS product. We cover what you must build, how to structure tenant isolation, the Okta and Entra ID quirks that require specific handling, queue design for large directory syncs, reconciliation job design, and the build-versus-buy decision. Getting SCIM right the first time is faster than discovering its failure modes during a 10,000-seat enterprise onboarding. If you are still deciding whether your product needs SCIM at all, start with the checklist in the enterprise-ready SaaS guide.
What Is SCIM 2.0 and What Does Your SaaS Need to Build?
SCIM (System for Cross-domain Identity Management) is a JSON REST API specification that lets identity providers push user lifecycle events — create, update, suspend, reactivate, delete — to downstream SaaS applications. The identity provider is the source of truth for who works at the company; SCIM is the protocol by which that truth propagates to every application the company uses. Your SaaS is the SCIM service provider: you expose the endpoints, the IdP calls them. A minimum production-ready SCIM 2.0 implementation requires the following resources and operations.
- →POST /scim/v2/Users — create a user. The request body is a SCIM User resource containing userName (typically the work email address), name.givenName, name.familyName, emails array, active (boolean), and externalId — the IdP stable identifier for the user that survives email address changes and is the correct field for deduplication across reconnects.
- →GET /scim/v2/Users — list users with optional filter query parameters. Okta issues a filter lookup by externalId before creating a user to check whether the user already exists in your system. Return a ListResponse envelope with totalResults, startIndex, itemsPerPage, and a Resources array. Support pagination via startIndex and count query parameters.
- →GET /scim/v2/Users/{id} — retrieve a single user by their SCIM id (your internal user ID, not the IdP externalId). Used by the IdP to read current state before issuing a PATCH or PUT.
- →PUT /scim/v2/Users/{id} — full replacement update. Okta favours PUT over PATCH for user attribute changes. Replace all writable attributes on the record. Silently ignore read-only attributes such as id and meta in the request body rather than returning an error.
- →PATCH /scim/v2/Users/{id} — partial update using SCIM PatchOp. Entra ID sends PATCH for nearly all updates. Parse the Operations array: each operation has an op (add, remove, or replace), a path, and a value. Paths can reference top-level attribute names or use complex filter-qualified expressions to target nested attributes within arrays.
- →DELETE /scim/v2/Users/{id} — deprovision a user. Return 204 No Content on success — this is the RFC-required response code, and deviating from it breaks some IdPs. Some products mark the user inactive on DELETE rather than hard-deleting; both are valid, but document which your implementation does.
- →GET and PATCH /scim/v2/Groups and /scim/v2/Groups/{id} — required if your product models groups or roles. A PATCH Groups operation includes member additions and removals in a single request; process all member changes atomically within a transaction.
- →GET /scim/v2/ServiceProviderConfig — returns a JSON document describing your endpoint capabilities. Okta reads this during connection setup to populate its attribute mapping UI. Return at minimum: patch supported true, filter supported true with a maxResults value, changePassword supported false, sort supported false, etag supported false.
Multi-Tenant SCIM Endpoint Architecture: Token Design and URL Structure
The foundation of a production-ready SCIM implementation in multi-tenant SaaS is the relationship between a bearer token, a tenant, and a SCIM connection. Each enterprise customer configures one SCIM connection from their IdP to your product. That connection is authenticated by a unique bearer token you generate per connection. The token is the tenant key — it determines which tenant's user records are accessible for every operation in that connection. This is the isolation boundary and it cannot be relaxed.
- →Base URL options: embed the tenantId in the path as /scim/v2/{tenantId}/, or use a static base URL /scim/v2/ where the tenant is resolved entirely from the bearer token. The static-URL pattern is safer because it eliminates any risk of a request where the URL-embedded tenantId disagrees with the token-derived tenantId.
- →Token generation: generate a cryptographically random token of at least 32 bytes entropy, URL-safe base64 encoded. In Node.js: crypto.randomBytes(48).toString('base64url'). A UUID is not sufficient — it has only 122 bits of entropy and is not designed for bearer token use.
- →Token storage: store a bcrypt or Argon2id hash of the token in your database, never the plaintext token. Show the unhashed token to the admin exactly once at creation time. A leaked database backup does not expose bearer tokens when stored as hashes.
- →Token-to-tenant binding: maintain a ScimConnection table with columns for id, tenantId, tokenHash, createdAt, and lastUsedAt. On every SCIM request, verify the Authorization Bearer header, hash the supplied token, look up the ScimConnection by hash, and extract the tenantId. All subsequent database queries for that request must filter by the token-derived tenantId.
- →Token rotation: expose a UI for admins to rotate their SCIM bearer token. Rotation generates a new token, updates the hash in ScimConnection, and immediately invalidates the previous token. The admin reconfigures their IdP with the new token. Rotation should also be available via your management API for programmatic credential rotation.
SCIM Tenant Isolation: The Security Requirement That Cannot Be Skipped
Tenant isolation in SCIM is a security-critical architectural requirement, not an optimisation. A SCIM endpoint that resolves the tenant from anything other than the verified bearer token has a horizontal privilege escalation vulnerability: a misconfigured or malicious IdP connection could read or modify another customer's user records. Every SCIM endpoint must enforce tenant isolation at the database query layer, not just at the routing layer.
- →Every database query on every SCIM endpoint must include the token-derived tenantId as a mandatory WHERE condition. This means GET /scim/v2/Users/{id} verifies that the user with that id belongs to the requesting tenant before returning any data. A valid SCIM user ID from a different tenant returns 404, not 403 — returning 404 avoids confirming that the resource exists for another tenant.
- →Never trust a tenantId supplied in the URL path or request body. If your URL scheme includes a tenantId path segment, treat it as unauthenticated input. Always re-derive the authoritative tenantId from the bearer token and reject any request where the URL tenantId does not match.
- →Use PostgreSQL row-level security or application-level guard functions — choose one approach and apply it consistently. Application-layer guards, where a request context object carries the verified tenantId and is threaded through every query builder call, are easier to audit than RLS policies because they are visible in the same code path as the query.
- →Scope the SCIM connection token type to user provisioning only. The SCIM bearer token must not be accepted by any other API endpoint in your system. Implement a distinct token prefix — for example scim_ versus apk_ — and check the prefix in authentication middleware before token hash lookup.
- →Log every SCIM operation with timestamp, tenantId, endpoint path, HTTP method, SCIM resource type and ID, and outcome. This is your audit trail for SOC 2 access control evidence and for debugging provisioning failures with enterprise IT teams who need a clear history of what their IdP sent and what your system did with it.
Okta and Entra ID Behavioural Differences That Require Specific Handling
SCIM 2.0 is a well-specified protocol, but the two dominant enterprise IdPs implement it with specific patterns that differ from each other and in some cases from a strict reading of the RFC. Handling both IdPs correctly requires knowing these patterns before you write the first endpoint. Most SCIM documentation describes the abstract protocol; production deployments reveal the implementation specifics.
- →Okta — user existence check: before POST /Users, Okta issues a GET /Users filter request to check whether the user already exists in your system by externalId. Return a ListResponse with totalResults set to 0 and an empty Resources array if the user is not found. Return the existing user resource if they are found. Responding 404 to this filter request causes Okta to silently skip user creation.
- →Okta — update method: Okta sends PUT /Users (full replacement) for most user attribute changes, rarely using PATCH. Your PUT handler must accept the full SCIM User resource and replace all writable fields. Do not return an error for read-only attributes in the PUT body — silently ignore them.
- →Okta — initial sync: when an admin connects their Okta directory to your SCIM endpoint, Okta triggers an immediate full sync of all assigned users. For a directory of 5,000 users this means 5,000 POST /Users requests arriving in rapid succession — more write traffic than a typical day of product usage compressed into under two minutes.
- →Entra ID — update method: Entra ID sends PATCH for nearly all user updates using SCIM PatchOp, including complex filter-qualified paths to modify attributes within arrays. Your PATCH handler must correctly parse the Operations array and handle both simple operations (op plus path plus value) and complex filter-qualified path forms.
- →Entra ID — externalId drift: Entra ID drops the externalId attribute in certain sync scenarios — a directory migration, a service principal reconfiguration, or a schema refresh. When externalId disappears and then reappears, naive implementations create a duplicate user record. Use userName as the primary deduplication key in addition to externalId, and handle the case where externalId is absent in a POST /Users body by falling back to a userName-based lookup.
- →Both IdPs: expect 204 No Content on DELETE success, not 200. Return 409 Conflict, not 422, when a POST /Users attempts to create a user whose userName or externalId already exists. Both IdPs implement exponential backoff when they receive 429 Too Many Requests with a Retry-After header. Test your endpoint against both IdPs in a staging environment before any customer-facing availability.
SCIM Rate Limiting and Queue Architecture for Large Directory Syncs
A SCIM endpoint that writes directly to the database on every request works correctly at low volume and fails at the moment it matters most — the initial sync of your first large enterprise customer. Designing your SCIM write path to absorb burst load is not premature optimisation; it is a requirement for enterprise readiness. This is closely related to the rate limiting patterns for multi-tenant SaaS discussed elsewhere on this blog — the same concerns around per-tenant rate limits, burst queuing, and backpressure apply directly to SCIM provisioning traffic.
- →Return 429 Too Many Requests with a Retry-After header when a tenant's SCIM connection exceeds your rate limit. Both Okta and Entra ID respect Retry-After and implement exponential backoff on receiving 429. Set rate limits per ScimConnection, not globally — one customer's initial sync should not degrade provisioning for other tenants.
- →Queue SCIM write operations through a durable job queue such as BullMQ, AWS SQS, or Google Cloud Tasks. Receive the SCIM request, validate the payload and authenticate the token synchronously, enqueue the write, and return the expected HTTP success response immediately. You do not need the database write to complete before responding — construct the user resource from the request payload for the 201 Created response body.
- →Use database upsert semantics for user creation so that retries from the IdP are safe. An INSERT with ON CONFLICT DO UPDATE ensures that a duplicate POST triggered by an idempotency retry updates the existing record rather than creating a duplicate user or returning an error that confuses the IdP.
- →Implement a separate processing queue for initial syncs with a per-tenant concurrency limit — for example ten concurrent writes per tenant during initial sync versus fifty during steady-state operation. This prevents a single customer's bulk import from saturating your database connection pool and causing write queuing for all tenants.
- →Monitor SCIM queue depth and processing latency per tenant. Alert when queue depth exceeds a threshold that would cause provisioning delays visible to the customer. A user assigned in Okta who cannot log in to your product after fifteen minutes will generate a support ticket from the customer's IT team.
SCIM Reconciliation: The Background Job That Catches What Events Miss
SCIM is an event-driven protocol — the IdP pushes changes as they occur — but event delivery is not guaranteed. Network failures, IdP bugs, and misconfiguration silently drop provisioning events. An employee who leaves the company may remain active in your SaaS indefinitely if the SCIM delete event was never delivered. Orphaned accounts — users who are provisioned but never deprovisioned when they leave — are the most common access control finding in SOC 2 audits and the primary risk a SCIM reconciliation job exists to prevent.
- →Implement a nightly reconciliation job per active SCIM connection. The job retrieves the complete list of users currently marked active in your SaaS for that tenant, compares it against the current directory state from the IdP where the IdP exposes a read API, and deactivates any user who is active in your system but absent from the IdP's current user set.
- →Track a lastScimActivityAt timestamp on every provisioned user record — the timestamp of the last SCIM event that touched that record. For IdPs that do not expose a read API for full directory comparison, flag any active provisioned user whose lastScimActivityAt is older than 30 days as requiring manual review.
- →Log SCIM deactivation and deletion events separately with the tenantId, userId, ScimConnection identifier, and timestamp. In regulated industries, deactivating an account is an auditable access control event — the log must be immutable and retained for the period your compliance framework requires.
- →Expose a manual re-sync trigger in your admin UI. An IT administrator at the customer should be able to initiate a full directory comparison from the settings page without contacting your support team. This self-service path resolves the most common provisioning support ticket: a user who was assigned in the IdP but never appeared in your product.
- →Test reconciliation explicitly in your staging environment. Scenario: assign a user in Okta, confirm provisioning. Disconnect the SCIM connection. Remove the user in Okta. Reconnect the SCIM connection. Run the reconciliation job. The user must be deactivated in your system despite the deletion event never arriving. This is the scenario that catches the gap between event-driven provisioning and actual directory state.
Build vs. Buy: When to Implement SCIM In-House and When to Use a Vendor
The SCIM vendor landscape in 2026 includes WorkOS, Frontegg, PropelAuth, and SSOJet, all offering a broker pattern: the vendor handles SCIM ingestion from IdPs and pushes normalised webhook events to your application. The build-versus-buy decision depends on your team's experience, your scale, and your tolerance for third-party dependencies in the user provisioning path.
- →Build in-house when: you have a mature multi-tenant architecture, your team has experience with identity protocols, your enterprise customers have non-standard IdP configurations requiring custom handling, or your pricing at scale makes per-seat vendor fees uneconomical at the volumes you expect.
- →Use a vendor when: you need SCIM live in weeks rather than months, your team has never built an identity protocol integration, you have fewer than 50 enterprise customers, or the engineering opportunity cost of building in-house exceeds the vendor fees at your current stage.
- →Broker pattern trade-offs: the vendor handles IdP-specific quirks, retry logic, and connection management — the highest-value parts of a SCIM implementation. You receive normalised webhooks and implement only the user model changes in your own codebase. The trade-off is that all provisioning events flow through a third party, which creates a dependency in your enterprise customer's access lifecycle and a data-processing relationship to disclose in your Data Processing Agreement.
- →Hybrid approach: implement the SCIM protocol endpoints yourself using a well-tested library such as SCIMMY in TypeScript to handle PatchOp parsing and filter evaluation, while using a vendor's connection management UI to avoid building the admin configuration interface from scratch.
- →Timeline for in-house: a production-ready SCIM implementation covering both Okta and Entra ID, tenant isolation, rate limiting, and reconciliation typically requires four to six engineering weeks for a team without prior SCIM experience. Week one for protocol implementation and schema design. Week two for tenant isolation and security hardening. Week three for IdP-specific testing and quirk handling. Weeks four through six for the reconciliation job, operational logging, and admin UI. Teams that compress this into a weekend discover the Entra ID PATCH edge cases and the initial-sync load problem during their first large customer onboarding.
Frequently Asked Questions
What SCIM 2.0 endpoints does a SaaS product need to implement as a minimum?
A minimum viable SCIM 2.0 implementation requires POST, GET list, GET single, PUT, PATCH, and DELETE on /scim/v2/Users, plus GET /scim/v2/ServiceProviderConfig for capability discovery. If your product models groups or roles, add the same operations for /scim/v2/Groups. The Schemas and ResourceTypes discovery endpoints are optional but expected by Okta when the admin uses the attribute mapping UI during initial connection setup.
How do you prevent one tenant's SCIM connection from accessing another tenant's users?
Derive the authoritative tenantId from the verified bearer token at the start of every request, and include that tenantId as a mandatory condition on every database query in every SCIM endpoint. Never trust a tenantId supplied in the URL path or request body — treat it as unauthenticated input and reject the request if it disagrees with the token-derived tenantId. Return 404, not 403, for resources belonging to a different tenant to avoid confirming that the resource exists in your system.
What HTTP response codes must a SCIM 2.0 endpoint return?
Return 201 Created for successful POST /Users with the created user resource in the response body. Return 200 OK for GET, PUT, and PATCH when a response body is included. Return 204 No Content for DELETE — this is the RFC-required code, and returning 200 instead breaks some IdPs. Return 404 for a resource that does not exist or belongs to a different tenant. Return 409 Conflict when a POST /Users duplicates an existing userName or externalId. Return 429 Too Many Requests with a Retry-After header when rate-limiting an IdP connection. Return 400 Bad Request for structurally invalid SCIM payloads.
How do you handle the initial directory sync when an enterprise customer first connects their IdP?
Route SCIM write operations through a durable job queue and process them asynchronously. Receive the SCIM request, validate the payload and token, enqueue the write, and return the expected HTTP success response immediately — do not wait for the database write to complete. Construct the user resource for the 201 Created response from the request payload. Set a per-tenant concurrency limit on the initial sync queue so a single customer bulk import does not saturate your database connection pool. Return 429 with a Retry-After header if queue depth for a tenant exceeds a safe threshold.
Should we build SCIM in-house or use a provisioning vendor?
Use a vendor when you need SCIM live quickly, your team lacks identity protocol experience, or you are at an early stage where time-to-market outweighs long-term cost. Build in-house when you have unusual schema requirements, your pricing at scale makes per-seat vendor fees significant, or you need complete control over the provisioning pipeline for compliance reasons. A hybrid approach — implement the SCIM endpoints yourself using an open-source library for PatchOp and filter parsing, and use the vendor only for connection management UI — is often the best middle ground: it eliminates the third-party dependency from the data path while still saving the admin interface work.
How Belsoft Helps You Ship SCIM Provisioning
Belsoft designs and builds the enterprise identity features that unblock enterprise deals — SCIM provisioning, SSO, audit logging, and the multi-tenant isolation architecture they depend on. We design implementations that are secure from day one, operationally maintainable long after the initial build, and structured to pass the access control requirements of SOC 2 and enterprise procurement reviews. If you are building SCIM into an existing multi-tenant product or designing the enterprise-readiness layer for a new platform, our SaaS engineering practice covers the full stack from data model through admin UI and IdP certification testing.
Whether you are evaluating the build-versus-buy trade-off, recovering from a naive SCIM implementation that failed under enterprise load, or planning SCIM alongside SSO and role-based access control as part of an enterprise-readiness programme, we provide the architecture review and implementation support to get it right. Book a technical conversation with our team to discuss your IdP requirements, existing multi-tenant architecture, and timeline.
“SCIM is not a checkbox. It is the interface your enterprise customers use to trust that your product will behave correctly when an employee leaves. Get the tenant isolation right the first time.”
Written by
Belsoft Team
More from the blog
Ready to build?
Let's talk about your project.
30 minutes. No pitch. We map your requirements and tell you honestly what it will take.
Book a Strategy Call