SCIM
May 1, 2025

How to build a SCIM endpoint from scratch (And when to use a SCIM connector instead)

Hrishikesh Premkumar
Founding Architect

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.

💡 Scalekit implements the full SCIM protocol spec out-of-the-box, including edge cases, IDP-specific handling, and SCIM-compliant client features so that you can skip low-level spec-wrangling.

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:

// Node.js / Express example app.get('/scim/v2/Users', listUsers); // GET users with optional filters app.post('/scim/v2/Users', createUser); // POST new user app.patch('/scim/v2/Users/:id', patchUser); // PATCH for partial updates app.delete('/scim/v2/Users/:id', deleteUser); // DELETE or deactivate

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.

🔁 Scalekit handles tenant routing, authentication, and request isolation internally if you're using its SCIM connector.

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:

{ "schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"], "id": "user-123", "userName": "alice@example.com", "name": { "givenName": "Alice", "familyName": "Doe" }, "emails": [ { "value": "alice@example.com", "primary": true } ], "active": true, "meta": { "resourceType": "User", "created": "2024-01-01T12:00:00Z", "lastModified": "2024-01-02T09:00:00Z" } }

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.

🔧 Scalekit’s SDK supports dynamic schema mapping and transformation per tenant, so your internal model can stay clean while still responding with SCIM-compliant structures.

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:

{ "schemas": ["urn:ietf:params:scim:api:messages:2.0:ListResponse"], "totalResults": 1, "startIndex": 1, "itemsPerPage": 1, "Resources": [ /* user objects */ ] }

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.

📦 Need a shortcut? Scalekit’s SCIM connector includes compliant CRUD behavior across all supported IDPs, with pagination and filtering handled automatically.

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

{ "schemas": ["urn:ietf:params:scim:api:messages:2.0:PatchOp"], "Operations": [ { "op": "replace", "path": "emails[type eq \"work\"].value", "value": "new.email@example.com" } ] }

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

🔁 PATCH is the most common source of SCIM provisioning bugs, especially across different IDPs. 🛡️ Scalekit’s SCIM connector normalizes PATCH behavior across identity providers like Google, Okta, and Azure AD, so you don’t have to manually handle their quirks.

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:

{ "totalResults": 256, "startIndex": 1, "itemsPerPage": 100, "Resources": [ /* users */ ] }

🚨 Incorrect pagination metadata breaks provisioning for Azure AD and Google Workspace, which expect exact adherence to the SCIM pagination schema. 📦 Scalekit handles all filtering, sorting, and pagination logic internally, with response validation included, removing the need for manual filter parsers or sort handlers.

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.

✅ Scalekit handles all of the above internally — from token validation to audit logging — to help teams stay secure without writing custom middleware.

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:

  1. Create user – Send a POST /Users with required fields
  2. Update user – Use PATCH /Users/{id} with array-based ops
  3. List users – Test GET /Users with pagination and filters
  4. Delete user – Call DELETE /Users/{id} and validate response
  5. 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:

{ "schemas": ["urn:ietf:params:scim:api:messages:2.0:Error"],  "detail": "Invalid filter syntax",  "status": "400" }

🧪 Scalekit provides a built-in SCIM test suite that runs IDP-style requests against your config and surfaces common integration failures before they reach production.

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)

🔁 Scalekit’s SCIM connector handles multi-tenant mapping, per-org schema extensions, and provides built-in dashboards to track provisioning metrics across customers.

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.

No items found.
On this page
Share this article
Start scaling
into enterprise

Acquire enterprise customers with zero upfront cost

Every feature unlocked. No hidden fees.
Start Free
$0
/ month
3 FREE SSO/SCIM connections
Built-in multi-tenancy and organizations
SAML, OIDC based SSO
SCIM provisioning for users, groups
Unlimited users
Unlimited social logins
SCIM

How to build a SCIM endpoint from scratch (And when to use a SCIM connector instead)

Hrishikesh Premkumar

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.

💡 Scalekit implements the full SCIM protocol spec out-of-the-box, including edge cases, IDP-specific handling, and SCIM-compliant client features so that you can skip low-level spec-wrangling.

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:

// Node.js / Express example app.get('/scim/v2/Users', listUsers); // GET users with optional filters app.post('/scim/v2/Users', createUser); // POST new user app.patch('/scim/v2/Users/:id', patchUser); // PATCH for partial updates app.delete('/scim/v2/Users/:id', deleteUser); // DELETE or deactivate

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.

🔁 Scalekit handles tenant routing, authentication, and request isolation internally if you're using its SCIM connector.

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:

{ "schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"], "id": "user-123", "userName": "alice@example.com", "name": { "givenName": "Alice", "familyName": "Doe" }, "emails": [ { "value": "alice@example.com", "primary": true } ], "active": true, "meta": { "resourceType": "User", "created": "2024-01-01T12:00:00Z", "lastModified": "2024-01-02T09:00:00Z" } }

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.

🔧 Scalekit’s SDK supports dynamic schema mapping and transformation per tenant, so your internal model can stay clean while still responding with SCIM-compliant structures.

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:

{ "schemas": ["urn:ietf:params:scim:api:messages:2.0:ListResponse"], "totalResults": 1, "startIndex": 1, "itemsPerPage": 1, "Resources": [ /* user objects */ ] }

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.

📦 Need a shortcut? Scalekit’s SCIM connector includes compliant CRUD behavior across all supported IDPs, with pagination and filtering handled automatically.

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

{ "schemas": ["urn:ietf:params:scim:api:messages:2.0:PatchOp"], "Operations": [ { "op": "replace", "path": "emails[type eq \"work\"].value", "value": "new.email@example.com" } ] }

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

🔁 PATCH is the most common source of SCIM provisioning bugs, especially across different IDPs. 🛡️ Scalekit’s SCIM connector normalizes PATCH behavior across identity providers like Google, Okta, and Azure AD, so you don’t have to manually handle their quirks.

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:

{ "totalResults": 256, "startIndex": 1, "itemsPerPage": 100, "Resources": [ /* users */ ] }

🚨 Incorrect pagination metadata breaks provisioning for Azure AD and Google Workspace, which expect exact adherence to the SCIM pagination schema. 📦 Scalekit handles all filtering, sorting, and pagination logic internally, with response validation included, removing the need for manual filter parsers or sort handlers.

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.

✅ Scalekit handles all of the above internally — from token validation to audit logging — to help teams stay secure without writing custom middleware.

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:

  1. Create user – Send a POST /Users with required fields
  2. Update user – Use PATCH /Users/{id} with array-based ops
  3. List users – Test GET /Users with pagination and filters
  4. Delete user – Call DELETE /Users/{id} and validate response
  5. 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:

{ "schemas": ["urn:ietf:params:scim:api:messages:2.0:Error"],  "detail": "Invalid filter syntax",  "status": "400" }

🧪 Scalekit provides a built-in SCIM test suite that runs IDP-style requests against your config and surfaces common integration failures before they reach production.

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)

🔁 Scalekit’s SCIM connector handles multi-tenant mapping, per-org schema extensions, and provides built-in dashboards to track provisioning metrics across customers.

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.

No items found.
Ship Enterprise Auth in days