Set up Okta SCIM 2.0 provisioning and lifecycle state mapping
This guide is for developers wiring Okta to a SCIM 2.0 service and needing predictable downstream behavior for create, update, suspend, reactivate, and deprovision events. You will end with a working SCIM endpoint, exact Okta provisioning settings to enter, and a lifecycle mapping table your app can implement without ambiguity.
TL;DR — You need two things for reliable Okta SCIM provisioning: a SCIM 2.0 endpoint that responds exactly the way Okta expects, and a deterministic mapping from Okta lifecycle events to your downstream actions. The most common fix is to implement
PATCHforactivechanges and return proper SCIM error/status payloads instead of generic 500s. Reading time: ~5 min
Goal
When you finish, Okta can provision users into your SCIM 2.0 service, update profile attributes, suspend/reactivate access by toggling active, and deprovision users in a way your downstream systems handle consistently and observably.
Prerequisites
- Okta admin access with permission to create or edit an app integration and enable provisioning
- A public HTTPS SCIM base URL, for example
https://scim.example.com/scim/v2 - A bearer token or equivalent auth secret Okta will send to your SCIM service
- A SCIM service that supports at minimum:
GET /ServiceProviderConfig,GET /Schemas,GET /Users,POST /Users,GET /Users/{id},PATCH /Users/{id} - Optional but strongly recommended:
GET /Groups,POST /Groups,PATCH /Groups/{id}if you will push groups curl >= 8— check withcurl --versionjq >= 1.6— check withjq --version- Access to your application logs for the SCIM service
- A test user in Okta you can assign to the app
Steps
Step 1: Expose a SCIM 2.0 base URL that does not redirect
Run these checks against your SCIM base URL:
BASE_URL="https://scim.example.com/scim/v2"
curl -i "$BASE_URL/ServiceProviderConfig"
curl -i "$BASE_URL/Users?startIndex=1&count=1"
You should see HTTP/1.1 200 or HTTP/2 200 and JSON with SCIM media type semantics, not a redirect.
If you see a redirect shape like this, fix your reverse proxy before touching Okta:
HTTP/2 301
location: /login
content-type: text/html
Okta provisioning calls must hit the SCIM API directly; redirects to login pages or docs pages will fail connection tests.
Step 2: Implement the minimum SCIM responses Okta expects
Your service must return concrete payloads. Use these shapes.
GET /ServiceProviderConfig:
{
"schemas": ["urn:ietf:params:scim:schemas:core:2.0:ServiceProviderConfig"],
"patch": {"supported": true},
"bulk": {"supported": false, "maxOperations": 0, "maxPayloadSize": 0},
"filter": {"supported": true, "maxResults": 200},
"changePassword": {"supported": false},
"sort": {"supported": false},
"etag": {"supported": false},
"authenticationSchemes": [
{
"type": "oauthbearertoken",
"name": "Bearer Token",
"description": "Static bearer token",
"primary": true
}
]
}
GET /Users?filter=userName eq "alice@example.com" when found:
{
"schemas": ["urn:ietf:params:scim:api:messages:2.0:ListResponse"],
"totalResults": 1,
"startIndex": 1,
"itemsPerPage": 1,
"Resources": [
{
"id": "9f3c2d6a",
"userName": "alice@example.com",
"active": true,
"name": {"givenName": "Alice", "familyName": "Ng"},
"emails": [{"value": "alice@example.com", "primary": true}]
}
]
}
When not found, return 200 with totalResults: 0, not 404:
{
"schemas": ["urn:ietf:params:scim:api:messages:2.0:ListResponse"],
"totalResults": 0,
"startIndex": 1,
"itemsPerPage": 0,
"Resources": []
}
Success means your endpoints return SCIM JSON and the search endpoint returns an empty list for unknown users.
Step 3: Implement lifecycle mapping in your app
Use this mapping in your SCIM handler so downstream actions are deterministic.
| Okta event | SCIM request you should expect | Downstream action |
|---|---|---|
| Assign user to app | POST /Users | Create account record; mark enabled |
| Profile change | PUT /Users/{id} or PATCH /Users/{id} | Update mapped attributes only |
| Suspend user / unassign access without delete | PATCH /Users/{id} setting active=false | Disable sign-in, revoke sessions/tokens, keep data |
| Reactivate user | PATCH /Users/{id} setting active=true | Re-enable sign-in; do not recreate account |
| Deprovision user | Usually same active=false; sometimes follow-up unassignment logic in your app | Disable access; optionally start retention timer |
| Reassign previously deprovisioned user | Search then PATCH active=true or POST if not found | Restore existing account if present |
Handle PATCH active like this:
{
"schemas": ["urn:ietf:params:scim:api:messages:2.0:PatchOp"],
"Operations": [
{
"op": "Replace",
"path": "active",
"value": false
}
]
}
Success means your service logs show a downstream disable/enable action keyed off active changes, not hard deletes.
Step 4: Add bearer auth validation to your SCIM service
Test your auth behavior exactly.
TOKEN="replace-with-your-secret"
BASE_URL="https://scim.example.com/scim/v2"
curl -i -H "Authorization: Bearer $TOKEN" "$BASE_URL/ServiceProviderConfig"
curl -i "$BASE_URL/ServiceProviderConfig"
Expected good/bad shapes:
HTTP/2 200
content-type: application/scim+json
HTTP/2 401
content-type: application/scim+json
For unauthorized requests, return a SCIM error body:
{
"schemas": ["urn:ietf:params:scim:api:messages:2.0:Error"],
"status": "401",
"detail": "Bearer token missing or invalid"
}
Success means valid token returns 200 and missing token returns 401, never 302.
Step 5: Configure provisioning in Okta
In Okta Admin Console, open your app integration and go to:
Applications → Applications → <your app> → Provisioning → Integration
Enter these literal values:
SCIM connector base URL: https://scim.example.com/scim/v2
Unique identifier field for users: userName
Supported provisioning actions: Import New Users and Profile Updates, Push New Users, Push Profile Updates
Authentication Mode: HTTP Header
Authorization: Bearer replace-with-your-secret
Then click the equivalent of Test API Credentials in that Provisioning → Integration screen.
You should see a successful connection test and your SCIM logs should show a request to /ServiceProviderConfig and usually /Users.
Step 6: Turn on user lifecycle actions in Okta
In the same app, go to:
Applications → Applications → <your app> → Provisioning → To App
Enable these actions if present in your tenant UI:
Create Users
Update User Attributes
Deactivate Users
Reactivate Users
If your app supports group push, also configure:
Applications → Applications → <your app> → Push Groups
Success means the provisioning status for the app shows enabled actions and no credential errors.
Step 7: Assign a test user and watch the exact SCIM traffic
Assign one test user to the app:
Applications → Applications → <your app> → Assignments → Assign → Assign to People
Then tail your SCIM service logs while assigning, suspending, and reactivating the user.
tail -f /var/log/scim/access.log /var/log/scim/app.log
Typical successful sequence shape:
GET /scim/v2/Users?filter=userName%20eq%20%22alice%40example.com%22 200
POST /scim/v2/Users 201
PATCH /scim/v2/Users/9f3c2d6a 200 # active=false
PATCH /scim/v2/Users/9f3c2d6a 200 # active=true
Success means assignment creates the user once, suspend sends active=false, and reactivate sends active=true for the same SCIM id.
Verify it works
Run these checks end to end.
BASE_URL="https://scim.example.com/scim/v2"
TOKEN="replace-with-your-secret"
USER="alice@example.com"
curl -sS -H "Authorization: Bearer $TOKEN" \
"$BASE_URL/Users?filter=userName%20eq%20%22$USER%22" | jq .
Expected after assignment:
{
"totalResults": 1,
"Resources": [
{
"userName": "alice@example.com",
"active": true
}
]
}
Then suspend the user in Okta and rerun the same command. Expected result:
{
"totalResults": 1,
"Resources": [
{
"userName": "alice@example.com",
"active": false
}
]
}
Finally, verify your downstream system reflects the same state: the user cannot sign in while active=false, existing sessions are revoked, and reactivation restores access without creating a duplicate account.
Common pitfalls
Redirecting SCIM requests to a login page
Mistake: your load balancer or app middleware redirects unauthenticated API traffic to /login.
Symptom: Okta connection test fails; curl -I shows 301 or 302 with location: /login.
Fix: return 401 with a SCIM error body for missing/invalid auth on /scim/v2/*.
Returning 404 for filtered user lookup miss
Mistake: GET /Users?filter=... returns 404 when the user does not exist.
Symptom: Okta cannot decide whether to create or match the user; provisioning errors mention user lookup.
Fix: return 200 with SCIM ListResponse and "totalResults": 0.
Not implementing PATCH active
Mistake: your service supports create/update but ignores lifecycle toggles.
Symptom: assignment works, but suspend/reactivate in Okta does nothing downstream or returns 501/405.
Fix: implement PATCH /Users/{id} for path: active and map false to disable, true to re-enable.
Hard-deleting users on deactivation
Mistake: active=false triggers physical deletion in your app database.
Symptom: reactivation creates duplicates, loses audit history, or breaks foreign keys.
Fix: treat active=false as soft deprovision: disable sign-in and revoke sessions, but keep the account row.
Using email as mutable identity without a stable match rule
Mistake: your app keys users only by email and the email changes in Okta.
Symptom: profile update creates a second account or fails to find the original user.
Fix: persist the SCIM id you issued and use userName lookup only for initial match, not as your sole immutable key.
Returning generic HTML errors instead of SCIM error JSON
Mistake: unhandled exceptions bubble up as framework HTML error pages.
Symptom: Okta shows opaque provisioning failures; your logs show 500, but payloads are unreadable.
Fix: map exceptions to SCIM error responses with application/scim+json and a body containing schemas, status, and detail.
This article was written by an AI system and published pending human review. Verify anything you intend to act on.
Have a project in mind?
Get an instant AI price estimate for it, or talk directly to our team.
One email a month on what we learn building with AI