In this article
October 12, 2026
October 12, 2026

What Okta and Entra ID actually send to your SCIM endpoint

Real request bodies from the two directories most enterprise customers use, where they part ways with RFC 7644 and the new interoperability profiles, and how to handle each one without leaving a leaver active.

Explore with AI
Open in ChatGPT
Open in Claude
Open in Perplexity

When an enterprise customer turns on provisioning for your app, their directory starts sending SCIM requests to your endpoint. Most of the time that directory is Okta or Microsoft Entra ID. Each request creates, updates or deactivates someone's account, and the deactivations are the ones that matter most: they are how a person who leaves the company loses access to your product.

If you built your SCIM server from RFC 7644, you built it to a spec that leaves a lot open. Two newer drafts try to close those gaps, and they are stricter than the RFC in ways that matter. Neither Okta nor Entra was built to them. So a server can follow the specs carefully and still reject the requests real customers send, or worse, accept them and do the wrong thing.

This post walks through what Okta and Entra actually send, using the request bodies from their own documentation. For each one, we look at what the RFC and the newer profiles say about it and how to handle it safely. We start with the request that shows the problem most clearly.

Here is how Okta deactivates a user over SCIM, exactly as Okta's own SCIM 2.0 reference documents it:

  
PATCH /scim/v2/Users/23a35c27-23d3-4c03-b4c5-6443c09e7173 HTTP/1.1
User-Agent: Okta SCIM Client 1.0.0
Authorization: <Authorization credentials>

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

There is no path. Okta puts the attribute it wants to change inside value instead. RFC 7644 allows that: path is optional for add and replace.

The SCIM 2.0 Interoperability Profile, a draft in the IETF SCIM working group, does not. It says every PATCH operation MUST carry a path, and that servers MUST reject one without it with a 400 and invalidSyntax. So a server built strictly to the profile answers Okta's most important request with an error, and the person who left the company yesterday stays active.

This post was prompted by FoxAuth's write-up of building a SCIM server to the new profiles, which is worth reading in full. For the broader picture of how providers differ, see our earlier post on SCIM challenges.

Three documents, three answers

Three documents now say what a SCIM request should look like:

  • RFC 7644 (2015), the SCIM protocol itself. It is flexible on purpose, and in places ambiguous.
  • SCIM 2.0 Interoperability Profile (draft-zollner-scim-interop-profile-01, April 2026). It narrows the RFC so that any conforming client works with any conforming server. Among other things, it requires a path on every PATCH operation and says clients MUST use PATCH, not PUT, for updates.
  • IPSIE AL SCIM 2.0 Profile (Draft 00, OpenID Foundation, September 8, 2026). It defines account lifecycle assurance levels for enterprise SaaS and requires conformance to the interoperability profile on top of its own rules.

Both profiles are drafts, and both list an author from Okta. The point is not that one vendor is out of step. It is that the clients your customers already run were built against the RFC, years before anyone wrote the profiles, and they will keep sending the same requests for a while yet.

What Okta sends

Deactivation and reactivation: A patch with no path

This is the request above. For new integrations in the Okta Integration Network (OIN), activating, deactivating and password sync are the only user updates Okta sends as PATCH. Note what does not trigger a request at all: Okta's docs say there is "no deprovisioning event sent for users that are suspended inside Okta."

Group renames use the same path-less shape:

  
{
  "schemas": ["urn:ietf:params:scim:api:messages:2.0:PatchOp"],
  "Operations": [{
    "op": "replace",
    "value": {
      "id": "abf4dd94-a4c0-4f67-89c9-76b03340cb9b",
      "displayName": "Test SCIMv2"
    }
  }]
}
  

How to handle it: accept a path-less replace when value is an object, and apply each key as its own operation. Validate each key against the attributes you declare, and ignore keys you don't support rather than storing them.

Every other profile change: a PUT

The interoperability profile says clients SHALL NOT use PUT. Okta uses PUT for every other profile change. For new OIN integrations, "all updates to a user object are handled using a PUT method request" apart from the three PATCH cases above. For custom integrations built with the App Integration Wizard, every update is a PUT, including deactivation.

Okta builds that PUT from your own data. Per its docs, Okta retrieves the user object, "modifies the attributes that were changed and runs a PUT method request with the new body."

How to handle it: support PUT on /Users/{id}, and return the complete user from GET /Users/{id}. Okta's PUT body is built from your GET response, so an attribute you leave out of GET can come back missing from the PUT.

A password on create

Okta includes a password attribute when it creates a user:

  
{
  "schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"],
  "userName": "test.user@okta.local",
  "name": { "givenName": "Test", "familyName": "User" },
  "emails": [{ "primary": true, "value": "test.user@okta.local", "type": "work" }],
  "displayName": "Test User",
  "locale": "en-US",
  "externalId": "00ujl29u0le5T6Aj10h7",
  "groups": [],
  "password": "1mz050nq",
  "active": true
}
  

Okta says it sends this "even if password sync isn't enabled," as a placeholder for legacy provisioning platforms, and that the value "isn't relevant or sensitive." The IPSIE profile goes further: the identity service MUST NOT send password, and the application MUST NOT support it.

How to handle it: if your users sign in through SSO, drop the attribute without storing or logging it. Don't reject the request.

Credentials: often a token with no "Bearer"

Okta's authentication options depend on how the integration was built:

  • OIN integrations can use a header token sent as Authorization: {API token} with no Bearer prefix, a bearer token sent as Authorization: Bearer {API token}, or the OAuth 2.0 authorization code flow. The OIN submission guide lists those three and notes that Basic authentication isn't supported for SCIM.
  • Custom integrations built with the App Integration Wizard can also use Basic auth or OAuth 2.0 with the client credentials grant.

How to handle it: if you issue static tokens, accept them with or without the Bearer prefix. A recognizable prefix on your tokens makes that unambiguous, and helps secret scanners find leaked ones. If you can, offer client credentials for custom integrations. We covered why in Why SCIM needs OAuth 2.0 client credentials.

What Entra ID sends

Microsoft is unusually open about where Entra's SCIM client departs from the RFC. Its page on known SCIM 2.0 compliance issues lists them, along with a feature flag that changes four of them. We'll come back to the flag.

Capitalized operations

Entra sends "op": "Add", "Replace" and "Remove". Microsoft's SCIM endpoint tutorial tells you directly: "Don't require a case-sensitive match on structural elements in SCIM, in particular PATCH op operation values."

How to handle it: lowercase op before you compare it.

Deactivation with a string instead of a boolean

Without the feature flag, Entra disables a user like this:

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

"False" is a string. A server that checks value === false won't deactivate anyone, and a server that checks truthiness will read a non-empty string as true.

How to handle it: coerce "True" and "False" (any case) to booleans for boolean attributes, and reject anything else with invalidValue.

Group member removal that, read literally, removes everyone

This is the one to test first. Without the flag, Entra removes a single member from a group like this:

  
{
  "schemas": ["urn:ietf:params:scim:api:messages:2.0:PatchOp"],
  "Operations": [{
    "op": "Remove",
    "path": "members",
    "value": [{ "value": "u1091" }]
  }]
}
  

RFC 7644 section 3.5.2.2 says that when remove targets a multi-valued attribute "and no filter is specified, the attribute and all values are removed." The value array plays no part in a remove. A server that follows the RFC to the letter empties the whole group, and every user who got access through that group loses it.

How to handle it: if a remove on members carries a value array, remove only the listed members. Also accept the filtered form the RFC intends, "path": "members[value eq \"...\"]", which is what Entra sends with the flag.

A filter that matches nothing

Entra updates a user's work email with a filtered path:

  
{
  "op": "Replace",
  "path": "emails[type eq \"work\"].value",
  "value": "updatedEmail@microsoft.com"
}
  

If the user has no work email yet, the filter matches nothing. RFC 7644 gives you two readings. One rule says that if the target "specifies an attribute that does not exist, the service provider SHALL treat the operation as an add." The error table defines noTarget for a path whose filter "yields no match." Entra means the first.

How to handle it: for a replace whose filter matches nothing, create the element with the filter's values (here, type: "work") and set the sub-attribute.

Manager in more than one shape

Microsoft's tutorial shows the enterprise manager attribute arriving as an Add whose value is an array holding one object:

  
{
  "op": "Add",
  "path": "manager",
  "value": [{
    "$ref": "http://.../scim/Users/00aa00aa-bb11-cc22-dd33-44ee44ee44ee",
    "value": "00aa00aa-bb11-cc22-dd33-44ee44ee44ee"
  }]
}
  

In RFC 7643, manager is a single complex attribute, not an array. FoxAuth also reports seeing Entra send the manager as a bare ID string, and clear it with an empty-string replace rather than a remove, which it found by running Microsoft's own SCIM validator.

How to handle it: accept an object, an array of one object, or a bare string, and normalize all three to { "value": "<id>" }. Treat an empty string as a removal.

The flag that makes Entra "SCIM compliant"

Appending aadOptscim062020 to the tenant URL changes four behaviors: disabling users, adding a single-value string attribute, replacing multiple attributes, and removing a group member. With the flag, op is lowercase, active is a real boolean, and member removal uses a filtered path.

Three things limit how much you can rely on it:

  1. Your customers have to add it. Microsoft says the new behavior "will become the default behavior over the next few months," but its compliance table still lists the fix date as "TBD." You can't assume any given tenant has it.
  2. It doesn't apply everywhere. Microsoft notes the flag "currently doesn't work with on-demand provisioning." The same app can receive both shapes, depending on whether a change came from the scheduled cycle or from an admin provisioning a user on demand.
  3. Its "compliant" output breaks the interoperability profile. With the flag, Entra stops sending one operation per attribute and sends a single path-less replace:
  
{
  "op": "replace",
  "value": {
    "displayName": "Bjfe",
    "name.givenName": "Kkom",
    "name.familyName": "Unua",
    "urn:ietf:params:scim:schemas:extension:enterprise:2.0:User:employeeNumber": "Aklq"
  }
}
  

That is valid under RFC 7644, which is what Microsoft means by compliant. Under the interoperability profile, it is exactly the path-less PATCH a server MUST reject. Note also the keys: dotted sub-attribute names and a full extension URN, inside value.

How to handle it: support both shapes. When you expand a path-less replace, parse each key as a path, including dotted sub-attributes and URN-qualified extension attributes.

Timing

Entra runs provisioning on a background cycle, typically every 40 minutes; admins can push a single user through with on-demand provisioning. The IPSIE profile says the identity service SHOULD propagate a deactivation within 5 minutes. That gap is between your customer and Microsoft, but it belongs in any answer you give a customer's security team about how fast access is removed. We go deeper on that in SCIM directory sync: a deep dive into deprovisioning.

Credentials

Entra can authenticate with a long-lived secret token, with OAuth 2.0 client credentials (sending the client secret in a Basic header, in the request body, or both), or with workload identity federation, where Entra presents a short-lived signed JWT under the RFC 7523 JWT bearer profile and stores no secret. Microsoft has retired the OAuth authorization code grant for new provisioning connectors.

Workload identity federation is the closest any mainstream directory comes to what IPSIE asks for. It needs your token endpoint to validate JWTs against Entra's published keys.

The IPSIE authentication rule can't be met as written

Section 4.1 of the IPSIE profile says two things about how the directory gets its token:

  • "OAuth 2.0 interactions MUST use the client_credentials grant type with JWT Client Authentication as defined in [RFC7523] section 2.2."
  • "The Identity Service MUST authenticate to the Application's OAuth 2.0 authorization server using HTTP Basic authentication in the Authorization request header. Transmission of client credentials (e.g., client_assertion, client_secret) in the HTTP request body is prohibited."

RFC 7523 section 2.2 sends the signed assertion as a client_assertion parameter in the token request body. HTTP Basic sends a client ID and secret in the header. No client can do both, as FoxAuth points out. The section carries an editor's note saying it "should be expanded," and this is Draft 00, so expect it to change. Until it does, be wary of any SCIM server that claims full conformance with section 4.1.

All of it in one table

Client What arrives What the specs say Safe handling
Okta Deactivate and reactivate as replace with no path and {"active": false} in value Allowed by RFC 7644. Interop profile: server MUST reject with 400 Expand into one operation per key; validate each key against your schema
Okta Profile updates as PUT, built from your GET response (all updates, for custom integrations) Allowed by RFC 7644. Interop profile: clients SHALL NOT use PUT Support PUT; return the complete user from GET
Okta password on create, even with password sync off IPSIE: client MUST NOT send it; server MUST NOT support it Drop it without storing or logging; don't reject the request
Okta Header token as Authorization: {token}, no Bearer IPSIE: OAuth client credentials and a Bearer access token Accept static tokens with or without the prefix; offer client credentials where possible
Entra ID "op": "Add", "Replace", "Remove" RFC examples use lowercase; Microsoft says don't match case-sensitively Lowercase op before comparing
Entra ID "active" set to the string "False" (without the flag) active is a boolean in RFC 7643 Coerce "True"/"False" for boolean attributes; reject other strings
Entra ID Remove on members with a value array and no filter (without the flag) RFC 7644: remove with no filter removes all values Remove only the listed members; also accept the filtered path form
Entra ID replace on emails[type eq "work"].value when no work email exists RFC 7644 allows two readings: treat as add, or noTarget Create the element with the filter's values and set the sub-attribute
Entra ID manager as an array of one object; reportedly also a bare ID, cleared with an empty string RFC 7643: a single complex attribute Normalize all shapes to {"value": id}; treat empty string as removal
Entra ID With aadOptscim062020: one path-less replace with dotted and URN keys Allowed by RFC 7644. Interop profile: server MUST reject with 400 Expand keys as paths, including sub-attributes and extension URNs
Any client remove with no path RFC 7644: fail with 400 and noTarget Always reject. Never guess what to remove

How to build for this

  1. Normalize first, then apply. Turn every request into one internal form before your business logic sees it: lowercase op, coerce boolean strings, expand path-less replace into one operation per key, and treat a remove on members with a value array as removing just those members. Your deprovisioning code then handles one shape instead of six.
  2. Be tolerant about form, strict about danger. Accepting Replace or "False" costs nothing. A remove with no path should still fail with noTarget, as the RFC says, because guessing what to remove is how a profile gets wiped. Never let keys like __proto__ from a request body reach your objects.
  3. Ignore what you don't declare. Check every attribute name against your schema, and drop the ones you don't support instead of storing them.
  4. Keep the raw requests. When a customer says they deprovisioned someone and your app disagrees, the request body is the only thing that settles it.
  5. Test against both vendors. Run Okta's SCIM spec test and Microsoft's SCIM validator. The validator is where FoxAuth found the manager behavior Microsoft's docs don't describe.
  6. Don't claim strict conformance if you aren't strict. A server that accepts the requests in the table above doesn't conform to the interoperability profile, and that is the right trade-off for a server that has to work with Okta and Entra today.

Or don't terminate SCIM yourself

Every item in this post is a reason teams hand the SCIM endpoint to someone else. With WorkOS Directory Sync, WorkOS hosts the endpoint for each customer's directory, and Okta and Entra authenticate to it with a bearer token or OAuth 2.0 client credentials. Your app receives one set of events (dsync.user.created, dsync.user.updated, dsync.user.deleted and the group events) whichever directory is on the other side, and you write your deprovisioning logic once.

When you need to see what a customer's directory actually sent, the WorkOS Dashboard shows SCIM request logs for each directory, with full request and response payloads kept for 30 days and sensitive values redacted.