Skip to main content
KaireonAI implements the SCIM 2.0 protocol (RFC 7644) for automated user provisioning from enterprise identity providers like Okta, Azure AD, and OneLogin. SCIM endpoints require bearer-token authentication configured in your IdP — the bearer token is a Kaireon krn_ API key scoped to "scim".
Base URL. The working SCIM base path is /scim/v2/* (e.g. https://playground.kaireonai.com/scim/v2/Users). The /api/v1/scim/* alias exists in the codebase but Bearer-authenticated calls to it are blocked at middleware. Always use /scim/v2/ when configuring your IdP.

Authentication

Every SCIM request runs through Kaireon’s SCIM auth gate, which accepts two modes:
  1. Bearer token (production IdP integration). Send Authorization: Bearer krn_<your_api_key>. The token is verified against your tenant’s API keys and the bound tenantId is used for scoping. The API key must carry the "scim" scope — a key with an empty scopes array (legacy full-access key) is also accepted for backwards compatibility, but a non-empty scopes array that does not include "scim" returns a SCIM-formatted 403 { detail: "API key lacks the 'scim' scope" }. Invalid or expired tokens return a SCIM-formatted 401.
  2. Session fallback (admin UI testing only). When no Authorization: Bearer header is present, the route falls back to the standard requireRole("admin") + requireTenant chain. This path is for debugging from a logged-in admin browser session and is not used by IdPs.

Creating a SCIM-scoped API key

curl -X POST https://playground.kaireonai.com/api/v1/api-keys \
  -H "Content-Type: application/json" \
  -H "X-Tenant-Id: <tenant-id>" \
  -H "X-User-Role: admin" \
  -d '{"name": "Okta SCIM Provisioner", "scopes": ["scim"]}'
Pass the returned key value as Authorization: Bearer <key> when configuring your IdP.

GET /scim/v2/Users

List users in SCIM ListResponse format.

Query Parameters

ParameterTypeRequiredDescription
startIndexnumberNo1-based pagination index (default: 1)
countnumberNoPage size (default: 100, max: 200)
startIndex is clamped to a minimum of 1 and count to the range [1, 200].

Response

{
  "schemas": ["urn:ietf:params:scim:api:messages:2.0:ListResponse"],
  "totalResults": 25,
  "startIndex": 1,
  "itemsPerPage": 25,
  "Resources": [
    {
      "schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"],
      "id": "clx...",
      "userName": "john@example.com",
      "name": { "formatted": "John Smith" },
      "emails": [
        { "value": "john@example.com", "primary": true }
      ],
      "active": true,
      "meta": {
        "resourceType": "User",
        "created": "2026-03-01T00:00:00.000Z",
        "lastModified": "2026-03-18T00:00:00.000Z"
      }
    }
  ]
}
userName is the user’s stored email (falling back to the user id when no email is set). name is emitted as { "formatted": "<stored full name>" }, and is omitted entirely when the user has no stored name. active is always true in this representation — the User resource does not carry a per-user disabled flag in this version. The default page size is count=50 (max 200) when no count query parameter is supplied.

POST /scim/v2/Users

Create a user from a SCIM resource. userName is required. New users are provisioned with role viewer.

Request Body

{
  "schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"],
  "userName": "jane@example.com",
  "name": {
    "givenName": "Jane",
    "familyName": "Doe"
  },
  "emails": [
    { "value": "jane@example.com", "primary": true }
  ]
}
The email is resolved in this order: the emails entry marked primary: true, then emails[0].value, then userName. The stored name is composed from name.givenName + name.familyName (space-joined, dropping any blank parts); when both are absent the name is stored as null.

Response (201)

Returns the created SCIM User resource (the same shape as the list Resources[] entries — id, userName, name.formatted, emails, active: true, meta).

Error — Invalid body (400)

When userName is missing or the body fails validation, the route returns the Zod validation message:
{
  "schemas": ["urn:ietf:params:scim:api:messages:2.0:Error"],
  "status": "400",
  "scimType": "invalidValue",
  "detail": "<zod validation message>"
}

Error — User Exists (409)

The user-uniqueness check is by email. The error detail embeds the conflicting email.
{
  "schemas": ["urn:ietf:params:scim:api:messages:2.0:Error"],
  "status": "409",
  "scimType": "uniqueness",
  "detail": "User jane@example.com already exists"
}

GET /scim/v2/Users/

Get a single user by ID. Tenant-scoped — returns 404 { detail: "User not found" } with a SCIM error envelope when the id does not belong to the bearer token’s tenant.

PUT /scim/v2/Users/

Replace a user resource. The accepted body fields are userName, name.formatted, and emails — the route updates the user’s email (from emails[0].value, falling back to userName, then the existing email) and name (from name.formatted, falling back to the existing name).
This version’s PUT does not accept or act on an active field — the PutBody schema has no active property, so any active value in the request is ignored. There is no enable/disable-via-PUT behavior. The returned resource always reports active: true.
Returns the updated SCIM User resource on success, or 404 { detail: "User not found" } when the id is not in the caller’s tenant, or 400 with the Zod validation message for a malformed body.

DELETE /scim/v2/Users/

Deletes the user record (prisma.user.delete). Tenant-scoped — the user must belong to the bearer token’s tenant.

Response

204 No Content on success. 404 { detail: "User not found" } with a SCIM error envelope when the id is not in the caller’s tenant.