SAQ implements SCIM 2.0 (RFC 7643 and 7644) so that an identity provider can create, update, deactivate, and group workspace users. This page is the wire-level reference; an Owner or Admin turns SCIM on and creates the token as described under SCIM provisioning.
Base URL and authentication
- Base URL:
https://saq.no/scim/v2. - Authentication:
Authorization: Bearer saq_scim_…, a token created under Settings → Authentication → SCIM provisioning. It is shown once and can be revoked there. Cookies are ignored; API tokens (saq_…) are refused. - Content type:
application/scim+jsonin responses; JSON request bodies up to 256 KiB (413above). - Responses use the standard SCIM error shape:
{ "schemas": ["urn:ietf:params:scim:api:messages:2.0:Error"], "status": "400", "detail": "...", "scimType": "invalidValue" }.
The token carries the authority of the Owner or Admin who created it: a token created by an Admin can grant Admin or Member, only a token created by an Owner can grant Owner, and an Admin-created token can neither demote nor deactivate an Owner (refused with 403).
Endpoints
| Endpoint | Methods | Notes |
|---|---|---|
/ServiceProviderConfig, /ResourceTypes, /Schemas |
GET | Discovery |
/Users |
GET, POST | List with filter, startIndex, count; create answers 201 with a Location header |
/Users/{id} |
GET, PUT, PATCH, DELETE | DELETE deactivates and answers 204 |
/Groups |
GET, POST | List with filter, startIndex, count |
/Groups/{id} |
GET, PUT, PATCH, DELETE | Rename, replace or change members, delete |
Filters and paging
Only attribute eq "value" is supported. Users filter by userName or externalId; groups by displayName or externalId. Anything else, including emails.value, displayName on users, and userName on groups, answers 400 invalidFilter.
Paging is 1-based: startIndex (default 1) and count (default 100, maximum 200). List responses carry totalResults, startIndex, itemsPerPage, and Resources.
User attributes
| Attribute | Behaviour |
|---|---|
userName |
Required. The person's email address and login key. Immutable: a PUT or PATCH that changes it answers 409 with scimType: "mutability" |
externalId |
Stored; unique per workspace (409 uniqueness on a duplicate) |
displayName, name.givenName, name.familyName, name.formatted |
Stored. The display name is displayName, else name.formatted, else given and family name, else the local part of the address. SAQ renames only identities it created itself; an adopted existing account keeps the name its owner chose |
active |
true on create unless given; a PUT that omits it leaves it unchanged. false deactivates |
emails |
Read-only; derived from userName. Not consulted on input |
groups |
Read-only; the groups the user belongs to |
urn:arktiq:saq:role |
Read-only; the workspace role in effect: owner, admin, or member |
Other attributes (title, phoneNumbers, addresses, …) |
Accepted and ignored |
Group attributes and members
A group has displayName (required, unique per workspace regardless of case), externalId, and members (a list of { "value": "<user id>" }). Member values must be SCIM user ids from this workspace; anything else answers 400 invalidValue. A PUT that omits members keeps the current list.
The workspace maps group names to roles under Settings → Authentication → SCIM provisioning. A user's role is the highest role among the mapped groups they belong to, else the default role. Group changes apply at once, in both directions.
PATCH semantics
Operations add and replace accept either a path or a value object. Supported user paths: active, displayName, externalId, name, name.givenName, name.familyName, name.formatted. Paths may carry the schema URN prefix. remove clears externalId, displayName, or name.*; removing active is refused; removing an attribute SAQ does not store is accepted and ignored so a bundled deactivation still applies.
String booleans are accepted for active ("True", "False"), as Microsoft Entra ID sends them.
Group PATCH: add/replace/remove on members (a list or a single member), remove with members[value eq "<id>"], and replace of displayName or externalId.
Provisioning rules
- Verified domain. A
userNamewhose domain the workspace has not verified by DNS is refused with400 invalidValue("The domain of userName is not verified in this workspace"). Invite guests on foreign domains instead. - Adoption. A create for an address that already has an account adopts it: the person keeps their profile, and a role above what the mapping says is kept. A create for a resource that was deactivated reactivates it under the same id. A create for an active resource answers
409 uniqueness. - Exclusions. Agent accounts and client users of the workspace cannot be provisioned (
409 uniqueness). An account that was deleted globally answers409 mutability. - Seats. Creating a member beyond the plan's seats answers
409(seat_limit). - Deactivation (
active: falseor DELETE) removes the membership, revokes the person's tokens, project memberships, and viewer grants, and pseudonymises their workspace-local profile ("Former member N"). The global account and other workspaces are untouched; the SCIM resource keeps its id so the provider can reactivate it. The last Owner cannot be deactivated (409). - Role changes that cross between Member and Owner/Admin revoke the person's personal API tokens.
Unsupported features
ServiceProviderConfig reports them: no sorting, no bulk operations, no ETags, no changePassword. Filters other than eq are refused.
Plan gating and read-only workspaces
SCIM provisioning is included from the Business plan (and in the trial). On a plan without it, existing tokens may only deprovision: DELETE and active: false pass, everything else answers 403 ("SCIM provisioning is not included in this plan"). A read-only workspace behaves the same way ("This workspace is read-only"). A workspace where SCIM is turned off answers 403 ("SCIM provisioning is turned off for this workspace"); an unknown or revoked token answers 401.
Rate limits
600 requests per minute per client address and 120 per token; both answer 429.
Examples
Create a user:
curl -X POST "https://saq.no/scim/v2/Users" \
-H "Authorization: Bearer saq_scim_YOUR_TOKEN" \
-H "Content-Type: application/scim+json" \
-d '{
"schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"],
"userName": "kari@acme.example",
"externalId": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"name": { "givenName": "Kari", "familyName": "Nordmann" },
"active": true
}'
Response (201):
{
"schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"],
"id": "01924c3e-6b2a-7d11-9d0e-3a1f4b5c6d7e",
"externalId": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"userName": "kari@acme.example",
"displayName": "Kari Nordmann",
"name": { "formatted": "Kari Nordmann" },
"active": true,
"emails": [{ "value": "kari@acme.example", "primary": true }],
"groups": [],
"urn:arktiq:saq:role": "member",
"meta": {
"resourceType": "User",
"created": "2026-09-19T10:15:00.000Z",
"lastModified": "2026-09-19T10:15:00.000Z",
"location": "https://saq.no/scim/v2/Users/01924c3e-6b2a-7d11-9d0e-3a1f4b5c6d7e"
}
}
Deactivate a user:
curl -X PATCH "https://saq.no/scim/v2/Users/01924c3e-6b2a-7d11-9d0e-3a1f4b5c6d7e" \
-H "Authorization: Bearer saq_scim_YOUR_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 }]
}'
Create a group:
curl -X POST "https://saq.no/scim/v2/Groups" \
-H "Authorization: Bearer saq_scim_YOUR_TOKEN" \
-H "Content-Type: application/scim+json" \
-d '{
"schemas": ["urn:ietf:params:scim:schemas:core:2.0:Group"],
"displayName": "SAQ admins",
"externalId": "3f2504e0-4f89-11d3-9a0c-0305e82c3301",
"members": []
}'
Add a member to a group:
curl -X PATCH "https://saq.no/scim/v2/Groups/01924c3e-0000-7000-8000-000000000010" \
-H "Authorization: Bearer saq_scim_YOUR_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": "01924c3e-6b2a-7d11-9d0e-3a1f4b5c6d7e" }]
}]
}'
Microsoft Entra ID notes
- Entra sends
userPrincipalNameasuserNameandmailas the primary email; SAQ keys onuserNameonly, which is whyemails.valuefilters are refused. - Entra sends
activeas the strings"True"and"False"; both are accepted. - Group PATCH values use SAQ's SCIM user ids (the
idin each User resource), not Entra object ids. Put Entra object ids inexternalIdfor lookups. - Group names must be unique in the workspace regardless of case; map the names Entra sends under Settings → Authentication.