> 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/fundamentals.md).

# API fundamentals

How to authenticate against the OpenIAM REST API, which response envelope each kind of endpoint returns, how failures are reported, and how to find the endpoint you need.

Four things are the same for every call: how you authenticate, what a response looks like, how failures are reported, and how to find the endpoint you need.

**Read this before writing any integration.** Two of the four behave differently from what most REST clients assume, and both produce failures that surface later than the mistake.

## What this builds, and how long it takes

|                |                                                                                          |
| -------------- | ---------------------------------------------------------------------------------------- |
| **You get**    | An authenticated call against your deployment, and code that handles responses correctly |
| **Time**       | 30 minutes                                                                               |
| **You need**   | A deployment, and permission to register an OAuth client                                 |
| **Reversible** | Yes — nothing here changes data                                                          |

## Before you start

* **A running deployment** you can reach over HTTPS.
* **Permission to create an authentication provider** in the console, or someone who can create one for you. Step 1 covers what it needs.
* **Somewhere to keep a client secret.** Not a configuration file, and not a repository.

## Set these first

```bash
export OPENIAM_URL=<the address your deployment is published on>
export OPENIAM_CLIENT_ID=<the client id generated when you create the provider>
```

***

## Build it

### 1. Register a client and get a token

**The API is secured with OAuth.** An integration authenticates as a registered OAuth client, not as a person — so before the first call there is a one-time setup step in the console.

**Create the client.** Webconsole → **Access Control** → **Authentication Provider** → **Create New Provider**, and choose **oAuth Client**.

| Field                          | Set it to                                                                                                   |
| ------------------------------ | ----------------------------------------------------------------------------------------------------------- |
| **Provider name**              | Something that identifies the integration. It appears in the audit log                                      |
| **Authorization Grant Flow**   | **`Client Credentials`** for a server-to-server integration                                                 |
| **Client Authentication Type** | `Basic Authentication`                                                                                      |
| **Default Scopes**             | The paths the client may reach, plus `user_name`. Scope it to what the integration needs, not to everything |
| **Token expiration**           | The idle timeout, in minutes                                                                                |
| **Use refresh token**          | On, unless the integration can re-authenticate cheaply                                                      |
| **Protect by 2FA**             | Off for an unattended integration                                                                           |

**Saving generates a client id and a client secret.** Put them straight into your credential store. The secret is shown so that you can copy it, not so that it can live in a configuration file or a repository.

**Then exchange them for a token:**

```
POST {server}/idp/oauth2/token
Authorization: Basic <base64 of clientId:clientSecret>
Content-Type: application/x-www-form-urlencoded

grant_type=client_credentials
```

The response carries `access_token`. Send it as `Authorization: Bearer <access_token>` on every subsequent call.

**The token endpoints are the one part of the API you reach without a token** — you cannot present a token in order to obtain one. Everything else expects the `Authorization` header.

**Scopes are what make this safer than a password.** A client scoped to the paths it needs cannot reach the rest of the API, and revoking it is deleting one provider rather than rotating an account's password and finding everything that used it.

**Tokens expire.** Handle a `401` by fetching a new token and retrying once — not by treating it as a failure of the call. [*Tokens*](/developer-guide/tokens.md) covers inspecting, refreshing and revoking them.

### 2. Know which response envelope you are reading

**Responses are wrapped, and there is more than one wrapper.** Which one you get depends on the kind of endpoint you called. **This is the single most common cause of an integration that appears to work and silently does not**, so it is worth learning before you write any response handling.

| Envelope                | Returned by                                                                                                    | Recognise it by                                   |
| ----------------------- | -------------------------------------------------------------------------------------------------------------- | ------------------------------------------------- |
| **`BasicAjaxResponse`** | Operations that **change** something — everything under `/rest/api/prov`, and most saves and actions elsewhere | `status`, `errorList`                             |
| **`BeanResponse`**      | **Searches and paged reads** — for example `POST /rest/api/users/search`                                       | `beans`, `page`, `size`                           |
| **`Response`**          | The **internal service layer**, not the REST API. You meet it in Groovy scripts, which call services directly  | `status: "success"`, `errorCode`, `fieldMappings` |

**Do not write response handling against `Response` if you are calling the REST API.** It is the contract between the platform's own components; a REST controller receives one and translates it into a `BasicAjaxResponse` before answering you. Groovy script authors do see it — see [*How scripting works*](/developer-guide/how-scripting-works.md).

#### `BasicAjaxResponse` — anything that changes state

| Field                            | Is                                                                                                                   |
| -------------------------------- | -------------------------------------------------------------------------------------------------------------------- |
| `status`                         | **An integer, and it carries the real outcome.** It is an HTTP-style code — `200`, `400`, `401`, `403`, `404`, `500` |
| `errorList`                      | The failures, each with an `error` code, an optional `validationError` naming the field, and a message               |
| `primaryKey`                     | The identifier of the record the operation acted on, where the operation produces one                                |
| `successMessage`, `successToken` | Human-readable confirmation, for interfaces rather than integrations                                                 |
| `contextValues`                  | Additional values an operation chooses to return                                                                     |
| `possibleErrors`                 | The errors the operation can produce                                                                                 |
| `stackTrace`                     | Diagnostic text, when the deployment is configured to return it. Not part of the contract — do not read it           |

**The HTTP status is not the outcome.** These endpoints answer `200` and put the real result in `status` inside the body. A request that was understood, processed and refused arrives as an HTTP `200` carrying `status: 401` or `status: 400`. **Code that checks only the HTTP status treats a refused save as a successful one.**

```python
resp = requests.post(url, json=payload, headers=headers)
resp.raise_for_status()                     # transport problems only

body = resp.json()
if body.get("status") != 200:               # the real outcome
    raise RuntimeError(body.get("errorList"))
```

**Note the comparison is to the integer `200`, not to a string.** There is no `"success"` value in this envelope.

#### `BeanResponse` — searches and paged reads

| Field                              | Is                                                                |
| ---------------------------------- | ----------------------------------------------------------------- |
| `beans`                            | The results                                                       |
| `beansMap`                         | The results grouped, where an endpoint returns more than one kind |
| `from`, `size`, `pageSize`, `page` | The paging window that produced them                              |
| `emptySearchBean`                  | Set when the search carried no criteria the endpoint could act on |
| `error`                            | The failure, when there is one                                    |

**An empty result and a rejected search look different.** A search whose criteria were all blank comes back with `emptySearchBean` set rather than returning the whole population — so check that field before concluding that nothing matched. **Searches take a structured body and page with `from` and `size`.**

### 3. Handle errors by code

**Errors are identified by a code, not by a message.** They are specific — `ACCESS_DENIED`, `INVALID_PASSWORD`, `ACCOUNT_LOCKED`, `ACCOUNT_DISABLED` — and a code tells you what to do far more precisely than an HTTP status.

There are two code sets, matching the two layers:

| Code set       | Where you see it                                    |       Size |
| -------------- | --------------------------------------------------- | ---------: |
| `Errors`       | In `errorList[].error`, on REST responses           | 879 values |
| `ResponseCode` | On the internal service layer, so in Groovy scripts | 554 values |

**Do not enumerate them in your integration.** Handle the few your workflow can act on, log the code for everything else, and let the rest surface intact. A code you have never seen is more useful in a log than a generic message you substituted for it.

**When a save is refused, read `errorList` first.** An entry carrying `validationError` names the field that failed, which is usually the whole answer.

### 4. Find the endpoint you need

**Your deployment serves its own API description.** The platform generates an OpenAPI specification from the running code and serves it from the deployment — so it describes the exact version you are running, which no written reference can.

Each front end serves its own. Prefix the path with your host, for example `https://$OPENIAM_URL/webconsole/swagger-ui`:

| Front end             | Browsable view            | Specification           |
| --------------------- | ------------------------- | ----------------------- |
| **Webconsole**        | `/webconsole/swagger-ui`  | `/webconsole/api-docs`  |
| **Identity Provider** | `/idp/swagger-ui`         | `/idp/api-docs`         |
| **SelfService**       | `/selfservice/swagger-ui` | `/selfservice/api-docs` |

You can also reach them from the console under **Administration → Documentation links**.

{% hint style="info" %}
These endpoints are enabled by the `documentation` Spring profile, which is **active by default**. If a deployment overrides `spring.profiles.active` and omits `documentation`, both the Swagger UI and the `/api-docs` endpoints are disabled — add the profile back to re-enable them.
{% endhint %}

**The specification is grouped by area, and the group is how you navigate it.** Each group covers one path prefix:

| Group                                               | Path prefix                                                      |
| --------------------------------------------------- | ---------------------------------------------------------------- |
| `user`                                              | `/rest/api/users`                                                |
| `provisioning`                                      | `/rest/api/prov`                                                 |
| `oauth2`                                            | `/oauth2`                                                        |
| `group`, `role`, `organization`                     | `/rest/api/groups`, `/rest/api/roles`, `/rest/api/organizations` |
| `resource`, `policy`                                | `/rest/api/resources`, `/rest/api/policy`                        |
| `managed-system`, `connector`                       | `/rest/api/managedsys`, `/rest/api/connector`                    |
| `sync-config`, `access-certification`               | `/rest/api/syncconfig`, `/rest/api/access-certifications`        |
| `audit-log`                                         | `/rest/api/auditlog`                                             |
| `groovy-manager`                                    | `/rest/api/groovyManager`                                        |
| `batch`                                             | `/rest/api/batch`                                                |
| `password`, `challenge-response`, `fido`, `ssh-key` | the identity provider's own paths                                |

**Generate a client against the deployment you will integrate with.** The API changes between releases, and a client generated from a different one fails at runtime with type errors rather than at build time.

**The Webconsole is the other reliable way in.** Everything the console does, it does through this API. If you can perform an action in the console and cannot find the endpoint for it, watch the request the console makes — it is a client like any other.

***

## Verify it

```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`.** Supply the secret from your credential store — not by typing it, where it enters shell history.

Then make one authenticated read and confirm you can find the results in the field the envelope actually uses:

```bash
curl -s -X POST "https://$OPENIAM_URL/webconsole/rest/api/users/search" \
  -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
  -d '{"lastName":"a","from":0,"size":5}' \
  | jq '{returned: (.beans | length), emptySearchBean, error}'
```

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

## If it goes wrong

| What you see                                      | What it means                                                                      | What to do                                                                 |
| ------------------------------------------------- | ---------------------------------------------------------------------------------- | -------------------------------------------------------------------------- |
| The token request returns `401`                   | The client id or secret is wrong, or Basic authentication is not encoded correctly | Confirm the pair, and that the header is base64 of `clientId:clientSecret` |
| The token request succeeds but calls return `403` | The client's scopes do not cover the path                                          | Widen the scope on the provider, to what is needed and no more             |
| `401` on a call that worked before                | The token has expired                                                              | Refresh it — [*Tokens*](/developer-guide/tokens.md)                        |
| HTTP `200` but nothing was saved                  | The code is reading the HTTP status instead of `status` in the body                | Branch on `status == 200` inside the body                                  |
| `status` is missing from a response               | You are reading a search result, which uses `BeanResponse`                         | Read `beans` and `error`                                                   |
| `errorCode` and `fieldMappings` are always null   | Those belong to the internal `Response`, not to a REST response                    | Read `errorList`                                                           |
| A search returns nothing although the data exists | The criteria were all blank, so nothing was searched                               | Check `emptySearchBean`, and set at least one criterion                    |
| An error code you do not recognise                | The platform is specific about failures                                            | Log it, and look the operation up in your deployment's own specification   |

## What this procedure does not cover

Worked integrations in a specific language. Also not covered: OAuth and OpenID Connect flows beyond obtaining a token, which are the identity provider's own subject.

## Related

* [*Your first calls*](/developer-guide/your-first-calls.md) — the walkthrough this page prepares you for
* [*Tokens*](/developer-guide/tokens.md) — inspecting, refreshing, revoking, and signing out
* [*Managing users*](/developer-guide/user-lifecycle.md) — the provisioning API
* [*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/fundamentals.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.
