> For the complete documentation index, see [llms.txt](https://docs.openiam.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.openiam.com/developer-guide/user-lifecycle.md).

# Managing users

The provisioning API — create and update users, move them through their lifecycle, manage passwords, and read the result correctly.

**Everything that changes a user goes through `/rest/api/prov`.** This is the API an integration automates: joiners, movers, leavers, entitlements and passwords.

**Look here rather than under `/rest/api/users`.** That path serves searches, lookup lists and read-only relationships — it has no create, update or delete. The two are easy to confuse and the distinction is worth learning once.

## What this covers

|               |                                                                                               |
| ------------- | --------------------------------------------------------------------------------------------- |
| **You get**   | The operations, their request bodies, the rights a caller needs, and how to read the result   |
| **You need**  | A token — [*API fundamentals*](/developer-guide/fundamentals.md) — and the rights named below |
| **Base path** | `/rest/api/prov`, served by the interfaces                                                    |
| **Method**    | `POST` for everything except one profile-picture operation                                    |

## Before you start

* **A token**, and a client whose scopes cover `/rest/api/prov`.
* **The rights the operation requires.** These are authorisation rights held by the requester, checked inside the operation — see *Rights* below.
* **A test deployment for your first attempts.** These operations change real users and propagate to connected systems.

## Set these first

```bash
export OPENIAM_URL=<the address your deployment is published on>
export TOKEN=<a bearer token>
```

***

## The operations

**Every one is a `POST` with a JSON body**, except `DELETE /images/{id}`. None of them takes the user identifier as a query parameter — **the identifier goes in the body.**

### Creating and updating

| Operation                                     | Body                        |
| --------------------------------------------- | --------------------------- |
| `POST /rest/api/prov/saveUser`                | `EditUserModel`             |
| `POST /rest/api/prov/template/user`           | `EditUserModel`             |
| `POST /rest/api/prov/entitle`                 | `EditUserEntitlementsModel` |
| `POST /rest/api/prov/create-user-from-orphan` | `OrphanToUserModel`         |

**`saveUser` both creates and updates, and which one it does depends on the body.** When `id` is absent or blank the operation creates a user; when `id` is present it updates that user. The audit record reflects whichever happened. **There is no separate create endpoint** — if you send an update without `id`, you get a second user rather than an error.

`EditUserModel` is the largest body in the API and every field on it is documented in your deployment's own specification. The fields that most often need explaining:

| Field                      | Is                                                                                                                                                |
| -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
| `metadataTypeId`           | Which kind of user this is. The type governs which attributes apply, and your deployment defines the available types                              |
| `notifyUserViaEmail`       | **Sends mail when true.** This is the flag that emails a few thousand people when a bulk load goes wrong. Default is `false`; set it deliberately |
| `notifySupervisorViaEmail` | The same, for the supervisor                                                                                                                      |
| `emailAddresses`, `phones` | Typed and multi-valued — `typeId` selects which one, and a user can carry several                                                                 |

### Lifecycle transitions

**These ten operations all take the same body**, `ActionUserModel`:

| Operation                             |                                                           |
| ------------------------------------- | --------------------------------------------------------- |
| `POST /rest/api/prov/activateUser`    |                                                           |
| `POST /rest/api/prov/deactivate-user` | note the hyphen; the others in this group do not have one |
| `POST /rest/api/prov/enableUser`      |                                                           |
| `POST /rest/api/prov/disableUser`     |                                                           |
| `POST /rest/api/prov/removeUser`      |                                                           |
| `POST /rest/api/prov/resetUser`       |                                                           |
| `POST /rest/api/prov/terminateUser`   |                                                           |
| `POST /rest/api/prov/deceasedUser`    | marks a user as deceased                                  |
| `POST /rest/api/prov/leaveWithPay`    |                                                           |
| `POST /rest/api/prov/leaveOfAbsence`  |                                                           |

#### `ActionUserModel`

| Field                              | Is                                                                                                                                  |
| ---------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
| `userId`                           | The user to act on. **Required, and it goes in the body**                                                                           |
| `performDataString`                | When to perform the action. **See the format below — it is not obvious**                                                            |
| `performNow`                       | Whether to act immediately rather than at `performDataString`                                                                       |
| `reason`                           | Free text recorded with the action                                                                                                  |
| `description`                      | Free text. Where an operation also writes a user attribute, this carries the value                                                  |
| `attrName`                         | Names a user attribute to write as part of the action. Honoured by some operations only; when set, `description` supplies the value |
| `skipProvisioningManagedSystemSet` | Managed system identifiers to leave untouched. **The user changes in OpenIAM and those systems do not**, so use it knowingly        |

#### The date format

`performDataString` is parsed with the pattern:

```
yyyy-MM-dd'T'HH:mm:ss.SSS'Z'
```

So a value looks like `2026-09-01T00:00:00.000Z`. **The offset is ignored and the value is taken as UTC** — sending a local time with a different offset does not shift it, it is read as though it were UTC.

**Use `performNow` for immediate actions** rather than sending the current time.

### Passwords

| Operation                                            | Body                                                                                                                                              |
| ---------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
| `POST /rest/api/prov/resetPassword`                  | `ResetPasswordBean`                                                                                                                               |
| `POST /rest/api/prov/resyncPassword`                 | `ResetPasswordBean` — pushes the OpenIAM password to every target system                                                                          |
| `POST /rest/api/prov/changePasswordForManagedSystem` | `ResetPasswordBean` — one managed system                                                                                                          |
| `POST /rest/api/prov/reset-bulk-password`            | `ResetPasswordBean` — several users                                                                                                               |
| `POST /rest/api/prov/admin-reset-bulk-password`      | `ResetPasswordBean` — several users, administrative                                                                                               |
| `POST /rest/api/prov/changePassword`                 | `RelatedAccountsPasswordResetRequest` — the user's password and related accounts                                                                  |
| `POST /rest/api/prov/syncPasswordFromSrc`            | `SyncPasswordFromSource` — brings a password **into** OpenIAM from a target system, which is how a connector reports a password changed at source |

**`syncPasswordFromSrc` runs in the opposite direction from the others.** Everything else pushes outward; this one accepts a change that happened elsewhere.

**Never log a request body from this group.** They carry passwords.

### Other

| Operation                                  | Body                                                 |
| ------------------------------------------ | ---------------------------------------------------- |
| `POST /rest/api/prov/update-principal`     | `EditLoginBean` — a login's status                   |
| `POST /rest/api/prov/saveAlternateContact` | `AlternateContactBean` — the out-of-office assistant |
| `POST /rest/api/prov/images/{id}`          | The profile picture, as a request parameter          |
| `DELETE /rest/api/prov/images/{id}`        | Removes it. **The only non-`POST` operation here**   |

***

## Rights

**These operations check the requester's authorisation rights, and a valid token is not sufficient on its own.** The pattern, using `disableUser` as the worked example: the requester must hold **`USER_EDIT_INFO`** and **`DISABLE_USER`**; failing that, the operation is still permitted when the target user is one of the requester's subordinates. `enableUser` applies the same rule with `ENABLE_USER`.

**A refusal comes back as `status: 401` with `ACCESS_DENIED` in `errorList`** — not as an HTTP error.

**Some operations additionally refuse to act on a terminated user** when the request arrives through a self-service context. If a lifecycle call is refused for a user you expect to be actionable, check the user's status before checking your rights.

**Which right each operation requires is recorded in your deployment's own specification.** Grant the integration's account only the rights the operations it calls actually need.

***

## Reading the result

**Every operation here returns `BasicAjaxResponse`, and the HTTP status is always `200`.** The real outcome is the integer `status` inside the body.

```python
body = resp.json()
if body.get("status") != 200:
    raise RuntimeError(body.get("errorList"))
```

| Field            | Use it for                                                                                                             |
| ---------------- | ---------------------------------------------------------------------------------------------------------------------- |
| `status`         | The outcome — `200`, or `400`, `401`, `403`, `404`, `500`                                                              |
| `errorList`      | What went wrong. An entry's `validationError` names the field that failed                                              |
| `contextValues`  | Values the operation returns. `saveUser` puts the user's identifier in `contextValues.userId`                          |
| `successMessage` | On a save, the managed systems the user was provisioned to and the state of each account                               |
| `primaryKey`     | Set by some endpoints elsewhere in the API. **The provisioning endpoints do not set it** — read `contextValues.userId` |

**These calls are synchronous.** The platform performs the provisioning operation and answers with its result — the response is not a mere acknowledgement that the request was queued. Asynchronous provisioning exists in the platform, but it is used by its own bulk paths — synchronisation, reconciliation and self-registration — and not by these endpoints.

**A successful response therefore means the operation completed as far as the platform is concerned.** Where a connected system was unreachable, that surfaces in the response and in the audit log rather than silently later.

**That is not the same as the account existing in every target system yet.** The response reports each managed system's account state in `successMessage`, and an account still being written reads as `PENDING_CREATE` — or `PENDING_UPDATE`, `PENDING_DELETE`, `PENDING_DISABLE`, `PENDING_ENABLE`. Read that rather than inferring target-system state from `status` alone.

## Pre- and post-processing

**Each of these operations raises a pre-event before it acts and a post-event after it succeeds.** A pre-processor can **stop the operation**: returning the break signal aborts it before anything is changed, and the caller receives a failure. This is the extension point to use when an operation must be validated against something OpenIAM does not know about.

Pre- and post-processors are Groovy, and they are written the same way as the other script types — see [*How scripting works*](/developer-guide/how-scripting-works.md).

***

## Verify it

Create a user, read the identifier back, then disable it:

```bash
AUTH=(-H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json')

curl -s -X POST "https://$OPENIAM_URL/webconsole/rest/api/prov/saveUser" "${AUTH[@]}" \
  -d '{"firstName":"Api","lastName":"Check","metadataTypeId":"<your user type>",
       "notifyUserViaEmail":false}' \
  | jq '{status, userId: .contextValues.userId, errorList}'
```

**Expected:** `status: 200`, an id in `contextValues.userId`, and `errorList` null or empty.

```bash
USER_ID=<the contextValues.userId from above, or an id from a search>

curl -s -X POST "https://$OPENIAM_URL/webconsole/rest/api/prov/disableUser" "${AUTH[@]}" \
  -d "{\"userId\":\"$USER_ID\",\"performNow\":true,\"reason\":\"api check\"}" \
  | jq '{status, errorList}'
```

**Expected:** `status: 200`. A `401` with `ACCESS_DENIED` means the requester lacks `DISABLE_USER` or `USER_EDIT_INFO`.

## If it goes wrong

| What you see                                           | What it means                                                                   | What to do                                                                              |
| ------------------------------------------------------ | ------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------- |
| HTTP `405`                                             | The request used `PUT` or `GET`                                                 | Every operation here is `POST`, except `DELETE /images/{id}`                            |
| The operation ignores the user you named               | The identifier was sent as a query parameter                                    | It goes in the JSON body, as `userId`                                                   |
| `status: 401`, `ACCESS_DENIED` in `errorList`          | The requester lacks a required right                                            | Grant the right, or act on a subordinate                                                |
| `status: 400` with a `validationError`                 | A field failed validation                                                       | The entry names the field                                                               |
| A second user appears instead of an update             | `saveUser` was sent without `id`                                                | Include `id` to update                                                                  |
| A scheduled action never happens                       | `performDataString` was not in the expected format, or `performNow` overrode it | Use `yyyy-MM-dd'T'HH:mm:ss.SSS'Z'`, and leave `performNow` false when scheduling        |
| A scheduled action happens at the wrong time           | The value was sent with a local offset                                          | The value is read as UTC; convert before sending                                        |
| The user changes in OpenIAM but not in a target system | `skipProvisioningManagedSystemSet` named it                                     | Remove it from the set                                                                  |
| `status` is missing entirely                           | The response is a search result, not a provisioning result                      | Searches return `BeanResponse` — [*API fundamentals*](/developer-guide/fundamentals.md) |
| Mail went out unexpectedly                             | `notifyUserViaEmail` or `notifySupervisorViaEmail` was true                     | Set both deliberately on every call                                                     |

## What this procedure does not cover

Entitlement design — which roles, groups and resources exist and what they grant — and the approval workflows that may sit in front of a request. Those are the [Administration Guide](/administration-guide/admin.md). Bulk loading a population, which is synchronisation rather than provisioning.

## Related

* [*API fundamentals*](/developer-guide/fundamentals.md) — authentication and the response envelopes
* [*Tokens*](/developer-guide/tokens.md)
* [*Your first calls*](/developer-guide/your-first-calls.md) — a worked end-to-end sequence
* [*Common errors*](/developer-guide/common-errors.md)

*Applies to OpenIAM 2026.8.1.*


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation by asking a question.

Perform an HTTP GET request on the following URL with the `ask` and `goal` query parameters:

```
GET https://docs.openiam.com/developer-guide/user-lifecycle.md?ask=<question>&goal=<user_goal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is what the user is ultimately trying to achieve, the reason they need the answer. Sharing it helps GitBook give you a better, more relevant answer. A goal is most helpful when it describes the outcome the user wants rather than restating the question. For example, with `ask=how do I create an API token`, a goal like `automate deployments from our CI pipeline` lets GitBook tailor the answer to that use case.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
