# SCIM provisioning

SCIM provisioning makes your identity provider the source of truth for who
exists in a LedgerBee company and what they can do. The IdP pushes users and
groups over SCIM 2.0; each group's effect is configured in LedgerBee under
**Settings → Access → User provisioning** and is never writable over the
protocol.

A synced user is an identity with no rights of its own. Group membership decides
what it materializes:

1. **Employee record** — the person appears in the employee directory and flows
   into payroll where that integration is connected.
2. **App access** — the person gets a LedgerBee account, can sign in, and holds
   the roles and admin rights their groups grant.

A group can do either, both, or neither. A user provisioned only through an
app-access group never becomes an employee, and an employee never requires app
access.

## Endpoint and authentication

The SCIM 2.0 base URL is `https://api.ledgerbee.com/api/v1/scim`. Every request
carries the provisioning token as a bearer token:

```
Authorization: Bearer scim_...
```

The token identifies the company, so no company identifier appears in any
request path or body.

Generate it under **Settings → Access → User provisioning**. It is displayed
once. The **Provisioning status** toggle suspends provisioning while keeping the
token valid; every request returns `403` while it is off. Revoking the token
invalidates it permanently.

These endpoints implement a protocol rather than the REST API, so they are not
part of the generated [API Reference](/api). The tables below are the contract.

### Supported operations

| Resource | Operations |
| --- | --- |
| Users | `POST /Users`, `GET /Users`, `GET /Users/{id}`, `PUT /Users/{id}`, `PATCH /Users/{id}`, `DELETE /Users/{id}` |
| Groups | `POST /Groups`, `GET /Groups`, `GET /Groups/{id}`, `PUT /Groups/{id}`, `PATCH /Groups/{id}`, `DELETE /Groups/{id}` |
| Discovery | `GET /ServiceProviderConfig`, `GET /ResourceTypes`, `GET /Schemas`, `GET /Schemas/{id}` |

### Support matrix

| Capability | Supported | Detail |
| --- | --- | --- |
| `PATCH` | Yes | Path-based and pathless replace forms |
| Filtering | Partial | `eq` only, on `userName` and `externalId` for Users, `displayName` and `externalId` for Groups |
| Pagination | Yes | `startIndex` and `count`; `count` is capped at 200 per page |
| Bulk operations | No | Send individual requests |
| Sorting | No | |
| ETag | No | |
| Password sync | No | A password is never accepted over SCIM |

## Group-driven provisioning

Groups pushed by the IdP appear under **Provisioned groups**. Opening a group
exposes four settings:

| Setting | Effect on members |
| --- | --- |
| **Provision employees** | An employee record is created. Leaving the group never removes it. |
| **Grant app access** | Not available yet. Will create a LedgerBee account the member can sign in with. |
| **Roles** | Not available yet. Will grant the permissions of the selected roles. |
| **Grant admin** | Not available yet. Will make the member an administrator of the company. |

**App access arrives with single sign-on.** The last three settings depend on a
provisioned member being able to authenticate, which needs the per-tenant
sign-on flow that ships next; enabling them returns
`400 SCIM_APP_ACCESS_NOT_AVAILABLE`. Until then a group provisions employee
records, and app accounts are created by invitation from the Users settings.

Grants combine additively across groups: a member of two app-access groups holds
the union of both role sets, and is an admin when either group grants admin.
Group-derived grants are tracked separately from roles assigned by hand, so
removing someone from a group withdraws exactly what that group gave and leaves
manual assignments untouched. The Users list marks group-derived roles and flags
accounts that SCIM created.

Losing membership of every app-access group deactivates the SCIM-created account
and ends its live sessions within the access-token lifetime. An account created
by invitation keeps its access and loses only the group-derived grants.

### Employee records are retained

A group change never removes an employee record, because bookkeeping records
reference employees. An employee is deactivated only when the IdP marks the user
inactive.

Only **Provision employees** creates one. A user whose groups carry
**Provision app access** alone receives an app account and no employee record.

## Provisioning lifecycle

| Phase | What happens |
| --- | --- |
| **Create** | `POST /Users` stores the identity. Employee and app-access projections are applied from current group membership. |
| **Update** | `PUT` or `PATCH /Users/{id}` updates the identity, mirrors changed attributes onto a linked employee record, and re-applies projections. |
| **Group change** | A membership `PATCH` on `/Groups/{id}` re-applies projections for every affected member. |
| **Deactivate** | `active: false` or `DELETE /Users/{id}` deactivates the account, ends live sessions, and marks a linked employee inactive. The record is retained. |

## Microsoft Entra ID

1. Open the LedgerBee enterprise application in the Microsoft Entra admin
   center. Create one through **Enterprise applications → New application →
   Create your own application** if none exists.
2. Open **Provisioning** and select **Automatic**.
3. Set **Tenant URL** to `https://api.ledgerbee.com/api/v1/scim` and **Secret
   Token** to the generated token.
4. Select **Test Connection**, then **Save**.
5. Keep the default attribute mappings. LedgerBee reads `userName`, `active`,
   `name.givenName`, `name.familyName`, `title`, `externalId`, and the work
   entries of `emails`, `phoneNumbers`, and `addresses`.
6. Enable group provisioning, then assign the groups to sync under **Users and
   groups**.
7. Set **Provisioning Status** to **On**.

Entra provisions on a fixed cycle of roughly 40 minutes. **Provision on demand**
applies a single user immediately. Repeated failures quarantine the provisioning
job, which is why a token change should use rotation with a grace window rather
than a revoke.

### Map `objectId` to `externalId` for Microsoft sign-in

LedgerBee stores a provisioned user's `externalId` as their Microsoft object id
when it creates the account, which is what lets them sign in with Microsoft
without a separate invitation. That only works when `externalId` carries the
**object id**: Entra's default mapping sends `mailNickname`, which is not an
object id, and LedgerBee ignores it rather than storing a value that cannot
match a sign-in.

In **Provisioning → Mappings → Provision Microsoft Entra ID Users**, edit the
attribute whose customappsso target is `externalId` and set its source to
`objectId`. Users provisioned before the change keep no object id — they link on
their first Microsoft sign-in instead.

## Okta

1. Open the LedgerBee application in the Okta admin console, select
   **Provisioning**, then **Configure API Integration**.
2. Enable **Enable API integration**, set the base URL to
   `https://api.ledgerbee.com/api/v1/scim`, and paste the token as the API token.
3. Select **Test API Credentials**, then **Save**.
4. Under **To App**, enable **Create Users**, **Update User Attributes**, and
   **Deactivate Users**.
5. Set the unique identifier field for users to `userName`. Okta resolves
   existing users with `GET /Users?filter=userName eq "..."`.
6. Push each group to sync from the **Push Groups** tab. Assigning a group to
   the application does not push it.

Okta imports users in pages of 100. Unlinking a pushed group offers to delete it
in the target application, which removes the group from LedgerBee and withdraws
the app access it granted.

## Deprovisioning

| Event in the IdP | Effect in LedgerBee |
| --- | --- |
| User deactivated (`active: false`) | Account deactivated, live sessions ended, linked employee marked inactive |
| User deleted from the application | Treated as deactivation; records are retained, never hard-deleted |
| Removed from every app-access group | SCIM-created account deactivated; invited accounts keep access and lose group-derived grants |
| Removed from an employee-flagged group | No effect on the employee record |
| Group deleted | Members lose the app access and roles that group granted |

## Errors & failure states

Errors use the SCIM error schema, with the code below in `detail`.

| Condition | What you observe | Fix |
| --- | --- | --- |
| Missing or wrong token | `401 SCIM_INVALID_TOKEN` | Re-copy the token, or generate a new one |
| Provisioning toggled off | `403 SCIM_CONNECTION_DISABLED` | Enable **Provisioning status** |
| Unsupported filter attribute or operator | `400 SCIM_INVALID_FILTER` | Use `eq` on a supported attribute |
| Malformed patch body or unsupported operation | `400 SCIM_INVALID_PATCH` | Correct the operation shape |
| Unknown user id | `404 SCIM_USER_NOT_FOUND` | Re-resolve the user by `externalId` filter |
| Unknown group id | `404 SCIM_GROUP_NOT_FOUND` | Re-push the group |
| Unknown schema URN | `404 SCIM_SCHEMA_NOT_FOUND` | Request a URN listed by `GET /Schemas` |
| Duplicate `externalId` or work email | `409 SCIM_DUPLICATE_USER` | Resolve the existing user and update it |
| Duplicate group `externalId` | `409 SCIM_DUPLICATE_GROUP` | Resolve the existing group and update it |
| Rate limited | `429` with `Retry-After` | Honour `Retry-After` and back off |

## Troubleshooting

**Test Connection fails.** Confirm the base URL ends in `/api/v1/scim`, the
token was pasted whole, and **Provisioning status** is enabled.

**Users sync but no groups appear.** In Entra, group provisioning must be enabled
and the groups assigned to the application. In Okta, groups must be pushed from
the **Push Groups** tab.

**A member of the right group has no access.** Confirm the group has **Grant app
access** enabled. A membership pushed before the user existed in LedgerBee is
skipped, so re-push that group.

**A user cannot sign in with Microsoft.** The Microsoft identity is linked only
on accounts SCIM creates. An account that already existed connects Microsoft
sign-in once from its own profile.

## SSO enforcement

**SSO enforcement**, in the same settings tab, controls how members sign in:

| Mode | Effect |
| --- | --- |
| **Off** | Any sign-in method works |
| **Optional** | Any method works; use this while rolling out SSO |
| **Required** | Only sign-in through the identity provider reaches the company. Password, passkey, Google, and Apple sessions lose access. |

**Required** can only be enabled from a session already signed in through the
identity provider, so enabling it cannot lock out the administrator who sets it.
It does not apply to advisor firms granted access — those are governed by the
grant under **Advisors**.
