SCIM API: Building and Testing a SCIM 2.0 Endpoint

Guilliano Molaire Guilliano Molaire 7 min read

Last updated: August 2026

A SCIM API is a REST interface defined by RFC 7644 that an application exposes so identity providers can create, update and deactivate its user accounts. At minimum it serves /Users and /Groups over HTTPS, speaks application/scim+json, supports POST, GET, PATCH and filtering by userName, and returns SCIM-shaped errors. Get those right and Okta and Entra ID will provision into it without either side reading the other’s docs.

That last part is the whole point of the standard, and it is also the trap. Identity providers do not negotiate. They send what the spec says and expect what the spec says back, so “mostly compliant” tends to fail in a way that is hard to see from your own side.

Key takeaways

  • The required surface is smaller than the spec: /Users with POST, GET, PATCH and a userName filter covers most real integrations.
  • PATCH with active: false is the offboarding path. Treat it as a hard lockout, not a soft hide.
  • Return 409 with scimType: uniqueness on duplicate userName, or identity providers retry forever.
  • Filtering, pagination and correct error bodies are where most homegrown implementations break.

The endpoints you actually have to serve

The specification defines a lot. Real identity providers exercise a fraction of it.

Method Path Purpose Needed?
POST /Users Create a user Yes
GET /Users/{id} Fetch one user Yes
GET /Users?filter=userName eq "x" Look up before creating Yes
PATCH /Users/{id} Update attributes, deactivate Yes
PUT /Users/{id} Full replace Sometimes
DELETE /Users/{id} Hard delete Rarely
GET/POST/PATCH /Groups Group sync If you map groups to roles
GET /ServiceProviderConfig Advertise what you support Recommended

Serve these under a versioned base path, conventionally /scim/v2. Content type is application/scim+json on both request and response, though most implementations also accept application/json because plenty of clients send it.

Creating a user

The identity provider almost always looks the user up first, then creates if absent. Your filter endpoint therefore gets hit more than your create endpoint.

POST /scim/v2/Users
Content-Type: application/scim+json
Authorization: Bearer <token>

{
  "schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"],
  "userName": "[email protected]",
  "name": { "givenName": "Dana", "familyName": "Okafor" },
  "emails": [{ "value": "[email protected]", "primary": true }],
  "active": true
}

Respond 201 Created with the full resource, including the id you assigned and a meta block. The id is yours, not theirs; the identity provider stores it and uses it for every subsequent call.

{
  "schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"],
  "id": "2819c223-7f76-453a-919d-413861904646",
  "userName": "[email protected]",
  "active": true,
  "meta": {
    "resourceType": "User",
    "created": "2026-08-06T09:14:22Z",
    "lastModified": "2026-08-06T09:14:22Z",
    "location": "https://api.example.com/scim/v2/Users/2819c223-7f76-453a-919d-413861904646"
  }
}

Duplicate handling is the first thing that breaks. If userName already exists, return 409 with scimType: uniqueness. Returning 400, or 500, or a 200 with the existing user, all cause identity providers to retry or to mark the integration failed.

{
  "schemas": ["urn:ietf:params:scim:api:messages:2.0:Error"],
  "scimType": "uniqueness",
  "detail": "userName already exists",
  "status": "409"
}

Note status is a string in SCIM error bodies, not a number. Small detail, real interop failures.

Filtering: implement one thing well

RFC 7644 defines a full filter grammar with and, or, not, grouping and a dozen operators. You do not need it.

What identity providers actually send, on nearly every provisioning cycle:

GET /scim/v2/Users?filter=userName eq "[email protected]"

Support userName eq and you have covered the overwhelming majority of traffic. emails.value eq and externalId eq are worth adding next. Everything else can return 501 with scimType: invalidFilter rather than pretending.

Responses are wrapped in a list envelope, even for one result:

{
  "schemas": ["urn:ietf:params:scim:api:messages:2.0:ListResponse"],
  "totalResults": 1,
  "startIndex": 1,
  "itemsPerPage": 1,
  "Resources": [ { "id": "2819c223...", "userName": "[email protected]" } ]
}

A zero-result filter is 200 with totalResults: 0 and an empty Resources array. Not 404. Returning 404 for “no match” is one of the most common homegrown bugs, and it makes identity providers conclude your endpoint is broken rather than that the user is absent.

PATCH, and the operation that actually matters

PATCH uses the PatchOp message with a list of operations. Offboarding is one of them:

PATCH /scim/v2/Users/2819c223-7f76-453a-919d-413861904646
Content-Type: application/scim+json

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

Identity providers deactivate rather than delete, so this single request is how access actually gets revoked. Two rules:

Treat active: false as a hard lockout. Existing sessions should be invalidated, API tokens revoked, and any subsequent login refused. A deactivated user who still holds a valid session has not been deprovisioned in any sense an auditor will accept.

Accept the shapes clients really send. The spec allows op values in any case, paths with and without quotes, and value as either a scalar or an object like {"active": false}. Okta and Entra ID differ here. Normalise on the way in rather than rejecting.

Pagination

Requested with startIndex and count, and startIndex is 1-based, not 0-based. Off-by-one here silently skips or repeats a user on every cycle.

GET /scim/v2/Users?startIndex=1&count=100

Cap count at something sane and report the cap honestly in itemsPerPage. Clients respect what you return.

Authentication

RFC 7644 leaves the scheme open. In practice nearly every deployment uses a long-lived bearer token configured in the identity provider, so treat that token as the most sensitive credential you issue: it can create and disable accounts.

  • Serve over TLS only. The token rides in a header on every request.
  • Scope it to users and groups, nothing else. Prefer OAuth client credentials with narrow scopes where supported.
  • Rotate on a schedule, and immediately when staff with access to it leave.
  • Log every provisioning event: who was created, updated or deactivated, when, by which client. This is high-value audit data.
  • Rate-limit and allowlist your identity provider’s ranges where you can.

Testing it

The happy path is the easy part. Compliance failures live in the edges, so test these deliberately:

  1. Create the same userName twice. Expect 409 with scimType: uniqueness.
  2. Filter for a user that does not exist. Expect 200 and totalResults: 0.
  3. PATCH an id that does not exist. Expect 404, not 500.
  4. PATCH active: false, then attempt a login and an API call with an existing token. Both must fail.
  5. PATCH with op as "Replace" and as "replace". Both must work.
  6. Page through more users than your count cap with startIndex=1, then startIndex=101. Nothing skipped, nothing repeated.
  7. Send Content-Type: application/json instead of application/scim+json. Accept it.

Our free SCIM Endpoint Tester runs these from the browser against a live endpoint and shows you the raw response, which is faster than wiring a real identity provider just to discover your error bodies are the wrong shape.

SCIM in Keycloak

Keycloak had no SCIM support for most of its history; teams used community extensions or wrote their own server. Recent releases include a native SCIM 2.0 server as an experimental feature behind a flag, covering core user and group operations rather than the full protocol surface. Experimental means it can change between releases, so check the Keycloak supported features list before you build on it.

If you would rather have SCIM as a supported feature than a preview, Skycloak ships SCIM 2.0 provisioning on managed Keycloak, and the setup guide covers enabling the server and exercising real calls.

Worth keeping straight: SCIM is not how Keycloak reads your on-premises directory. That is user federation. SCIM is for provisioning across organizational boundaries.

Frequently asked questions

Is SCIM a REST API?

Yes. SCIM 2.0 is a REST API over HTTPS with JSON payloads, a defined schema, and a standard set of endpoints and error shapes. The standardisation is what lets any compliant identity provider talk to any compliant application.

What is the minimum SCIM implementation?

/Users with POST, GET by id, GET with a userName eq filter, and PATCH for active. That satisfies most real integrations. Add /Groups when you map groups to roles.

What is the difference between the SCIM client and the SCIM service provider?

The identity provider is the client and your application is the service provider, because your application serves the API. The naming is backwards from how most people say it, and it makes vendor docs much easier to read once you know.

Should SCIM delete users or deactivate them?

Deactivate. Identity providers send PATCH with active: false on offboarding rather than DELETE, which preserves the audit trail. Support DELETE if you like, but do not depend on receiving it.

Why does my SCIM integration keep retrying?

Usually wrong status codes. 404 for an empty filter result and non-409 responses to duplicate userName are the two that cause identity providers to retry or disable the integration.

Do I need to implement the full filter grammar?

No. userName eq covers nearly all traffic. Return 501 with scimType: invalidFilter for anything else rather than silently returning wrong results.

The short version

Ship /Users with create, lookup by filter, and PATCH active. Get the status codes right. Treat deactivation as a real lockout. Everything after that is refinement.

For the protocol background, see what SCIM is and why it matters, and for where it sits next to your login flow, SCIM vs SAML and user provisioning explained.

Want SCIM without building and maintaining the endpoint? See Skycloak pricing.

Tired of running Keycloak yourself?

Skycloak runs real upstream Keycloak for you with a 99.99% SLA. No fork, no lock-in, just managed Keycloak that stays patched and on call so you don't have to.

Guilliano Molaire
Written by
Founder

Guilliano is the founder of Skycloak and a cloud infrastructure specialist with deep expertise in product development and scaling SaaS products. He discovered Keycloak while consulting on enterprise IAM and built Skycloak to make managed Keycloak accessible to teams of every size.

Start Free Trial Talk to Sales
© 2026 Skycloak. All Rights Reserved. Design by Yasser Soliman