SCIM provisioning
Provision and deprovision enterprise members from your identity provider. SCIM 2.0 (RFC 7643/7644), served over REST rather than GraphQL.
The base URL carries no enterprise id. The tenant comes from your token, so a connector is configured with this URL exactly as written.
Point your IdP's SCIM connector at that base and give it an OAuth 2.0 bearer token. Everything below describes what the connector will find.
Credentials
client_credentials only. Mint a token at the enterprise token endpoint:
The request body is JSON and the grant field is spelled grant_type — verified
against the deployed endpoint as of 2026-09-01. See
OAuth 2.0 for the full exchange.
Two scopes gate the surface:
| Scope | Grants |
|---|---|
enterprise:scim:read | Discovery, and reading Users and Groups |
enterprise:scim:write | Creating, updating, deactivating and deleting them |
Both must be requested by name, and must be on your client's registration
before that will work. The coarse grant enterprise deliberately does not
expand into them: enterprise:scim:write can deactivate every member of your
organisation, which is not something a consent granted for order data should
silently carry. The surface wildcard enterprise:* does cover them.
Okta and Entra ID
Okta supports OAuth 2.0 client credentials for SCIM, so its provisioning connector authenticates against this API directly: give it the token endpoint above, your client id and secret, and the two scopes.
Entra ID's provisioning UI cannot authenticate against this API as it
stands. That UI pushes a static Secret Token, and its OAuth support is
limited to a pre-authorized flow. This API accepts no static token — every
request needs a bearer token minted by client_credentials — so there is no
field in the Entra provisioning form that will produce a credential this server
accepts. Plan for that before you begin the integration rather than at the
point the first sync fails.
Authentication and authorisation failures
The checks run in a fixed order and the first one to fail is the one you see, so the response tells you which switch is off.
| # | Check | Failure |
|---|---|---|
1 | Bearer token present, decodable, and not revoked | 401 — A bearer token is required. or The bearer token is invalid or has expired. |
2 | Token resolves to exactly one enterprise | 403 — This token is not scoped to a single enterprise. |
3 | SCIM is enabled on the tenant | 403 — SCIM provisioning is not enabled for this enterprise. |
4 | Token carries the scope the operation needs | 403 — Insufficient scope: enterprise:scim is required. (or enterprise:scim:write) |
5 | Account holds enterprise.scim.manage — user-context tokens only | 403 — Access denied. |
Insufficient scope is 403, not 401. A 401 means the bearer token itself is
missing, malformed, expired or revoked, and those responses carry
WWW-Authenticate: Bearer realm="scim". A token that has been revoked but has
not yet expired fails check 1 rather than a later one — the check is a liveness
lookup, not a signature test.
None of the 403s carry a scimType field. It is omitted, not null.
Access denied. is deliberately ambiguous. The same string is returned when the
account behind a user-context token lacks enterprise.scim.manage, when that
account is not a member of the enterprise at all, and when the enterprise itself
has been deactivated or deleted. That is on purpose — a caller cannot use the
response to discover whether an enterprise id exists — and it is why this is the
hardest of these failures to debug. Check the account's permissions first.
A client_credentials token skips check 5 entirely: it carries user_id: 0 and
no account, so there is no permission for it to hold.
enterprise.scim.manage constrains only tokens minted for a signed-in account.
The tenant switch in check 3 is the scimEnabled field in
Settings, writable with enterprise:settings:write. It
is read per request, so turning SCIM off takes effect immediately rather than
when tokens expire.
SCIM is one of two ways to provision members automatically. The other is
SFTP data automation, which takes CSV files from an HR
system instead. An enterprise runs one or neither, never both: a settings write
that would leave scimEnabled and sftpEnabled both on is rejected, so turn
SFTP off in the same write that turns SCIM on.
One failure happens before SCIM is reached at all. If enterprise:scim:read or
enterprise:scim:write is not on your client's registration, the token
endpoint refuses to issue and you never get as far as a 403:
Endpoints
| Method | Path | Scope |
|---|---|---|
GET | /ServiceProviderConfig | enterprise:scim:read |
GET | /ResourceTypes, /ResourceTypes/{id} | enterprise:scim:read |
GET | /Schemas, /Schemas/{id} | enterprise:scim:read |
GET | /Users | enterprise:scim:read |
POST | /Users | enterprise:scim:write |
GET | /Users/{id} | enterprise:scim:read |
PUT, PATCH, DELETE | /Users/{id} | enterprise:scim:write |
GET | /Groups | enterprise:scim:read |
POST | /Groups | enterprise:scim:write |
GET | /Groups/{id} | enterprise:scim:read |
PUT, PATCH, DELETE | /Groups/{id} | enterprise:scim:write |
Responses are always application/scim+json. Request bodies are parsed
regardless of the content type you send.
Discovery
/ServiceProviderConfig reports the subset of SCIM this server implements:
| Feature | Advertised | Consequence |
|---|---|---|
patch | supported: true | |
filter | supported: true, maxResults: 200 | Narrower than the flag implies — see below |
sort | supported: false | sortBy and sortOrder are ignored |
bulk | supported: false, maxOperations: 0, maxPayloadSize: 0 | There is no /Bulk endpoint |
etag | supported: false | No meta.version and no conditional requests |
changePassword | supported: false | There is no password attribute; members set their own through the account verification email |
documentationUri points at this page,
https://developers.kesk.app/enterprise/scim.
filter is advertised as supported, and ServiceProviderConfig has no field
that can express how much narrower it really is. Filtering is equality only:
userName eq and externalId eq on Users, displayName eq on Groups. Anything
else returns 400 invalidFilter. A connector that reads the config and builds
a compound or substring filter from it will be rejected at request time.
/Schemas advertises exactly two schemas:
| Schema | Attributes |
|---|---|
urn:ietf:params:scim:schemas:core:2.0:User | userName, name, displayName, emails, groups — all read-only. active and externalId — read-write |
urn:ietf:params:scim:schemas:core:2.0:Group | displayName and members read-write, system read-only |
system is a top-level, non-standard attribute. It is declared in /Schemas
rather than hidden under a vendor URN, so a connector that reads schemas will
see it.
/ResourceTypes lists two: User, at /Users, described as Enterprise
member; and Group, at /Groups, described as Enterprise role.
The SCIM enterprise User extension,
urn:ietf:params:scim:schemas:extension:enterprise:2.0:User, is not
implemented, and a request carrying its attributes is accepted and silently
ignored rather than rejected: a PATCH that sets department or manager
returns 200 and has no effect. Entra ID sends exactly those attributes this
way, so they will appear to sync and will not.
No discovery endpoint is anonymous. /ServiceProviderConfig,
/ResourceTypes, /Schemas and the per-id forms of the last two all require
enterprise:scim:read and return 401 without it. RFC 7644 permits gating
them, but many connectors probe them unauthenticated during setup and will fail
on the first step. Configure the credential before pointing a setup wizard at
the base URL.
Also ignored rather than rejected: attributes and excludedAttributes, any
other unrecognised query parameter, and unknown body fields.
Users
A User is an enterprise member. The resource id is the member's id within your
tenant, rendered as a string. externalId is stored and echoed back but is never
used as the id.
Listing
| Parameter | Default | Notes |
|---|---|---|
startIndex | 1 | 1-based. Values below 1 are treated as 1 |
count | 100 | Clamped to [1, 200]. 200 is the advertised maxResults |
filter | none | Equality only — see below |
Deactivated members stay in the list. That is deliberate: a connector that stopped seeing them would re-create everyone it had just deprovisioned.
Filters
Only attribute eq "value" parses, and only on two attributes:
| Filter | Behaviour |
|---|---|
userName eq "[email protected]" | Matched case-insensitively |
externalId eq "00u1a2b3c4" | Matched verbatim, case-sensitive |
Anything else — another attribute, another operator (co, sw, pr, ne), an
unquoted value, or an and/or compound — returns 400 with
scimType: "invalidFilter" rather than quietly returning the whole directory.
A userName filter that matches nobody returns an empty page with
totalResults: 0, not a 404.
Creating
Returns 201 with a Location header. userName is the only required field;
active defaults to true when absent. If emails is supplied, the primary
entry is used as the address and userName is not; otherwise userName is the
address.
The address must be on a verified domain of your enterprise. If it is not,
the create fails with 400 invalidValue — That address is not on a verified
domain of this enterprise. This is not standard SCIM behaviour and it is the
most common first-run failure. Domain verification is an administrator task and
is not part of this API, so confirm the domain is verified before you point a
connector at it.
A create for an address that is already a live member returns 409 with
scimType: "uniqueness". Identity providers rely on that to switch from create
to update, so it is the correct answer rather than an error to design around. If
the address belongs to a member you previously deleted, the create revives that
member and returns their original id.
A new member who lands in no group gets the Enterprise Member role. If the
address has no Kesk account yet, one is created and a verification email is
sent; the password is the platform's and cannot be set through SCIM.
Updating
A PUT writes only externalId and active. userName, emails, name
and displayName are read from the body, validated, and then discarded — a PUT
cannot rename a member or change their email address. Directory attribute
changes made in your IdP do not propagate to Kesk.
Note also that active defaults to true, so a PUT that omits it activates
the member.
PATCH is the safer verb. It honours these paths:
op | path | Effect |
|---|---|---|
add, replace | active | Activate or deactivate |
add, replace | externalId | Set the IdP's identifier |
remove | externalId | Clear it |
replace | (omitted) | Reads active from the value object |
any | name, emails, displayName, userName | Accepted and ignored |
any | enterprise extension attributes | Accepted and ignored |
Any other path returns 400 invalidPath.
A rejected operation discards the whole request, including operations earlier in
the same Operations array that would otherwise have applied. If your connector
sends a department update alongside a deactivation, the deactivation is lost
with it.
Deprovisioning
Returns 200 with the full resource. The member is retained, not deleted:
they stop being active, they stay in the directory, and the same call with
true reactivates them.
op is accepted in any casing (replace, Replace, REPLACE), and value
accepts the strings "true" and "false" as well as JSON booleans, because
provisioning agents send all of these.
DELETE /Users/{id} returns 204 with no body. It is a soft delete — the
member stops appearing, and a later create for the same address revives them.
Groups
A Group is an enterprise role. Creating a group creates a role; assigning members assigns that role.
Returns 201 with a Location header. A duplicate displayName returns 409
uniqueness.
members[].value is a User resource id — the id from /scim/v2/Users,
which is the member's id within your tenant. A member id that is not live in
your enterprise fails the whole request with 400 invalidValue naming the
offending ids; nothing is applied partially.
A group created through SCIM carries no platform permissions. Granting any would let an identity provider mint authority by choosing a group name. What a role grants is set by an administrator, not through this API and not through SCIM — so a group your IdP creates is a container for membership until someone gives the matching role its permissions.
Platform-provisioned groups
Groups the platform created carry system: true. Members can be assigned to
them, but they cannot be renamed or deleted — both return 400 with
scimType: "mutability".
There are three of them, and /Groups returns them under these exact
displayName values:
displayName | Permissions |
|---|---|
Enterprise Administrator | Every enterprise permission, including enterprise.scim.manage |
Enterprise Manager | Fourteen named permissions. Not SCIM |
Enterprise Member | Three: view orders, view vouchers, view analytics |
A user created through SCIM who is assigned to no group defaults to
Enterprise Member.
A tenant that already had a group of one of those three names keeps both. The
duplicate is not marked system, so such a tenant sees a fourth group over
SCIM — and unlike the three above, that one can be renamed and deleted.
Filters
displayName eq "Engineering" only. Any other attribute returns 400
invalidFilter.
PATCH
op | path | Effect |
|---|---|---|
add | members | Add members. Idempotent; accepts an array or a single object |
remove | members[value eq "42"] | Remove that member |
remove | members with a value | Remove those members |
replace | members | Replace the membership wholesale |
any | displayName | Rename. 400 mutability on a system group |
replace | (omitted) | Reads members and displayName from the value object |
Two ways to empty a group by accident:
- A
PUTwith an absent or emptymembersarray replaces the membership with nothing. - A bare
removeonmemberswith no filter and novalueclears the group entirely.
Group operations are also applied one at a time rather than in a transaction, so
a request that fails halfway leaves the earlier operations in place. Send
membership changes as explicit add and remove operations.
Response shapes
A list:
Resources has a capital R. itemsPerPage is the real length of the page
returned, not the count you asked for, so a short page is how you recognise
the last one.
An error:
status is a string, per RFC 7644 §3.12, and always matches the HTTP status.
scimType is omitted when there isn't one. The values this server emits are
invalidFilter, invalidPath, invalidSyntax, invalidValue, mutability
and uniqueness.
An unknown path under /scim/v2 returns a SCIM 404 rather than the platform's
own error shape, and an unsupported method returns a SCIM 405.
Reference
Generated reference — every type, field and argument of the Enterprise GraphQL API, built from the live schema. SCIM is a separate REST surface and is documented in full on this page.