# Provisioning with SCIM 2.0

By the end of this page, a directory service connects the accounts of an organization to Szenera: Joining creates an account, leaving suspends it, and a group change sets the role. You learn what the token looks like, which endpoints exist, what is mapped to what, and what happens on conflicts.

> **Type:** reference
> **For:** Anyone who connects a directory service: Microsoft Entra ID, Okta, or a custom integration per RFC 7643 and RFC 7644.
> **Prerequisite:** an active sign-in method through the organization’s provider and a SCIM token `sct_…` from the portal

## The SCIM token

The interface lives under `/scim/v2` and **not** under `/v1`: The path belongs to a third-party protocol, and a directory service validates its own data against the address it talks to.

| What | Value |
|---|---|
| Base URL | `$SZENERA_API/scim/v2` |
| Credential | `Authorization: Bearer sct_…` |
| Content type | `application/scim+json` in both directions |
| Reach | exactly one organization per token |

If your environment has a sign-in host, the portal will in future name `https://auth.<your-domain>/scim/v2` as the base URL, and exactly this is what appears in `meta.location` of the responses. New setups enter it from the start, and an existing one at the API base URL remains reachable until further notice.

The token is created in the portal under **Settings › Organization › Company sign-in**, section **Provisioning (SCIM)**. Its value appears there exactly once. It is accepted on **no** other route of the platform: Everywhere else it is rejected with `403`, and conversely no API key reaches `/scim/v2`.

**The sign-in method is a condition, not a recommendation.** An account created this way gets no password and signs in through the organization’s provider. Without an active provider, no token can be issued.

## The endpoints

| Route | Methods |
|---|---|
| `/scim/v2/ServiceProviderConfig` | `GET`, feature set: partial update yes, filter yes, bulk no, sorting no |
| `/scim/v2/ResourceTypes`, `/scim/v2/ResourceTypes/{id}` | `GET` |
| `/scim/v2/Schemas`, `/scim/v2/Schemas/{id}` | `GET` |
| `/scim/v2/Users` | `GET` (list and filter), `POST` |
| `/scim/v2/Users/{id}` | `GET`, `PUT`, `PATCH`, `DELETE` |
| `/scim/v2/Groups` | `GET` (list and filter), `POST` |
| `/scim/v2/Groups/{id}` | `GET`, `PUT`, `PATCH`, `DELETE` (rejected, see below) |

A directory service starts with the first row:

```bash
curl -sS "$SZENERA_API/scim/v2/ServiceProviderConfig" \
  -H "Authorization: Bearer $SZENERA_SCIM_TOKEN"
```

Create an account. This is repeatable, and the same address returns the same account:

```bash
curl -sS -X POST "$SZENERA_API/scim/v2/Users" \
  -H "Authorization: Bearer $SZENERA_SCIM_TOKEN" \
  -H "Content-Type: application/scim+json" \
  -d '{
        "schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"],
        "userName": "deniz.acar@demo.local",
        "externalId": "entra-8f3c1a20",
        "name": { "givenName": "Deniz", "familyName": "Acar" },
        "active": true
      }'
```

Suspend an account. A partial update is the standard way:

```bash
curl -sS -X PATCH "$SZENERA_API/scim/v2/Users/usr_01KFB7QPW0TEAM000000000002" \
  -H "Authorization: Bearer $SZENERA_SCIM_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 }]
      }'
```

## Paging and filtering

`startIndex` counts from **one**, and `count` is capped at 200. Exactly the form `attribute eq "value"` is accepted as a filter: `userName eq` and `externalId eq` on `Users`, `displayName eq` on `Groups`. Everything else is `400` with `scimType: "invalidFilter"`, because a silently ignored filter would return the entire set of accounts.

```bash
curl -sS --get "$SZENERA_API/scim/v2/Users" \
  -H "Authorization: Bearer $SZENERA_SCIM_TOKEN" \
  --data-urlencode 'filter=userName eq "deniz.acar@demo.local"'
```

## The mapping

| SCIM | Szenera |
|---|---|
| `userName` | the email address. It is unique per organization and therefore the shared key |
| `externalId` | is stored, is unique per organization, and can be found via `externalId eq` |
| `name.givenName`, `name.familyName`, `displayName` | the display name of the profile |
| `active: false` | suspend the account. `active: true` reactivates it |
| `DELETE /Users/{id}` | **suspends**, does not delete |
| `Group` | a role of the organization. `displayName` is its name |
| `members` | the accounts that hold this role |

Fields that Szenera does not keep (`title`, `phoneNumbers`, extension schemas) are silently discarded. Your mapping in the directory service does not need to be thinned out for that.

Four peculiarities that every integration must know about:

- **Nothing is ever deleted.** Audit log entries, uploaded media, and created programs hang off an account. Permanent deletion remains a deliberate step by a person in the portal.
- **An account holds exactly one role, never none.** An `add` assigns the role and replaces the previous one. A `remove` resets to the base role. A `PUT` is both together, because anyone who is missing from the list falls back.
- **Groups are not created through SCIM.** The substance of a role is its permissions, and the protocol does not carry them. `POST /Groups` therefore matches an existing role by its `displayName`. An unknown name gets `400` with `scimType: "invalidValue"`. Create the roles in the portal first and name the groups in the directory identically.
- **Every change is repeatable.** A directory service retries after every timeout, even if the first attempt went through. A second `POST /Users` with the same address returns the same account, and a `PATCH active:false` on an already suspended account is a silent success.

Every change writes an entry into the audit log of the organization, and the originator shown there is “SCIM”.

## Errors

Under `/scim/v2`, the error format of the protocol applies, not [that of this API](/api/error-format): A directory service evaluates `scimType` programmatically and decides accordingly whether to retry, skip the entry, or stop.

```json
{
  "schemas": ["urn:ietf:params:scim:api:messages:2.0:Error"],
  "status": "409",
  "scimType": "uniqueness",
  "detail": "A user with this userName already exists in this organization"
}
```

`status` is a string, as the protocol prescribes.

| Case | Response |
|---|---|
| The credential is missing or invalid | `401` |
| The credential is something other than `sct_…` | `403` |
| The address already exists in this organization | `409` with `scimType: "uniqueness"` |
| The filter has a form other than `attribute eq "value"` | `400` with `scimType: "invalidFilter"` |
| `POST /Groups` names a role that does not exist | `400` with `scimType: "invalidValue"` |
| `DELETE /Groups/{id}` | `400` with `scimType: "mutability"`. Roles are deleted in the portal |

## Related

- [Authentication: keys and sessions](/api/authentication)
- [Base URL, /v1, and stability](/api/base-url-and-stability)
- [Handbook: Accounts from your employee directory (SCIM)](https://szenera.com/docs/organization/scim-provisioning)
