SCIM 2.0
PurpletGo exposes a SCIM 2.0 (System for Cross-domain Identity Management) endpoint so your identity provider can sync employee records and trigger deprovisioning the moment a user is removed there.
SCIM is the recommended integration method for organisations using Okta, Azure AD, JumpCloud, OneLogin, or Ping Identity as their source of truth for user lifecycle management.
Base URL
https://api.purpletgo.com/scim/v2All SCIM endpoints are relative to this base URL. Note this sits outside the regular /api and /api/v1 prefixes - it's a separate router mounted specifically for SCIM traffic.
Authentication
SCIM uses Bearer token authentication. Generate a dedicated SCIM token from the PurpletGo dashboard - it's separate from regular API keys and only works against these SCIM endpoints.
Generate a SCIM token
- Go to Settings → Integrations → SCIM
- Click Generate SCIM Token
- Copy the token - it is shown only once
Configure your IdP
Paste the base URL and token into your IdP's SCIM provisioning settings. Each IdP uses slightly different field names:
| IdP | Base URL field | Token field |
|---|---|---|
| Okta | SCIM Connector Base URL | API Token |
| Azure AD | Tenant URL | Secret Token |
| JumpCloud | Base URL | Token |
| OneLogin | SCIM Base URL | OAuth Bearer Token |
| Ping Identity | SCIM Base URL | Bearer Token |
Enable provisioning actions
Enable at minimum: Create Users, Deactivate Users. Optionally enable Update User Attributes.
A SCIM token grants provisioning access to every employee record in the org. Rotate it immediately if it's exposed - it can be revoked from Settings → Integrations → SCIM without touching other API keys.
Supported endpoints
| Method | Path | Description |
|---|---|---|
GET | /ServiceProviderConfig | Capability advertisement - the only endpoint that doesn't require a Bearer token |
GET | /Users | List employees for the org |
GET | /Users/:id | Get a single employee |
POST | /Users | Provision a user (creates, or updates if the email already exists) |
PATCH | /Users/:id | Update attributes; active: false triggers offboarding |
DELETE | /Users/:id | Deprovision a user |
There's no /Schemas endpoint and no SCIM Groups support - department and role are synced as plain attributes on the User resource, not as group memberships.
User schema and attribute mapping
PurpletGo maps a handful of urn:ietf:params:scim:schemas:core:2.0:User attributes onto its org_employees table:
| SCIM Attribute | PurpletGo field | Notes |
|---|---|---|
userName | email | Used as the unique identifier if no primary email is supplied |
emails[primary] | email | Preferred over userName when both are present |
displayName | full_name | Falls back to name.formatted, then givenName + familyName, then the email |
name.givenName / name.familyName | - | Only used to help derive full_name on create; PurpletGo doesn't store given/family name separately |
title | job_title | |
department | department | Sent as a plain top-level attribute, not a namespaced extension |
active | - | false triggers auto-offboarding; PurpletGo doesn't otherwise persist an active flag on the employee record |
externalId | employee_id | Your IdP's internal ID, kept for correlation |
There's no support for phoneNumbers or a manager-ID attribute - anything else in the request body outside the fields above is accepted but ignored.
Provisioning flow
Creating a user (POST /Users)
Request
POST /scim/v2/Users
Authorization: Bearer scim_live_••••••••••••••••
Content-Type: application/scim+json{
"schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"],
"userName": "jordan.lee@acme.com",
"name": { "givenName": "Jordan", "familyName": "Lee" },
"displayName": "Jordan Lee",
"title": "Senior Engineer",
"department": "Engineering",
"active": true,
"emails": [{ "value": "jordan.lee@acme.com", "type": "work", "primary": true }],
"externalId": "okta_00u1ab2cd3ef4gh5ij6k"
}Response 201 Created
{
"schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"],
"id": "8f4c6d1a-9e2b-4a3f-8c1d-2b7e9f0a1c3d",
"externalId": "okta_00u1ab2cd3ef4gh5ij6k",
"userName": "jordan.lee@acme.com",
"name": { "formatted": "Jordan Lee", "givenName": "Jordan", "familyName": "Lee" },
"displayName": "Jordan Lee",
"emails": [{ "value": "jordan.lee@acme.com", "primary": true, "type": "work" }],
"title": "Senior Engineer",
"department": "Engineering",
"active": true,
"meta": {
"resourceType": "User",
"location": "https://api.purpletgo.com/scim/v2/Users/8f4c6d1a-9e2b-4a3f-8c1d-2b7e9f0a1c3d"
}
}POST /Users is an upsert keyed on email, not a strict create. If a user with that email already exists in the org, PurpletGo updates their name, title, and department rather than rejecting the request - so you won't see a duplicate-user error from a re-sync or a retried request.
Deactivating a user (PATCH /Users/:id)
Setting active to false is the deprovisioning signal PurpletGo looks for. It responds by creating an offboarding for the employee, guarded so a retried or duplicate PATCH won't create a second one.
Request
PATCH /scim/v2/Users/8f4c6d1a-9e2b-4a3f-8c1d-2b7e9f0a1c3d
Authorization: Bearer scim_live_••••••••••••••••
Content-Type: application/scim+json{
"schemas": ["urn:ietf:params:scim:api:messages:2.0:PatchOp"],
"Operations": [
{ "op": "replace", "path": "active", "value": false }
]
}Response 200 OK - the updated user resource, with active: false.
PurpletGo also recognizes the nested form some IdPs send ({ "op": "replace", "value": { "active": false, "displayName": "...", "title": "..." } }) and will apply displayName/title updates alongside the deactivation. Other PATCH paths and add/remove operations are accepted but have no effect.
The offboarding created here starts life the same way a manually-created one does - if your org has approval chains configured, it enters pending_approval before access revocation begins.
Deleting a user (DELETE /Users/:id)
DELETE does not remove the employee record. It runs the exact same "create an offboarding if one isn't already in progress" logic as PATCH active=false - the employee record stays in PurpletGo so its offboarding history and audit trail remain intact. If you need to fully erase a record for data-privacy reasons, that has to be done separately from the PurpletGo dashboard; there's no API for it.
Response 204 No Content
Listing users (GET /Users)
Supports SCIM-style pagination via startIndex and count (max 200, default 100).
GET /scim/v2/Users?startIndex=1&count=25
Authorization: Bearer scim_live_••••••••••••••••Response 200 OK
{
"schemas": ["urn:ietf:params:scim:api:messages:2.0:ListResponse"],
"totalResults": 1,
"startIndex": 1,
"itemsPerPage": 25,
"Resources": [
{
"schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"],
"id": "8f4c6d1a-9e2b-4a3f-8c1d-2b7e9f0a1c3d",
"userName": "jordan.lee@acme.com",
"displayName": "Jordan Lee",
"active": true
}
]
}GET /ServiceProviderConfig advertises filter: { supported: true } for IdP discovery purposes, but the /Users endpoint doesn't currently evaluate filter query expressions - if your IdP sends one, it's silently ignored and every employee page is returned. Rely on startIndex/count for pagination.
Error responses
{
"schemas": ["urn:ietf:params:scim:api:messages:2.0:Error"],
"status": "400",
"detail": "userName / email must be a valid email address"
}| HTTP Status | Meaning |
|---|---|
400 | Malformed request - missing/invalid email, or Operations isn't an array |
401 | Missing or invalid SCIM bearer token |
404 | User ID not found in this organisation |
429 | Rate limit exceeded (120 requests/minute per SCIM endpoint) |
500 | Internal error while processing the request |
There's no 409 case - see the upsert note above.
Related
- Offboarding Initiations - initiations created by SCIM deactivation
- Integrations - manage IdP connections
- Authentication - regular API key authentication