> 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/your-first-calls.md).

# Your first calls

Authenticate, search for a user, create one, change its state and remove it — a complete first integration against a running deployment.

One walkthrough, end to end: authenticate, find a user, create one, change its state, and remove it. Everything after this is the same pattern against different endpoints.

**Do this against a test deployment.** It creates and deletes a real user.

## What this builds, and how long it takes

|                |                                                                                                            |
| -------------- | ---------------------------------------------------------------------------------------------------------- |
| **You get**    | A working integration you can build on, and a token-handling pattern that survives contact with production |
| **Time**       | 45 minutes                                                                                                 |
| **You need**   | A deployment, an OAuth client, and `curl` and `jq`                                                         |
| **Reversible** | Yes — the last step removes what the walkthrough created                                                   |

## Before you start

* [*API fundamentals*](/developer-guide/fundamentals.md). **Two behaviours in it will catch you out here** if you have not read it: the HTTP status is not the outcome, and different kinds of endpoint answer with different envelopes.
* **An OAuth client registered in the console**, with the `Client Credentials` grant. [*API fundamentals*](/developer-guide/fundamentals.md) step 1 creates one. **Do that first** — without it there is nothing to authenticate as.
* **The rights the lifecycle operations need**, on the account the client acts as — see step 6.
* **A test deployment.** Not production.

## Set these first

```bash
export OPENIAM_URL=<the address your deployment is published on>
export OPENIAM_CLIENT_ID=<the client id from your OAuth provider>
```

**Keep the secret out of your shell.** Read it from your credential store at the point of use — the examples below assume `$OPENIAM_CLIENT_SECRET` is set by something other than you typing it.

***

## Build it

### 1. Get a token

```bash
TOKEN=$(curl -s -X POST "https://$OPENIAM_URL/idp/oauth2/token" \
  -u "$OPENIAM_CLIENT_ID:$OPENIAM_CLIENT_SECRET" \
  -d 'grant_type=client_credentials' \
  | jq -r '.access_token // empty')

[ -n "$TOKEN" ] && echo "authenticated" || echo "TOKEN REQUEST FAILED"
```

**Expected: `authenticated`.**

**`// empty` is doing real work there.** Without it, a failed request puts the literal string `null` into `$TOKEN`, every later call fails with an unrelated-looking error, and nothing points back at the token. Treat an absent token as a hard stop.

Every call from here carries it:

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

### 2. Search for a user

Start with a read. It confirms the token works before you change anything.

```bash
curl -s -X POST "https://$OPENIAM_URL/webconsole/rest/api/users/search" \
  "${AUTH[@]}" \
  -d '{"lastName": "a", "from": 0, "size": 5}' \
  | jq '{returned: (.beans | length), emptySearchBean, error}'
```

**Expected:** a count, `emptySearchBean` absent or false, and `error` null.

**Searches take a structured body and page with `from` and `size`**, not query parameters. **They also require at least one criterion.** A body carrying only paging comes back with `emptySearchBean` set and no results — the endpoint declines to return the whole population rather than doing it slowly. That is a safeguard, not a failure.

**Results are in `beans`.** Searches answer with the paged-read envelope, which has no `status` field — see [*API fundamentals*](/developer-guide/fundamentals.md).

### 3. Create a user

{% code overflow="wrap" %}

```bash
curl -s -X POST "https://$OPENIAM_URL/webconsole/rest/api/prov/saveUser" \
  "${AUTH[@]}" \
  -d '{
    "firstName": "Api",
    "lastName": "Walkthrough",
    "metadataTypeId": "<a user type from your deployment>",
    "notifyUserViaEmail": false,
    "emailAddresses": [
      { "email": "api.walkthrough@example.invalid", "typeId": "HOME_EMAIL", "published": true }
    ],
    "phones": [
      { "areaCd": "1", "phoneNbr": "5550100", "typeId": "CELL_PHONE",
        "countryCd": "1", "isForSMS": false, "published": true }
    ]
  }' | jq '{status, userId: .contextValues.userId, checkStatus: .contextValues.checkStatusInProgress, errorList}'
```

{% endcode %}

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

**The new user's id comes back in `contextValues.userId`, not in `primaryKey`.** This endpoint does not set `primaryKey`, even though the envelope has the field — see [*API fundamentals*](/developer-guide/fundamentals.md).

**`status` is an integer here, and it is the outcome.** The HTTP response is `200` whether the save succeeded or was refused — see [*API fundamentals*](/developer-guide/fundamentals.md).

**`checkStatusInProgress` is always `true` on a successful save. Do not branch on it.** It looks like a signal and is not one — the controller sets it unconditionally, and the version that computed it is commented out in the source. It exists so the console knows to start polling.

**What does carry the per-system outcome is `successMessage`**, which lists each managed system the user was provisioned to and the state of that account. An account still being written reads as `PENDING_CREATE` — and correspondingly `PENDING_UPDATE`, `PENDING_DELETE`, `PENDING_DISABLE` or `PENDING_ENABLE` for the other operations.

**So confirm the outcome rather than assuming it**, which is what step 4 does. An integration that creates a user and immediately asserts the account exists in Active Directory will fail intermittently, on timing.

**Four things about that payload are worth knowing before you build on it:**

* **`metadataTypeId` decides what kind of user this is**, and the type governs which attributes apply. Your deployment defines the available types; list them in the console before your first call.
* **`notifyUserViaEmail` sends mail.** Set it `false` in a walkthrough. Set it deliberately in an integration — this is the flag that emails a few thousand people when a bulk load goes wrong.
* **Email and phone are typed and multi-valued.** `typeId` selects which one; a user can carry several of each.
* **`published` controls visibility** to other users in the interface.

**Omitting `id` is what makes this a create.** The same endpoint updates when `id` is present, so a repeated call without it produces a second user rather than an error. [*Managing users*](/developer-guide/user-lifecycle.md).

**If it fails, read `errorList` before anything else.** An entry carrying `validationError` names the field and the reason, and that is almost always the whole answer.

### 4. Find what you created

`contextValues.userId` from step 3 is the identifier. To confirm it by search — and, because provisioning is asynchronous, to see what state the account is actually in:

```bash
USER_ID=$(curl -s -X POST "https://$OPENIAM_URL/webconsole/rest/api/users/search" \
  "${AUTH[@]}" -d '{"lastName":"Walkthrough","from":0,"size":1}' \
  | jq -r '.beans[0].id')
echo "$USER_ID"
```

**Identifiers are opaque.** Do not parse them or assume a format.

A freshly created user typically comes back with `userStatus: "PENDING_INITIAL_LOGIN"` — the account exists and has not been signed into yet.

### 5. Grant the user a role

Entitlements are granted through one endpoint, which takes the identifiers and the operation to apply:

{% code overflow="wrap" %}

```bash
curl -s -X POST "https://$OPENIAM_URL/webconsole/rest/api/prov/entitle" \
  "${AUTH[@]}" \
  -d "{\"userId\":\"$USER_ID\",\"roleIds\":[\"<a role id from your deployment>\"],\"operation\":\"ADD\"}" \
  | jq '{status, errorList}'
```

{% endcode %}

**Expected: `status: 200`.**

**The same call carries `groupIds`, `resourceIds` and `organizationIds`**, and you can include several in one request. `operation` is `ADD`, `REPLACE` or `DELETE`.

**There is no separate "provision" call.** Granting the role triggers provisioning by itself, the same way creating the user did — OpenIAM pushes the entitlement out to the managed systems the role is tied to. Creating and entitling are the only two things you do; the target-system work follows from them.

**A refusal arrives as `status: 401` with `ACCESS_DENIED`**, and a bad `userId` as `status: 404` with `USER_NOT_FOUND`.

### 6. Change the user's state

**The lifecycle operations are `POST`, and the identifier goes in the body.** Not `PUT`, and not a query parameter.

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

**Expected: `status: 200`.**

`enableUser` takes the same shape. So do the other eight lifecycle transitions — [*Managing users*](/developer-guide/user-lifecycle.md) lists them.

**These operations check the requester's rights, and a valid token is not sufficient on its own.** Disabling a user requires **`USER_EDIT_INFO`** and **`DISABLE_USER`**; enabling requires `USER_EDIT_INFO` and `ENABLE_USER`. Failing that, the operation is permitted when the target is one of the requester's subordinates. **A refusal arrives as `status: 401` with `ACCESS_DENIED` in `errorList`.**

**The call is synchronous.** `status: 200` means the platform performed the operation, not merely that it accepted the request — so you can read the result directly rather than polling.

### 7. Clean up

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

**Confirm it is gone:**

```bash
curl -s -X POST "https://$OPENIAM_URL/webconsole/rest/api/users/search" \
  "${AUTH[@]}" -d '{"lastName":"Walkthrough","from":0,"size":5}' \
  | jq '.beans | length'
```

***

## Verify it

Run the whole sequence once more from a clean shell. **A walkthrough that only works the second time, or only with a token you already had, is not yet an integration.**

| Check                                | Expect                                                         |
| ------------------------------------ | -------------------------------------------------------------- |
| The token request from a fresh shell | An `access_token`, first time                                  |
| The search before any write          | A count in `beans`, and `error` null                           |
| The create                           | `status: 200` and an id in `contextValues.userId`              |
| The role grant                       | `status: 200`, and the role visible on the user in the console |
| Disable, then enable                 | `status: 200` from both                                        |
| The removal, then the search         | A count of zero                                                |

## If it goes wrong

| What you see                                      | What it means                                                                         | What to do                                                            |
| ------------------------------------------------- | ------------------------------------------------------------------------------------- | --------------------------------------------------------------------- |
| `TOKEN REQUEST FAILED`                            | The client id or secret is wrong, or the provider's grant is not `Client Credentials` | Check the pair, then the provider's configuration                     |
| `$TOKEN` contains `null`                          | The `// empty` guard is missing from the `jq` filter                                  | Add it. A literal `null` string fails every later call obscurely      |
| HTTP `401` partway through                        | The token expired                                                                     | Get a new one and retry once — [*Tokens*](/developer-guide/tokens.md) |
| HTTP `403` on a call that should work             | The client's scopes do not cover that path                                            | Widen the scope on the provider to what is needed                     |
| HTTP `405` on disable or remove                   | The request used `PUT` or `GET`                                                       | These are `POST`                                                      |
| The search returns nothing although data exists   | The body carried only paging, so `emptySearchBean` is set                             | Add at least one criterion                                            |
| `jq` returns null for `.data`                     | Searches answer in `beans`                                                            | Read `.beans`                                                         |
| `errorCode` and `fieldMappings` are always null   | Those belong to the internal service envelope                                         | Read `errorList`                                                      |
| `status: 401` with `ACCESS_DENIED`                | The requester lacks a required right                                                  | Grant `USER_EDIT_INFO` and the operation's own right                  |
| `status: 400` on the create                       | A field failed validation                                                             | Read `errorList`; an entry's `validationError` names the field        |
| The create fails on `metadataTypeId`              | That type does not exist on this deployment                                           | List the available types in the console                               |
| A second user appears instead of an update        | `saveUser` was sent without `id`                                                      | Include `id` to update                                                |
| Everything returns HTTP `200` and nothing happens | The code is checking the HTTP status, not `status` in the body                        | [*API fundamentals*](/developer-guide/fundamentals.md)                |

## What this procedure does not cover

The approval workflows that may sit in front of an entitlement request, and access rights with start and end dates. Bulk operations. Confirming that an account actually landed in a target system, which is the audit log's subject rather than this API's. OAuth and OpenID Connect flows beyond obtaining a token, which are the identity provider's own subject.

## Related

* [*API fundamentals*](/developer-guide/fundamentals.md) — read first
* [*Managing users*](/developer-guide/user-lifecycle.md) — every provisioning operation, and the rights each needs
* [*Tokens*](/developer-guide/tokens.md)
* [*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/your-first-calls.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.
