Monosign management API, SCIM 2.0 and webhooks
What changed
Every content update is versioned. Select a version to compare it with the one before.
Version 7 → 8
UpdateTwo modules added so the course covers the MCIE exam: OAuth clients and token lifetimes, reading a 403, tenants, the assignment and entitlement endpoints, the calls the SCIM push makes and how to read its results, and Provision Services with their dead letters. Diagrams and first-check lists were added to the new lessons.
5 lessons updatedduration
Content update. Enrolled learners were notified.
Duration changed from 30 to 34 minutes.
A `Rest API` key is not the only way a backend talks to Monosign. When your own API must accept tokens that Monosign issues, the caller is an **OAuth client**, and a few rules decide whether its token request works.
### One key is one client
A client is an application key of type `OIDC/OpenID`, `OAuth 2.0` or `JWT`. The key is the `client_id` and the key secret is the `client_secret`. The three key types are served under three path prefixes, `/openid`, `/oauth` and `/jwt`. The handler behind them is the same, but the prefix decides which key type is looked up, so a client must call the prefix of the type it was created with. A correct ID and secret sent to the wrong prefix are rejected.
### The client credentials request
A backend job has no user and no browser, so it uses the client credentials grant. The request needs only the grant type and the client's own credentials:
```
POST /oauth/token
Content-Type: application/x-www-form-urlencoded
grant_type=client_credentials&client_id=<key>&client_secret=<key secret>
```
There is no redirect address and no user name in this flow, because nobody signs in.
::diagram api-client-credentials
### Public clients cannot do this
A key with `Public Client` switched on is meant for single-page, mobile and command-line apps. It has no secret, PKCE becomes mandatory, and the client credentials grant is refused with `The client_credentials grant is not allowed for public clients.` Do not switch the option off to make a backend job work. Register a second, confidential client for the job, with its own key and secret, so each caller can be rotated alone.
### Lifetimes are in minutes
| Setting | Empty or `0` means | Unit |
| --- | --- | --- |
| `Access Token Lifetime` | 5 minutes | minutes |
| `Refresh Token Lifetime` | 30 minutes | minutes |
| `ID Token Lifetime` | 1 minute | minutes |
Up to v2026.07-hf02 the client credentials path read these values as seconds, although the form said `In minutes`. Since v2026.09 every path reads minutes. A key tuned on the old build with `3600` for one hour now issues tokens that live 3600 minutes, which is 60 hours. After the upgrade, enter `60`.
### What goes into which token
`Audience` and `Resource` on the key are written into the **access token** only. The ID token is addressed to the client itself, so its `aud` stays the client ID. An API that validates `aud` must therefore validate access tokens, never ID tokens.
### Disabling a user does not recall tokens
Access tokens already issued stay valid until their `exp`. What stops is the next refresh: for a user who is not active or not `Enabled`, the token endpoint answers `The refresh token is no longer valid.` The gap between the two is the access token lifetime, so a short `Access Token Lifetime` is the control that narrows it.
### What to check first when a token request fails
1. Does the path prefix (`/openid`, `/oauth` or `/jwt`) match the type the key was created with?
2. Is `Public Client` on? Such a key cannot use the client credentials grant.
3. Do tokens live far longer than planned after an upgrade? Read the lifetime fields as minutes.
Most support cases for an integration start with one refused call. The answer itself tells you what to do, if you read all of it.
### The 403 names the permission
With API Security on, a call to an endpoint whose permission is not granted to the key answers HTTP 403. The body looks like this:
```
{
"Message": "Permission Denied",
"InternalMessage": "Permission denied for this API endpoint. You need to have one of the following permission(s): System.API.Users.View."
}
```
`InternalMessage` lists the permissions the endpoint accepts. The working method is always the same:
1. Copy the permission name from the 403.
2. Open the application, `Keys`, the key, tab `Permissions`.
3. `Search` for that name and `Grant` it. Grant exactly what the 403 named. A broader permission widens what the key can call.
### Why the next call can still fail
Each API process answers from its own in-memory copy of the security settings. A change is picked up when the process drops its cached entry, which happens shortly afterwards, not instantly. Behind a load balancer, two instances can disagree for that short window. If a call still returns 403 right after the grant, wait a moment and test one instance at a time. Do not react by granting more permissions or by switching API Security off.
::diagram api-403-path
### What the v2026.09 default does not touch
The secure default is written at one moment only: when a new `Rest API` key is created on an application that never had a setting. Two consequences help when a customer asks about existing integrations:
- An application with an explicit choice, on or off, keeps it.
- Editing an existing key never applies the default.
### Tenants
A key belongs to one tenant. A customer tenant's key can never act in another tenant. Since v2026.10 there is one supported hand-off: a key of the **main tenant's master `api` application** may send a base-tenant hand-off header with its call, and the call then runs inside the request's tenant. Any other key that tries it gets `Invalid Application Key`. For automation that serves several tenants, the choice is therefore simple: use each target tenant's own key, or use the main tenant's master application.
### API writes do not provision by themselves
Creating, saving or removing a user in Management always queues the user for the Provision Services. The API behaves differently: a user create or update queues provisioning only when the call sets `AutoProvision`. If users created by an HR integration never reach a customer's provisioning endpoint while hand-made users do, check the request body before you check the service.
Three resource families carry most lifecycle automation: application assignments, entitlements and audits. Each has one behaviour that surprises integrators.
### Application assignments
| Call | Permission | What it does |
| --- | --- | --- |
| `POST v1.4/application-users/detail` | `System.API.ApplicationUsers.View` | Reads an assignment |
| `PUT v1.4/application-users` | `System.API.ApplicationUsers.Edit` | Changes an existing assignment |
| `DELETE v1.4/application-users/{id}` | `System.API.ApplicationUsers.Remove` | Removes one assignment (v2026.10 and later) |
`PUT` never creates an assignment. It looks for an active, non-removed assignment of the `UserId` and `ApplicationId` you send, and answers 404 `Application user not found` when there is none.
### Extending an expiry, the right way
The request has two date fields, and they do different jobs. `NewExpirationDate` **writes** the new expiry. The older `ExpirationDate` only **narrows the lookup**, so sending the new date there makes the search miss the row and the call answers 404.
```
PUT v1.4/application-users
{
"UserId": "<user id>",
"ApplicationId": "<application id>",
"NewExpirationDate": "2026-12-31T23:59:59Z"
}
```
Leave `ExpirationDate` out. Also check what your JSON library sends for an unset date. `NewExpirationDate` is applied whenever it has a value, and the server has no guard against past values: a default date such as `0001-01-01T00:00:00` expires the assignment at once. Send `null` or omit the field when you do not mean to change the expiry.
### What changed in v2026.10
- A key without master rights reaches only the assignments of **its own application**. Detail, `PUT` and `DELETE` for another application's assignment answer 404 `Application user not found`. An integration that manages several applications needs master rights or one key per application.
- `DELETE v1.4/application-users/{id}` exists. It soft-removes the assignment, runs the `Removed` trigger and writes the revoke event to the access timeline. Use it for deprovisioning. Setting a past expiry only expires the assignment and skips that trail.
### Group memberships through entitlements
`POST v1.5/entitlements/groups` adds a user to a group. From v2026.10 a repeat of the same add answers `409` with code `EntitlementAlreadyExists` and carries the ID of the existing membership. For a client that retries after a timeout, this is the answer that makes the call idempotent: count it as done. `DELETE v1.5/entitlements/{objectAssignmentId}` with that ID ends the membership.
Before v2026.10 the same call stored a record that the group store never saw, so rules and triggers ignored the membership. Those old records stay ignored after the upgrade. The fix is to add the memberships again through the API.
::diagram api-entitlement-retry
### Audits are not a list API
`v1.4/audit` offers `GET {id}`, `POST` and `POST bulk`. The last two write audit rows for external systems. There is no list route, although a permission named `System.API.Audits.List` exists, and the Audit page has no file export either. To move a week of audit rows to a data lake, build a Report Studio report on the `Audit Events` data source and deliver it as a file.
A target vendor will ask which SCIM operations it has to support. The answer is short, and knowing it explains most of the symptoms you will meet later.
### The calls for one user
For every assigned user the push first looks the user up, then decides:
```
GET /Users?filter=userName eq "jdoe"
```
| Lookup result | Assignment | Next call |
| --- | --- | --- |
| No user | Active | `POST /Users` |
| One user | Active | `PUT /Users/{id}` |
| One user | Inactive | `DELETE /Users/{id}` |
| More than one user | Any | Nothing, the user is aborted |
The user document carries `userName`, the work e-mail, given and family name, `externalId` and `active`. The user push does not use `PATCH`. A target therefore has to support the `filter` parameter with `eq`, plus `POST`, `PUT` and `DELETE` on `/Users`.
::diagram api-scim-user-decision
### Choosing the lookup attribute
`User Identifier Property` under the advanced settings decides which attribute the filter uses. The default is `externalId`. The other useful values are `userName` and `email`. Two log lines in the service log tell you that the choice does not fit the target:
| Service log line | What it means | Fix |
| --- | --- | --- |
| `SCIM API returned no result for user ... Will be created.` on every run, with duplicates building up | The target does not keep the attribute you filter on, so the lookup always misses | Pick an attribute the target persists, such as `userName` or `email` |
| `SCIM API returned more than one user ...` and the user is never updated | The attribute is not unique on the target | Pick an attribute that points to exactly one account |
### Force Sync and the schedule
`Force Sync` pushes users and groups at once, regardless of `Features` and `Enable Auto-sync`, which makes it the right button while you test. Its answer looks like `120 users sync is completed. [112 success / 0 skipped / 8 error]`. Read the numbers with care: the skipped counter is never incremented, so users that were skipped, for example for a missing first name, appear under error. Check the service log before you blame the target.
A **scheduled** push starts only when three things hold: the application is enabled, `Enable Auto-sync` is on, and a `Timing` entry has a next occurrence that is already due. `Timing` offers every hour at a given minute or every day at a given time, both in UTC, and the form shows the current UTC and local time next to it. A customer at UTC+3 who wants 02:00 local time enters `Every Day at 23:00`.
### Groups
Groups are written with `PUT` and are keyed by `externalId`, which holds the Monosign group ID. The group step reads the `externalId` of every group on the target as such an ID. One old target group with a plain text label in `externalId` stops the whole group step, and none of your mapped groups arrive. Clean that group up on the target first. `Group Identifier Property` is stored but the push does not read it, so changing it does not help.
### Outbound proxy
The push reads the global `Proxy Host` setting under Settings, `System`. Its description says the proxy is used in SCIM, SMS, call, push and license services. When the field is empty the push connects directly, which fails in networks where only the proxy may reach the internet.
### Reading a SaaS application's groups instead
The `SCIM Entitlement Provider` source works in the other direction: it imports the application's groups as entitlements and their members as grants. Its `Test` runs four steps, `Connect to SCIM endpoint`, `Read users and groups`, `Provisioning capability` and `User matching`. If the third one ends with a warning that the server does not advertise PATCH support, the import still works, but adding and removing members from Monosign will likely fail, because membership is provisioned with `PATCH /Groups/{id}`.
### What to check first
| Symptom | Look at |
| --- | --- |
| Duplicates, or users that are never updated | `User Identifier Property` and the two service log lines above |
| `Force Sync` reports errors | The service log, because skipped users are counted as errors |
| The scheduled push never starts | Application enabled, `Enable Auto-sync` on, and a `Timing` entry in UTC |
| No mapped group arrives | The `externalId` of the groups that already exist on the target |
| The push cannot connect | `Proxy Host` under Settings, `System` |
SCIM is not the only outbound path. A **Provision Service** binds a provision provider to an application, with a title, a `Url`, JSON settings and rules. The user provisioning service checks every user against every enabled service and sends provision or deprovision work to the customer's endpoint. If you build that endpoint, the life of one work row decides your design.
### The life of a row
| Step | What happens |
| --- | --- |
| Queued | A row waits as `WaitingForProvision` (or the deprovision equivalent) |
| Sent | The service POSTs to the `Url`; the row becomes `Provisioning` |
| Your endpoint answers 2xx | Nothing else changes. There is no callback, and nothing sets the row to provisioned |
| About one hour later | A sweep puts rows that are still in flight back to waiting, and the row is POSTed again |
| 48 hours after the row was created | It is marked as an error with the message that the retry budget is spent, and it appears under `Dead Letters` |
The consequence is the most important sentence of this lesson: **a 2xx does not end the row, so your endpoint receives the same user again about every hour.** Design it so that a repeat changes nothing: look the account up first, create it only when it is missing, and answer 2xx either way.
::diagram api-provision-row-life
### Three settings that mislead
- **`Url` is not required.** With an empty `Url` the service's rows stay `WaitingForProvision` for ever and no error is logged. Check this first when a new service delivers nothing.
- **`State` is a stored badge.** It does not start or stop anything.
- **`Remove` does not delete.** It only sets `Is Enabled` to false, and most roles get 403 when they try it. To take a service out of use for a while, use `Edit`, untick `Is Enabled` and save. The pause is reversible and does not deprovision the accounts the service already created.
### Who counts as out of scope
A user who is inactive, not `Enabled` or **has no e-mail address** is treated as outside every rule and is queued for deprovision. A data clean-up that blanks e-mail addresses can therefore remove real accounts on the customer's side. Agree on this with the HR team before such a clean-up.
`Start Full User Provision` checks every user against every enabled service. The toast `Full User Provision is Started.` appears before any work is done, and there is no progress view. Review the rules and `Is Enabled` on every service before you click it, because users outside a rule are queued for removal.
### Dead letters for provisioning
Management, System, Services, `Dead Letters` lists the provisioning rows that ran out of budget. They are not retried on their own.
| Button | Use it when | Effect |
| --- | --- | --- |
| `Requeue` | The cause is fixed and the user still needs the account | The row goes back to its waiting state and the service picks it up again. It can fail again and return to the list |
| `Discard` | The work already happened another way, for example IT created the account by hand | The row moves to a final discarded state, keeps its content and last error, and nothing is sent |
A discard cannot be undone, but it does not block the future: when the user becomes eligible again, new work is queued.