Provisioning and deprovisioning users is a baseline requirement for SaaS platforms integrating with enterprise identity providers. Standards like SCIM 2.0 are expected by default when selling to companies using Okta, Azure AD, or Google Workspace for user identity and access management. SCIM enables identity providers to automatically create, update, and disable users without manual effort.
The protocol seems simple at first — just a few REST endpoints and a JSON schema — but real-world implementations quickly get complex. PATCH handling, strict filter syntax, schema mapping, multi-tenancy, and IDP-specific quirks all introduce subtle but critical challenges.
SCIM appears lightweight, but identity providers expect full compliance with RFC 7643 and RFC 7644. Misinterpreted PATCH payloads, missing metadata, or inconsistent filtering logic are common causes of failed integrations, and most of them don’t surface until after deployment.
This guide covers how to build a SCIM 2.0-compliant API from scratch, including edge cases, validation, and testing, as well as when it makes more sense to use a production-ready SCIM connector like Scalekit.
SCIM in 5 minutes: A developer’s primer
SCIM stands for System for Cross-domain Identity Management. It’s a standard that defines how identity providers (IdPs) like Okta or Azure AD communicate user and group information to external SaaS applications.
The protocol is defined across two key RFCs:
- RFC 7643 defines the core schema (User, Group, EnterpriseUser, etc)
- RFC 7644 defines the REST API protocol used to interact with those objects
At its core, SCIM is a CRUD-style API that operates on standardized resource types, most commonly User and Group. All SCIM requests and responses use the media type application/scim+json, enabling consistent provisioning of users across systems.
To support SCIM 2.0, an app must implement a minimal set of endpoints:
GET /Users → List users with optional filtering/pagination
POST /Users → Create a new user
GET /Users/{id} → Retrieve a specific user
PATCH /Users/{id} → Partially update user attributes
DELETE /Users/{id} → Deactivate or delete a user
The protocol includes specific expectations for:
- HTTP status codes (e.g., 201 Created, 409 Conflict, 400 Bad Request)
- Schema compliance (responses must include SCIM-defined fields like schemas, id, and meta)
- Error format standardization
- Filtering using a restricted common language query: userName eq "alice@example.com"
SCIM is not just about exposing CRUD operations — it enforces strict schema and behavior contracts, and identity providers will reject or fail to sync if those aren't followed exactly.
Setting up your SCIM server
A SCIM server is just an HTTP API that conforms to RFC 7644, exposing API endpoints for automatic provisioning and lifecycle management of user accounts.
Choose a backend framework that gives you control over routing, JSON handling, and middleware. Popular choices include Express (Node.js), Flask or FastAPI (Python), and Go’s standard net/http.
At minimum, your server needs to expose critical Users endpoints under a versioned path like /scim/v2:
Authentication is required for all SCIM endpoints. Identity providers support:
- Basic auth (Okta often sends credentials via header)
- Bearer tokens (Azure AD SCIM integrations require OAuth)
Use a secure mechanism to verify these credentials on every request, and reject unauthenticated access with a 401 Unauthorized status.
For multi-tenant applications, route requests to the correct organization context based on a shared secret, subdomain, or token-based mapping. Identity providers don’t pass tenant info explicitly, so you’ll need to infer it from the request headers or credentials.
Structuring your SCIM user schema
The SCIM User object is defined in RFC 7643 and includes a fixed set of common attributes that must be returned in every user response. Your API responses must match this structure exactly, or identity providers will reject the payload.
A minimal SCIM-compliant user object includes fields like:
Your internal user model may not have all these fields. You’ll need to:
- Map SCIM fields like userName, emails, and active to internal attributes (e.g., email, status, user_id)
- Generate or store SCIM-specific fields like meta.created and meta.lastModified
- Handle optional fields gracefully — SCIM clients may omit name, phoneNumbers, or address
For extensibility, SCIM supports extension schemas via the schemas array, allowing enterprise-specific fields and additional custom attributes. These are typically used for tenant-specific attributes, like department codes or external system IDs.
Implementing GET, POST, DELETE: The CRUD foundation
SCIM’s CRUD operations follow a REST-style interface, but the behavior expected by identity providers is strict and predictable. Your implementation must fully conform to the SCIM 2.0 spec, or provisioning may silently fail.
GET /Users
The GET /Users API should list collections of users based on filter query parameters, supporting both pagination and sorting.
List users with support for:
- Pagination via startIndex (1-based) and count
- Filtering via the SCIM filter language (userName eq "john.doe@example.com")
- Total results count via the totalResults field in the response
Example response structure:
POST /Users
Create a new user in your system using the SCIM-compliant request body. The userName field is required and must be unique. You must return the full user object, including metadata and the generated id. A POST /Users sample request must conform exactly to the SCIM common user schema expectations and return a fully-formed response body.
Return status: 201 Created
The response should match the structure of GET /Users/{id}, including metadata.
DELETE /Users/{id}
Deletion can be interpreted in two ways:
- Hard delete: Physically remove the user from your database by a hard delete request from the system
- Soft delete: Deactivating the active users field as active: false, but retaining them for audit or compliance
Most identity providers tolerate either approach, but your implementation should be consistent. If you soft-delete, update the active attribute, and respond with 200 OK.
If the user doesn’t exist, return 404 Not Found.
Real-world SCIM integrations often break at this layer due to incorrect pagination, missing metadata, or inconsistent field mapping. It’s critical to validate every response against the spec.
PATCH operations: The real headache
PATCH is the most complex part of the SCIM protocol. Unlike a full PUT, SCIM's PATCH allows partial updates using a structured array of operations, and every identity provider constructs these requests a bit differently.
Each SCIM PATCH request includes:
- Schemas: Always includes urn:ietf:params:scim:api:messages:2.0:PatchOp, along with operations that modify individual attributes inside the resource objects.
- Operations: An array of operations using add, remove, or replace
🚨Be careful when handling replace operations, many IDPs send the entire resource object even for small changes to specific fields.
Example: Replace a user’s primary email
Common edge cases to handle:
- Nested paths like name.givenName or emails[type eq "work"].value
- Multi-valued attributes (emails, phoneNumbers)
- Full-object replaces with no path (some IDPs do this)
- No-op operations or unknown fields (should return 400 Bad Request)
- Case-insensitive attribute handling (SCIM fields must not be treated as case-sensitive)
- add ops must merge arrays instead of replacing them entirely
Response behavior:
- Return updated resource with 200 OK
- Include full user object — partial responses are discouraged
- Invalid operations should not partially apply; reject the entire request
Filtering, sorting, and pagination logic
SCIM clients rely on query parameters like filter, sortBy, and count to fetch subsets or sort collections of users during synchronization. Your API must interpret these parameters precisely, or risk failed or incomplete provisioning.
Filtering
SCIM defines a restricted query language for filters:
- Equality: userName eq "alice@example.com"
- Contains: emails.value co "example.com"
- Logical operators: and, or (some IDPs use them inconsistently)
You must parse the filter query parameter from the URL and apply it to your internal user store.
Example:
GET /Users?filter=userName eq "alice@example.com"
Always validate the filter query parameter and restrict it to known SCIM fields to avoid injection risks.
Sorting
Support the sortBy and sortOrder parameters:
GET /Users?sortBy=userName&sortOrder=descending
Invalid or unsupported query parameters should return a proper SCIM error response with 400 Bad Request.
Pagination
Support startIndex (1-based) and count:
GET /Users?startIndex=1&count=100
Response must include:
- totalResults: number of matching users
- startIndex: where the page begins
- itemsPerPage: number of users returned
- Resources: array of user objects
Example:
Security and authentication best practices
SCIM endpoints manage account creation and deletion, meaning they sit at a sensitive point in your infrastructure. Exposing them without proper security is a serious risk.
Always use HTTPS
All SCIM traffic must be served over HTTPS. Identity providers like Okta and Azure AD reject non-TLS endpoints and may not attempt provisioning if the server is not HTTPS-secured.
Authenticate every request
Most identity providers support:
- Basic auth: Okta sends a pre-shared Authorization: Basic base64(client_id:secret)
- Bearer token auth: Azure AD and Google Workspace typically use OAuth 2.0 Bearer tokens in the Authorization header
For basic auth, validate the credentials against your internal tenant mapping. For bearer tokens, verify:
- Signature using the provider’s public keys (JWKS)
- Audience and issuer claims
- Expiry time (exp)
Reject unauthorized requests with a 401 Unauthorized. Do not expose implementation details in error messages.
Enforce rate limits
To prevent abuse or accidental overload:
- Set per-tenant rate limits (e.g., 100 requests/minute)
- Return 429 Too Many Requests with Retry-After header
Whitelist IPs where required
Use IP whitelisting if required by your IDP or internal policy.
Log safely and intentionally
Log:
- The operation performed (PATCH, DELETE, etc.)
- Requestor identity (if available)
- Response time and status code
Do not log:
- Full SCIM payloads with PII (emails, names, etc.)
- Authorization headers or tokens
🚨When returning errors, limit exposure; your response body should include only the SCIM-defined fields like detail and status, never stack traces or internal exceptions.
Protect against replay and injection
Harden input handling:
Use short-lived Bearer tokens to limit exposure and enforce strict validation on fields like filter and sortBy. Only allow expected attribute names and sanitize all inputs to prevent injection or misuse.
Testing your SCIM endpoint like an IDP would
SCIM integrations often break not because of missing features, but because of subtle spec mismatches. Testing your SCIM server as if you were Okta or Azure AD is the only reliable way to catch these issues before production. Testing SCIM servers with real-world sample requests helps surface protocol mismatches before identity providers begin automatic provisioning in production.
Test the full provisioning flow
Simulate these operations in order:
- Create user – Send a POST /Users with required fields
- Update user – Use PATCH /Users/{id} with array-based ops
- List users – Test GET /Users with pagination and filters
- Delete user – Call DELETE /Users/{id} and validate response
- Fetch user – Verify GET /Users/{id} returns correct format
Use tools like:
- curl for raw HTTP testing
- Postman for visual testing and request chaining
- SCIM Gateway (from GitHub) to simulate SCIM clients
- Okta SCIM Connector Builder for end-to-end simulation
Validate with real payloads
Okta and Azure AD both use specific PATCH payload structures that may include:
- emails[type eq "work"].value paths
- Full object replaces via replace with no path
- Empty Operations arrays (which must be handled gracefully)
Test these real-world cases against your server and log exactly what’s received and returned. Validate real-world Okta and Azure AD flows, including PATCH structures that replace current attributes or add new attribute values.
Test error conditions
Return appropriate status codes for these scenarios:
- 400 on invalid schema or unsupported fields
- 401 on auth failure
- 409 if trying to create a user that already exists
- 429 for rate limiting
- 500 for unexpected server failures (with minimal response body)
Ensure error responses include the SCIM error schema:
Making it production-ready and multi-tenant
SCIM integrations are rarely one-off. Once you onboard one enterprise customer, others will follow, each with their own identity provider, provisioning rules, and expectations. Your SCIM server must be built for repeatable, tenant-aware provisioning. For multi-tenant environments, you must map incoming SCIM calls to the correct enterprise organization based on auth credentials or subdomains.
Route requests by tenant context
Identity providers don’t send explicit tenant IDs. You must infer the target organization by:
- Mapping basic auth credentials (client ID/secret → org)
- Parsing bearer tokens and resolving tenant from claims
- Matching subdomain or endpoint path (e.g., /scim/org-123/Users)
Ensure all operations are scoped correctly. A PATCH request from Okta Org A should never modify users from Org B. Your SCIM server must support tenant-specific logic, enforcing role mappings, domain restrictions, and validation of access levels assigned to user accounts.
Apply org-specific provisioning logic
Enterprise customers often need tenant-specific mappings, like default roles, restricted email domains, or required custom attributes. Your SCIM server must allow for flexible configuration per tenant to meet these demands.
Add observability for sync health
Track request metrics (method, status, latency) and expose KPIs like provisioning success rate and patch error rate per org to detect issues early.
Harden your deployment
- Deploy behind an API gateway or ingress controller
- Use retries with backoff on IDP-side failures
- Load test with bulk operations (e.g., 1,000 user creates)
When to use a SCIM connector instead of building your own
Building a SCIM 2.0 endpoint gives full control but adds long-term costs: spec compliance, IDP-specific testing, edge case debugging, and protocol maintenance.
A SCIM connector abstracts away the need to build and maintain your own SCIM-compliant Client, while providing reliable cross-IDP support at a predictable flat rate. It provides a prebuilt implementation of the SCIM protocol that your app can plug into, often with multi-IDP support and built-in tenant routing.
Here’s how in-house builds compare with Scalekit’s SCIM connector:

⚡ Want to skip protocol debugging and get SCIM working in hours? Explore Scalekit’s SCIM Connector, production-ready out of the box.
Building and scaling a SCIM endpoint that works in production
SCIM integrations don’t fail because teams can’t build APIs — they fail because edge cases never end, the spec is strict, and each identity provider behaves slightly differently. Supporting SCIM 2.0 means more than returning JSON. It means speaking a common language of standardized schemas, filters, and API behavior that scales across Okta, Azure AD, and Google Workspace — without breaking sync or delaying onboarding.
This guide outlined how to build a SCIM endpoint from scratch: compliant routes, schema mapping, PATCH logic, security, and multi-tenant routing. If you’re building in-house, this is your blueprint. But if you’re tight on time or tired of integration bugs, you don’t have to go it alone.
Scalekit’s SCIM Connector is production-ready, tested across major IDPs, and built to normalize tenant behavior. Start provisioning users in hours, and stay focused on your product while Scalekit handles the protocol.
FAQs
What is SCIM used for in enterprise SaaS?
SCIM (System for Cross-domain Identity Management) enables identity providers like Okta or Azure AD to automatically provision, update, and deprovision users in SaaS applications. It removes the need for manual account management and ensures users have the right access at the right time.
Why is PATCH the most complex part of SCIM?
SCIM PATCH operations use an array-based syntax with support for add, remove, and replace, often targeting nested or multi-valued attributes. Each IDP structures these requests slightly differently, and handling all cases correctly requires a deep understanding of both the SCIM spec and real-world IDP behavior.
Can I support only some SCIM endpoints?
Technically yes, but most identity providers expect full support for at least GET, POST, PATCH, and DELETE on the /Users resource. Partial implementations may pass initial validation but often break in production sync flows.
What if I have multiple tenants or orgs?
SCIM doesn’t include a tenant ID in requests. You need to infer the tenant context based on the authentication method (e.g., Basic Auth credentials, Bearer token claims) and route each request to the appropriate org. This is critical to avoid cross-tenant data leakage.
Is there a way to test SCIM before deploying?
Yes. You can use tools like Postman, curl, SCIM test harnesses, or simulate real-world identity provider requests using Okta’s SCIM connector builder. Scalekit also provides a built-in test suite for validating SCIM behavior before going live.