SCIM Provisioning
SCIM (System for Cross-domain Identity Management) is an industry-standard REST/JSON protocol for pushing user and group changes from an identity provider into a downstream application. xAssets implements SCIM 2.0 so identity providers such as Microsoft Entra ID, Okta or OneLogin can provision users and groups directly into xAssets without the daily directory pull.
The SCIM endpoint is a parallel surface to the standard REST API: it lives at /scim/v2/, uses bearer-token authentication, and emits application/scim+json responses conforming to RFC 7643 and RFC 7644.
Prerequisites
- xAssets 7.3 or later (the SCIM endpoint and
SCIMVirtualGrouptable land in the 7.3 upgrade scripts). - Administrator access to xAssets to mint a SCIM token.
- An identity provider that supports SCIM 2.0 push provisioning. Microsoft Entra ID is the primary integration target; Okta and OneLogin also work.
- HTTPS access to your xAssets instance from the identity provider's network.
How SCIM Fits Alongside SSO Auto-Provisioning
xAssets already auto-provisions users on first SSO logon: any Entra user in a permitted group gets a Custodian + UserOptions record created the moment they sign in (see Maintaining User Identities and Entra Auto-Permissions). For pure joiners-via-login that mechanism is enough. SCIM solves the cases SSO auto-provision cannot:
| Lifecycle event | SSO auto-provision | SCIM | Why SCIM matters |
|---|---|---|---|
| Joiner who logs in | Creates Custodian + UserOptions on first login | Creates Custodian (and UserOptions if SCIMCreateMode = Both) |
Both work; SSO path is the simpler one |
| Joiner who never logs in (executive who only owns assets, contractor, asset-pool user) | Never creates them | Creates the Custodian record from the directory push | SCIM is the only path |
| Leaver removed from Entra | No event reaches xAssets — the user keeps their Custodian status, asset assignments and (if it exists) login | Sets Custodian.CustodianStatusID = "Left" and UserOptions.DisabledFlag = 1 within ~40 minutes |
The primary driver. Without SCIM, leaver de-provisioning depends on someone running a script. |
| Group membership change between sessions | Re-evaluated on next login only | Pushed within the next provisioning cycle | Important when group changes drive permissions |
| Asset reassignment on offboarding | Cannot trigger — no event | Triggers via the standard Custodian-status workflow | Operational not theoretical |
| SOC 2 / ISO 27001 access-removal evidence | No audit trail tied to Entra | SCIM writes are attributed to the SCIM token's user, with timestamps and operations | Hard requirement on enterprise procurement |
The clean division is: SSO auto-provision owns UserOptions creation on first login; SCIM owns the Custodian lifecycle and de-provisioning. When a SCIM-provisioned user later logs in via SSO, the SSO auto-provision path finds the existing Custodian (matched by email/UPN) and creates the UserOptions row linked to it.
| Approach | Cadence | Use when |
|---|---|---|
| SSO auto-provision (existing) | At login | Users will sign in and need a login account |
| Graph / LDAP pull (existing) | Daily or every 2 hours | Full directory inventory, customers with strict outbound-only network policies |
| SCIM push (this page) | ~40 minutes (Entra default), under a minute on demand | Timely de-provisioning of leavers, pre-provisioning Custodians for non-login users, compliance-driven access removal |
Most customers will run all three paths concurrently. They do not conflict.
Architecture at a Glance
Entra ID -- bearer token, REST --> https://your-xassets.example.com/scim/v2/*
|
v
SCIMRouter (BusClassNET04)
|
+-----------+-----------+
| |
UserOptions / Custodian UserGroup / SCIMVirtualGroup
The endpoint dispatches every URL under /scim/v2/ through a single SCIM-aware request handler. SCIM-scoped tokens cannot be used against the standard /api/api.ashx surface, and ordinary API tokens cannot be used against /scim/v2/. This separation is enforced server-side.
Generating a SCIM Token
Until the admin UI for token management lands, SCIM tokens are minted by calling the SpecialGenerateSCIMToken SaveSpecial command from any administrator session. The simplest way is a short AMSX script run from the admin console:
print(SaveSpecial("", "SpecialGenerateSCIMToken", {"365"}))
The argument is the token expiry in days. The result has the form special=<GUID> -- copy the GUID portion and paste it into the identity provider's secret token field. Display it once; it is not retrievable later.
Equivalent direct call from xacli:
xacli savespecial SpecialGenerateSCIMToken 365
To revoke a token:
SaveSpecial("", "SpecialRevokeSCIMToken", {"the-token-guid"})
Revocation deletes both the encrypted token file and its underlying APIKey, so the bearer immediately stops working.
Note: SCIM tokens carry a
scim-onlyscope flag. The same token cannot be used against/api/api.ashx-- a leaked SCIM token has no value beyond the SCIM surface.
URL Map
All routes are mounted under /scim/v2/. Path segments are case-insensitive per the SCIM specification.
| Method | Path | Purpose |
|---|---|---|
GET |
/scim/v2/ServiceProviderConfig |
Capability declaration document |
GET |
/scim/v2/ResourceTypes |
Lists User and Group resource types |
GET |
/scim/v2/Schemas |
Lists supported schema URNs |
GET |
/scim/v2/Users |
List users (filter, startIndex, count) |
POST |
/scim/v2/Users |
Create a user |
GET |
/scim/v2/Users/{id} |
Retrieve a user by id |
PUT |
/scim/v2/Users/{id} |
Replace a user |
PATCH |
/scim/v2/Users/{id} |
Partially update a user (Entra's main verb) |
DELETE |
/scim/v2/Users/{id} |
Soft-delete a user (sets status to "Left", does not hard-delete) |
GET |
/scim/v2/Groups |
List groups |
POST |
/scim/v2/Groups |
Create a group |
GET |
/scim/v2/Groups/{id} |
Retrieve a group with members |
PUT |
/scim/v2/Groups/{id} |
Replace group including membership |
PATCH |
/scim/v2/Groups/{id} |
Add or remove members |
DELETE |
/scim/v2/Groups/{id} |
Remove group; members preserved |
Resource ids are integers for users and a mix of UserGroupCode (permission groups) or VG-<n> (informational groups) for groups -- see "Two-Tier Group Model" below.
For dev or multi-tenant deployments with several databases on the same host, append ?database=<name> to any request. In single-tenant deployments the host header alone is sufficient.
Testing With curl
Set the environment variables once and reuse:
export SCIM_BASE="https://your-xassets.example.com/scim/v2"
export SCIM_TOKEN="paste-the-guid-from-the-mint-step"
Capability discovery
curl -sS "$SCIM_BASE/ServiceProviderConfig" \
-H "Authorization: Bearer $SCIM_TOKEN" \
-H "Accept: application/scim+json"
A successful response declares which SCIM features are supported:
{
"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": "Bearer token issued by xAssets admin" }
]
}
Create a user
curl -sS -X POST "$SCIM_BASE/Users" \
-H "Authorization: Bearer $SCIM_TOKEN" \
-H "Content-Type: application/scim+json" \
-d '{
"schemas": ["urn:ietf:params:scim:schemas:core:2.0:User",
"urn:ietf:params:scim:schemas:extension:enterprise:2.0:User"],
"externalId": "abc-123-entra-objectid",
"userName": "alice@example.com",
"name": { "givenName": "Alice", "familyName": "Adams" },
"active": true,
"emails": [{ "type": "work", "value": "alice@example.com", "primary": true }],
"title": "Senior Engineer",
"urn:ietf:params:scim:schemas:extension:enterprise:2.0:User": {
"employeeNumber": "E4821",
"department": "Engineering"
}
}'
Response is 201 Created with the full SCIM User resource and a Location header pointing at /scim/v2/Users/<id>. The id is an integer that maps to Custodian.CustodianID server-side.
List users with a filter
curl -sS "$SCIM_BASE/Users?filter=userName+eq+%22alice%40example.com%22&count=10" \
-H "Authorization: Bearer $SCIM_TOKEN"
Filters supported in this release: eq, ne, co (contains), sw (starts with), pr (present), combined with and. or, parentheses and the broader filter grammar return 501 invalidFilter; identity providers fall back to full enumeration when they receive that response.
Patch a user (deactivate)
curl -sS -X PATCH "$SCIM_BASE/Users/4821" \
-H "Authorization: Bearer $SCIM_TOKEN" \
-H "Content-Type: application/scim+json" \
-d '{
"schemas": ["urn:ietf:params:scim:api:messages:2.0:PatchOp"],
"Operations": [
{ "op": "replace", "path": "active", "value": false }
]
}'
Setting active = false sets Custodian.CustodianStatusID to "Left" and UserOptions.DisabledFlag = 1. The user record is preserved -- SCIM never hard-deletes.
Delete a user
curl -sS -X DELETE "$SCIM_BASE/Users/4821" \
-H "Authorization: Bearer $SCIM_TOKEN"
Returns 204 No Content. Equivalent to PATCH active=false -- the row remains queryable, with status set to "Left".
Create a permission group with members
curl -sS -X POST "$SCIM_BASE/Groups" \
-H "Authorization: Bearer $SCIM_TOKEN" \
-H "Content-Type: application/scim+json" \
-d '{
"schemas": ["urn:ietf:params:scim:schemas:core:2.0:Group"],
"displayName": "xAssets SAM Managers",
"externalId": "entra-group-objectid-here",
"members": [{ "value": "4821" }, { "value": "4822" }]
}'
Because the displayName starts with xAssets, this becomes a permission group -- a row in the UserGroup table. The returned id is the derived UserGroupCode.
Add a member via PATCH
curl -sS -X PATCH "$SCIM_BASE/Groups/SAMMGRS" \
-H "Authorization: Bearer $SCIM_TOKEN" \
-H "Content-Type: application/scim+json" \
-d '{
"schemas": ["urn:ietf:params:scim:api:messages:2.0:PatchOp"],
"Operations": [
{ "op": "add", "path": "members", "value": [{ "value": "4823" }] }
]
}'
Testing With Postman
The standard xAssets API has a logon-then-call sequence that needs scripting to stitch session values together (see REST API Postman Guide -- non-trivial setup with xa-Authorization and Sec headers). SCIM is dramatically simpler: a single bearer token, no logon step, no scripts. The setup mirrors the existing guide's structure so you can reuse the same environment pattern.
Step 1 - Create a Postman Environment
- In the left sidebar, click Environments.
- Click + to create an environment named
xAssets SCIM Production(or similar). - Add the following variables:
| Variable | Initial Value |
|---|---|
scim_base |
https://mycompany.mydomain.xassets.net/scim/v2 |
scim_token |
(paste the GUID from the SCIM token mint step) |
The scim_token value should be marked as Secret in Postman so it does not appear in shared exports.
- Click Save and select the environment from the top-right dropdown.
Step 2 - Create a Collection
- In the left sidebar, click Collections.
- Click + to create a new collection named
xAssets SCIM. - Right-click the collection and choose Edit.
Step 3 - Configure Collection-Level Auth and Headers
This is the key shortcut: set authentication and content negotiation once at the collection level so every request inherits both.
- On the collection's Authorization tab:
- Type:
Bearer Token - Token:
{{scim_token}}
- Type:
- On the collection's Headers tab (if your Postman version puts headers under Variables + a header preset, the same fields apply):
| Key | Value |
|---|---|
Accept |
application/scim+json |
Content-Type |
application/scim+json |
- Click Save.
Unlike the standard API there is no post-response script and no logon request -- the SCIM bearer is long-lived and self-contained.
Step 4 - Add Requests
Inside the collection, create requests that all leave Authorization and Headers set to Inherit auth from parent / Inherit headers from parent. The four most useful starter requests:
| Name | Method | URL |
|---|---|---|
ServiceProviderConfig |
GET | {{scim_base}}/ServiceProviderConfig |
Users - List |
GET | {{scim_base}}/Users?count=10 |
Users - Create |
POST | {{scim_base}}/Users |
Users - Patch Active False |
PATCH | {{scim_base}}/Users/{{user_id}} |
For the Create body, paste the JSON shown earlier under "Create a user" into the request's Body tab (set type to raw, format to JSON).
For the Patch request, use the patch body shown earlier and put the user id either directly into the URL or in a user_id environment variable populated from a previous response (a one-line post-response script: pm.environment.set("user_id", pm.response.json().id);).
Step 5 - Send the First Request
Click Send on ServiceProviderConfig. You should get a 200 response with the capability document. If you get 401 the token is wrong or revoked; if 403, you pasted a non-SCIM token.
Running a Sequence
- Click your xAssets SCIM collection.
- Click Run collection.
- Order the requests:
ServiceProviderConfig, thenUsers - Create, thenUsers - Patch Active False. - Click Run xAssets SCIM.
This drives a complete provision-then-deactivate cycle in a few seconds, which is the same shape Entra performs every 40 minutes in production.
SSL Certificate Verification
If you are connecting to a development environment with a self-signed certificate, turn off SSL certificate verification in Postman's Settings (gear icon). This is rarely needed for production hosted instances.
Microsoft Entra ID Configuration
In the Microsoft Entra admin centre:
- Navigate to Enterprise applications, choose or create the xAssets application.
- Open Provisioning, set the provisioning mode to Automatic.
- Under Admin Credentials:
- Tenant URL:
https://your-xassets.example.com/scim/v2 - Secret Token: the GUID from the SCIM token mint step
- Tenant URL:
- Click Test Connection. Entra issues
GET /scim/v2/ServiceProviderConfigand expects a 200 response. - Under Mappings, leave the default User and Group attribute mappings unless you have a specific reason to customise. The defaults map cleanly to the xAssets SCIM implementation.
- Assign the users and groups you want provisioned to the enterprise application.
- Set the provisioning status to On. Entra runs an initial cycle within a few minutes, then continues every 40 minutes by default.
Two-Tier Group Model
xAssets stores SCIM groups in one of two tables depending on the displayName:
| Trigger | Stored in | xAssets visibility |
|---|---|---|
displayName starts with xAssets (configurable via the SCIMGroupPrefix SpecialOption) |
UserGroup table -- a full permission group |
Drives access to forms, queries and menus |
| Any other displayName | SCIMVirtualGroup table -- an informational group |
Tracked on the Custodian record only; does not affect permissions |
The membership for both tiers is stored as a CSV on Custodian.UserGroups. A user in two permission groups (ADMINS, SAMMGRS) and two informational groups (VG-47, VG-103) has UserGroups = "ADMINS,SAMMGRS,VG-47,VG-103". Their UserOptions.UserGroup (the alphabetically-first permission code) becomes ADMINS.
This design means a tenant with hundreds of Entra groups but only a handful of xAssets-relevant ones gets a curated UserGroup table -- the rest survive as round-trippable SCIM resources without polluting the permission catalogue.
User Mapping
A SCIM User resource maps onto two xAssets tables:
| SCIM field | xAssets target |
|---|---|
id (integer) |
Custodian.CustodianID |
externalId |
Custodian.ADSPath (prefixed entra:) |
userName |
UserOptions.UserID if present, otherwise Custodian.UserID or Custodian.EMail |
name.givenName, name.familyName |
Split from Custodian.CustodianName (best-effort -- xAssets stores a single name field) |
displayName |
UserOptions.FullName or Custodian.CustodianName |
emails[type=work].value |
UserOptions.Email and Custodian.EMail |
phoneNumbers[type=work\|mobile].value |
Custodian.Phone / Custodian.Mobile |
active |
UserOptions.DisabledFlag (inverted) and Custodian.CustodianStatusID |
title |
Custodian.PostTitle |
enterprise.employeeNumber |
Custodian.PostNumber |
enterprise.department |
Custodian.DepartmentID (department auto-created on demand) |
enterprise.manager.value |
Custodian.ManagerCustodianID (FK lookup) |
By default, SCIM POSTs create a Custodian row only — not a UserOptions login account. The existing SSO auto-provision path creates the UserOptions row on first login and links it to the Custodian by matching email/UPN. The two paths share responsibility cleanly:
- SCIM owns the Custodian record (presence, status, ADSPath, departments, manager FK, group memberships).
- SSO auto-provision owns the UserOptions record (login credential, FullName, Email).
A user who is SCIM-provisioned but never logs in stays as a Custodian-only record — correct for asset-owning identities that don't need an xAssets account. A user who later logs in via SSO automatically gets the matching UserOptions row, with the SCIM-managed status carried across (a SCIM-deactivated user is denied login by the standard DisabledFlag check that SCIM DELETE / active=false already set).
Setting SCIMCreateMode = Both
For deployments where SCIM is the primary identity feed and users must have a usable login account before their first sign-in (e.g. SCIM-only without SSO, or pre-provisioning admin accounts), set the SCIMCreateMode SpecialOption to Both. SCIM POSTs will then create the Custodian and a paired UserOptions row in one operation:
| Field on UserOptions | Value |
|---|---|
UserID |
SCIM userName |
FullName |
SCIM displayName (or givenName + familyName if displayName is absent) |
Email |
SCIM work email |
UserGroup |
USERS (default; the alphabetically-first permission group code overrides this on the next group-membership change) |
CustodianID |
FK to the Custodian row created in the same POST |
DisabledFlag |
mirrors SCIM active (false → 1, true → 0) |
| Password | auto-generated and stored encrypted in CustomerDataPrivate (same path used by SSO first-login) |
Behaviour is idempotent: re-POSTing the same externalId does not create a second UserOptions row. If a user was provisioned under Custodian mode and the option is later flipped to Both, the next POST or PATCH that touches that user fills in the missing UserOptions row.
To set the option:
INSERT INTO SpecialOption (SpecialOptionCode, SpecialOptionName, OptionValue, SpecialOptionClass)
VALUES ('SCIMCreateMode', 'SCIM Create Mode', 'Both', 'SCIM')
Or via the standard SpecialOption admin UI under Administration → Settings. Default is Custodian — leave it that way unless you know you need the UserOptions row to exist before first login.
Error Responses
Every error returns a standard SCIM error envelope:
{
"schemas": ["urn:ietf:params:scim:api:messages:2.0:Error"],
"status": "404",
"detail": "User not found",
"scimType": "invalidValue"
}
| Status | scimType |
Meaning |
|---|---|---|
| 401 | invalidCredentials |
Missing, malformed or unrecognised bearer token |
| 403 | insufficientScope |
Token authenticated but lacks the scim-only scope |
| 400 | invalidValue |
Required field missing or value rejected (e.g. userName not supplied on POST) |
| 400 | invalidPath |
PATCH operation targets a path not in the supported whitelist |
| 400 | invalidSyntax |
Body is empty or not valid JSON |
| 404 | -- | Unknown resource id or unknown URL |
| 409 | uniqueness |
Duplicate externalId or userName collision |
| 413 | -- | Request body exceeds 1 MB |
| 501 | invalidFilter |
Filter uses an unsupported operator (e.g. or, parentheses) |
Troubleshooting
| Symptom | Likely cause |
|---|---|
| Entra "Test Connection" returns 401 | The Secret Token field contains an old or revoked GUID. Mint a new token and paste again. |
| Entra reports 403 on every request | A non-SCIM API token was pasted into the SCIM secret field. Use SpecialGenerateSCIMToken, not SpecialGenerateAPIToken. |
| Users appear in xAssets without a department | The Entra user has no department attribute, or the mapping has been removed in the Entra Provisioning Mappings UI. |
| Group membership changes don't appear | Entra schedules group reconciles on its own cadence -- typically within 40 minutes. Trigger an on-demand cycle from the Provisioning page. |
UserGroup table contains unexpected rows |
A group was created with the displayName prefix xAssets. Either rename the Entra group or change the SCIMGroupPrefix SpecialOption. |
| 500 with "potentially dangerous Request.Path value" | The client sent a colon (:) in the URL path. Resource ids no longer use colons -- update to the latest IdP connector or strip the prefix client-side. |
Related Articles
- SSO Introduction and Setup -- enabling browser-based SSO alongside SCIM
- Azure User Groups -- group-to-permission mapping for SSO logins
- Maintaining User Identities -- how local user records work in an SSO environment
- Azure Direct Integration -- the daily Graph pull (complementary to SCIM push)
- REST API Overview -- the standard
/api/api.ashxsurface (separate from SCIM) - API Tokens -- scoped tokens for non-SCIM workflows