SCIM inbound provisioning
Let your HR system or directory be the source of truth, so joiners, movers and leavers happen without anybody remembering. Endpoints, filters, and what maps to what.
SCIM is a standard way for one system to keep accounts in another up to date. Your HR system or directory already knows who joined on Monday, who changed department, and whose last day was Friday. SCIM lets it push those facts here, so a Qorionix account appears, changes and is switched off without anybody having to remember.
The leaver half is what matters. Most organisations create accounts reliably, because somebody is waiting for one; almost nobody removes them reliably, because nobody is waiting for that. An account that still works three months after its owner left is one of the most common findings in a real assessment, and automating that path is the single most effective thing on this page.
The current wiring status, stated plainly
This feature has a history worth knowing, because you may find a document — including one of ours — that describes it wrongly. The SCIM server used to write into a pair of shadow tables of its own. A person provisioned there had no login, no organisation membership, no role and no groups, so a push from Okta or Workday appeared to succeed and produced nothing the sign-in path or an administrator could see.
Real limits remain, and they are in the sections below rather than a footnote: the role every provisioned user receives is fixed, two discovery endpoints the standard defines do not exist, and an unparseable filter returns the wrong HTTP status. None of those stops the feature working; all of them change how you configure your provider.
The endpoints
The base path is /api/v1/scim/v2. Give your provider that as the SCIM base URL.
GET /api/v1/scim/v2/ServiceProviderConfig
GET /api/v1/scim/v2/Users POST /api/v1/scim/v2/Users
GET /api/v1/scim/v2/Users/{id} PUT /api/v1/scim/v2/Users/{id}
PATCH /api/v1/scim/v2/Users/{id} DELETE /api/v1/scim/v2/Users/{id}
GET /api/v1/scim/v2/Groups POST /api/v1/scim/v2/Groups
GET /api/v1/scim/v2/Groups/{id} PUT /api/v1/scim/v2/Groups/{id}
PATCH /api/v1/scim/v2/Groups/{id} DELETE /api/v1/scim/v2/Groups/{id}What /ServiceProviderConfig advertises: patch supported, filtering supported with a maximum of 200 results, bulk not supported, password change not supported, sorting not supported, entity tags not supported, and OAuth bearer token as the only authentication scheme.
Pagination uses the standard parameters. startIndex defaults to 1, count defaults to 100, and 200 is the ceiling. A count above 200 is not an error — it is ignored and you get 100, so a provider asking for 500 a page silently receives far fewer and takes longer rather than failing.
The token, and why the URL has no organisation in it
Every other administrative path in the platform carries your organisation in the URL. These do not, deliberately: an HR system has no user session and cannot refresh one, and a revoked employee must not be able to take the payroll feed down with them by having their own account disabled. The only credential accepted here is a dedicated SCIM bearer token, which is also what tells us which organisation the request belongs to. An ordinary user session does not authenticate here at all.
GET /api/v1/organizations/{orgID}/scim/tokens sso:read
POST /api/v1/organizations/{orgID}/scim/tokens sso:write
DELETE /api/v1/organizations/{orgID}/scim/tokens/{id} sso:delete
400 scim token: organization id is required
400 scim token: name is requiredThese are gated on the SSO permission family rather than something weaker, because a SCIM token can create users and memberships. An ungated mint let any member of an organisation provision themselves an administrator without ever leaving the tenant.
- The token format
zqscim_followed by 43 characters — 32 bytes of cryptographic randomness. The prefix exists so a leaked secret is recognisable in a log or a pasted snippet without resolving it against the database.- Shown exactly once
- The full token is returned by the mint call and never again; only its hash and first 14 characters are kept. Put it into your provider immediately. If you lose it, mint a new one and revoke the old.
- Stored as a hash
- SHA-256, not a slow password hash. The token is 256 bits of random output, so it has no guessable structure for a slow hash to protect.
- Revoked tokens are kept
- Revoking marks the row dead rather than deleting it, so the token list includes dead tokens — an auditor needs to see that a credential existed and when it stopped.
401 missing or invalid Bearer token
401 invalid SCIM tokenA disabled, revoked, expired and never-existed token are deliberately indistinguishable — same message, same timing — because anything else turns the endpoint into a way of testing which tokens are real. The scheme is also matched case-sensitively: Bearer <token> works and bearer <token> does not, which trips up hand-written integrations more often than you would think.
What maps to what
| SCIM attribute | Becomes |
|---|---|
userName | The email address, which is the account identifier here |
emails.value | The same email address |
name.givenName | First name |
name.familyName | Last name |
displayName | Stored on the profile as displayName |
externalId | Kept alongside the user so your directory's own identifier survives |
active | Whether the account can sign in |
id | The Qorionix user identifier |
For groups: displayName is the group name, externalId your directory's identifier, members the memberships. Groups created this way are ordinary Qorionix groups.
A newly created user gets the employee user type, an unverified email address, a record that SCIM provisioned it, and a password value that is deliberately not a valid hash — so the account exists but cannot be signed in to with a password. That is the right default for a federated-only identity. If these people also need a password, set one through the ordinary invitation flow; see Inviting your team.
One case is worth knowing in advance. Email addresses are unique across the whole platform, so if the person your feed pushes already exists — a contractor shared between two customers — the existing identity is adopted into your organisation rather than duplicated: they gain membership and a mapping, and that is all. Your feed does not get to rewrite that shared account's name, status or credential, because that would be one customer writing into another's data.
The supported operations
Create, read, replace, patch and delete, on users and groups. Patch supports the three standard operations — add, remove, replace — and acts on an explicit list of attributes rather than anything you send.
- On a user:
active,userName,externalId,displayName,name.givenName,name.familyName,name.formatted, andemails.userNamecannot be emptied or removed. - On a group:
displayName,externalIdandmembers, including the filtered member paths providers use to remove one person without resending the whole list. - Anything outside those lists is refused with
invalid SCIM patch pathand the attribute named, rather than silently ignored — an ignored patch is how two systems disagree for months.
Filters
Your provider will use filters constantly — to check whether a person exists before creating them, and to reconcile its view with ours. The grammar is the standard one: eq, ne, co, sw, ew, gt, ge, lt, le, the presence test pr, and and, or, not(...) with parentheses.
The attributes you may filter on are an allow-list, and that is a security decision rather than tidiness: answering an unknown attribute with an unfiltered result would hand the whole directory to a probe like filter=nickName eq "x". Filterable on users: userName, externalId, active, emails.value, name.givenName, name.familyName, displayName, id. On groups: displayName, externalId, id.
invalid SCIM filter
invalid SCIM filter: unsupported resource type "<rt>"
invalid SCIM filter: unexpected "<tok>" at end of filterIt fails loudly because the previous version failed quietly: it turned any filter it could not parse into a substring search for the filter's own text, which matched nothing for every real provider probe, so Okta's import reported 0 users with nothing in any log to explain why.
{
"schemas": ["urn:ietf:params:scim:api:messages:2.0:Error"],
"detail": "<what went wrong>",
"status": "<number>"
}Setting it up
Mint a token and name it after the system that will hold it
One token per connected system, so revoking the Workday feed does not break the Entra ID feed and the history tells you which system did what.
Give your provider the base URL and the token
Base URL
/api/v1/scim/v2, authentication type OAuth bearer token, the token value exactly as returned including thezqscim_prefix.Map only the attributes in the table above
Anything else is either ignored on write or a hard failure on a filter. Resist mapping your whole HR schema across; the extra attributes are not validated and do not drive behaviour.
Test with one person, then with one leaver
Provision a test user and confirm they appear in your user list with a role. Then deactivate them in your directory and confirm the account goes inactive and drops out of its groups. The second test is the one that will matter.
What to do next
- Set up provisioning before federated sign-in. Accounts arriving and leaving correctly is worth more than the sign-in experience, and it is the half that is finished.
- If you also want federated sign-in, read Single sign-on with SAML — its path resolves an assertion to a user who must already exist, which is exactly what SCIM gives you.
- Make sure every provisioned account has a second factor. Set up two-factor authentication.
- If your provider will not finish its setup wizard, or reports the connection as unhealthy, read the two callouts above first — between them they explain most of it. Otherwise, get in touch.