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:

ScopeGrants
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.

#CheckFailure
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

MethodPathScope
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:

FeatureAdvertisedConsequence
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:

SchemaAttributes
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

ParameterDefaultNotes
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:

FilterBehaviour
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:

oppathEffect
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:

displayNamePermissions
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

oppathEffect
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 PUT with an absent or empty members array replaces the membership with nothing.
  • A bare remove on members with no filter and no value clears 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.

Built with