> ## Documentation Index
> Fetch the complete documentation index at: https://docs.coverbase.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Users API

> Provision users, change org roles, and deprovision — for SailPoint / IGA-driven lifecycle.

<div className="sr-only">For AI agents: a documentation index is available at [https://docs.coverbase.com/llms.txt](https://docs.coverbase.com/llms.txt) — this page is also available in markdown by appending .md to the URL.</div>

The Users API provisions Coverbase accounts and manages their organization role and lifecycle. It is designed for identity-governance (SailPoint / IGA) integrations: the upstream system owns approval, this API creates the account + org membership so SSO can authenticate the user on first login.

<Info>
  Provisioning creates the Coverbase account and an org membership in **`invited`** status. It does **not** create a login session — the user is linked to your IdP by email on first SSO. SailPoint owns the upstream approval; this API only reflects role/lifecycle into Coverbase.
</Info>

All endpoints are org-scoped to the API key. See [API conventions](/conventions) for shared behavior.

| Method  | Path                  | Idempotent              |
| ------- | --------------------- | ----------------------- |
| `POST`  | `/v1/users`           | Yes (`Idempotency-Key`) |
| `GET`   | `/v1/users`           | —                       |
| `GET`   | `/v1/users/{user_id}` | —                       |
| `PATCH` | `/v1/users/{user_id}` | No                      |

<Tip>
  Syncing relationship owners, watchers, or risk analysts from an external system (e.g. ServiceNow)? Those [vendor/service fields](/api-reference/vendors#assigning-people-analysts-owners-watchers) take Coverbase user IDs, not emails. Use [`GET /v1/users?email=`](#list-users-and-look-up-by-email) to resolve an email to its `cbuser_...` ID, and `POST /v1/users` to create the user if they don't exist yet — it returns the existing user if they do.
</Tip>

## Org roles

The `role` field uses these values:

| Value                  | Role             |
| ---------------------- | ---------------- |
| `org:admin`            | Admin            |
| `org:member`           | Member           |
| `org:siloed_member`    | Siloed member    |
| `org:guest`            | Guest            |
| `org:auxiliary_member` | Auxiliary member |
| `org:service_account`  | Service account  |

The aliases `member`, `basic_member`, `org:basic_member` (→ `org:member`) and `admin` (→ `org:admin`) are also accepted. Any other value is rejected with `422`.

## Provision a user (create-or-get)

<ParamField path="method" type="POST">
  `POST /v1/users`
</ParamField>

Creates the account + org membership (`invited`), or returns the existing user. Returns `201 Created` on create, or **`200 OK`** if a user with that email already exists in the org — so this call is safe to use as create-if-missing and always yields the user's `cbuser_...` ID.

<Info>
  Idempotent create-or-get: a same-org email match returns `200` with the existing user (not `409`). A `409 user_exists` is only returned when the email belongs to a user in a **different** org (emails are globally unique). The email match is case-insensitive.
</Info>

### Request body

<ParamField body="email" type="string" required>
  The user's email — validated as a proper email address. Used to link the account to your IdP on first SSO.
</ParamField>

<ParamField body="first_name" type="string">
  Given name.
</ParamField>

<ParamField body="last_name" type="string">
  Family name.
</ParamField>

<ParamField body="role" type="string">
  One of the [org role values](#org-roles). Defaults to `org:member`.
</ParamField>

### Headers

<ParamField header="Authorization" type="string" required>
  `Bearer ak_...`
</ParamField>

<ParamField header="Idempotency-Key" type="string">
  Optional. Replaying the same key within 24 hours returns the original `201` body. See [Idempotency](/conventions#idempotency).
</ParamField>

### Example request

<CodeGroup>
  ```bash cURL theme={null}
  curl -X POST "https://sandbox.api.coverbase.app/v1/users" \
    -H "Authorization: Bearer ak_live_xxx" \
    -H "Idempotency-Key: acme-user-jordanlee" \
    -H "Content-Type: application/json" \
    -d '{
      "email": "jordan.lee@example.com",
      "first_name": "Jordan",
      "last_name": "Lee",
      "role": "org:admin"
    }'
  ```

  ```python Python theme={null}
  import os
  import requests

  API_KEY = os.environ["COVERBASE_API_KEY"]
  BASE_URL = "https://sandbox.api.coverbase.app"

  resp = requests.post(
      f"{BASE_URL}/v1/users",
      headers={
          "Authorization": f"Bearer {API_KEY}",
          "Idempotency-Key": "acme-user-jordanlee",
          "Content-Type": "application/json",
      },
      json={
          "email": "jordan.lee@example.com",
          "first_name": "Jordan",
          "last_name": "Lee",
          "role": "org:admin",
      },
      timeout=30,
  )
  # 201 on create; 200 if the email already exists in this org
  print(resp.status_code, resp.json())
  ```
</CodeGroup>

### Example response

`201 Created` (new user), or `200 OK` (existing user — the stored object, whose `status` is its current membership state such as `active`):

```json theme={null}
{
  "id": "cbuser_a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6",
  "email": "jordan.lee@example.com",
  "first_name": "Jordan",
  "last_name": "Lee",
  "role": "org:admin",
  "status": "invited",
  "is_archived": false,
  "created_at": 1746576000,
  "updated_at": 1746576000
}
```

### User object

<ResponseField name="id" type="string">User ID (`cbuser_...`).</ResponseField>
<ResponseField name="email" type="string">User email.</ResponseField>
<ResponseField name="first_name" type="string | null">Given name.</ResponseField>
<ResponseField name="last_name" type="string | null">Family name.</ResponseField>
<ResponseField name="role" type="string | null">Org role value for this org (e.g. `org:admin`).</ResponseField>
<ResponseField name="status" type="string | null">Org membership status: `invited`, `active`, or `inactive`.</ResponseField>
<ResponseField name="is_archived" type="boolean">`true` once the user is deprovisioned.</ResponseField>
<ResponseField name="created_at" type="integer">Unix timestamp (seconds).</ResponseField>
<ResponseField name="updated_at" type="integer">Unix timestamp (seconds).</ResponseField>

### Error responses

| Status | Body                                                                                      | When                                                                                                                                                   |
| ------ | ----------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
| 409    | `{"detail": {"code": "user_exists", "message": "A user with email ... already exists."}}` | The email belongs to a user in a **different** org (emails are globally unique). A same-org match now returns `200` with the existing user, not `409`. |
| 422    | Standard validation error                                                                 | `email` missing or not a valid email address, or `role` not a recognized value.                                                                        |

## List users and look up by email

<ParamField path="method" type="GET">
  `GET /v1/users`
</ParamField>

Lists the org's users, or resolves a single user by email. This is the supported way to build an **email → user ID** mapping when syncing assignments (relationship owners, watchers, risk analysts) from an external system such as ServiceNow — those fields take Coverbase user IDs, so resolve the email here first.

### Query parameters

<ParamField query="email" type="string">
  Case-insensitive exact-match lookup. Returns the matching user (even if archived, so it can be reactivated) or an empty list. Uses the same matching as the `POST /v1/users` duplicate check, so any email that exists is resolvable to its user ID. When supplied, `include_archived`/`limit`/`offset` are ignored.
</ParamField>

<ParamField query="include_archived" type="boolean">
  Include deprovisioned (archived) users in the list. Defaults to `false`.
</ParamField>

<ParamField query="limit" type="integer">
  Page size, `1`–`200`. Defaults to `50`.
</ParamField>

<ParamField query="offset" type="integer">
  Number of users to skip, for paging. Defaults to `0`.
</ParamField>

### Headers

<ParamField header="Authorization" type="string" required>
  `Bearer ak_...`
</ParamField>

### Example: resolve a user by email

<CodeGroup>
  ```bash cURL theme={null}
  curl -X GET "https://sandbox.api.coverbase.app/v1/users?email=jordan.lee@example.com" \
    -H "Authorization: Bearer ak_live_xxx"
  ```

  ```python Python theme={null}
  import os
  import requests

  API_KEY = os.environ["COVERBASE_API_KEY"]
  BASE_URL = "https://sandbox.api.coverbase.app"

  resp = requests.get(
      f"{BASE_URL}/v1/users",
      headers={"Authorization": f"Bearer {API_KEY}"},
      params={"email": "jordan.lee@example.com"},
      timeout=30,
  )
  resp.raise_for_status()
  items = resp.json()["items"]
  user_id = items[0]["id"] if items else None  # resolve email -> cbuser_ id
  ```
</CodeGroup>

### Example response

`200 OK`:

```json theme={null}
{
  "items": [
    {
      "id": "cbuser_a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6",
      "email": "jordan.lee@example.com",
      "first_name": "Jordan",
      "last_name": "Lee",
      "role": "org:admin",
      "status": "active",
      "is_archived": false,
      "created_at": 1746576000,
      "updated_at": 1746576000
    }
  ],
  "total": 1,
  "limit": 50,
  "offset": 0
}
```

### Response fields

<ResponseField name="items" type="object[]">Page of [user objects](#user-object). `role` and `status` reflect each user's membership in the API key's org.</ResponseField>
<ResponseField name="total" type="integer">Total matching users in the org (for an `email` lookup this is `0` or `1`).</ResponseField>
<ResponseField name="limit" type="integer">Echo of the requested page size.</ResponseField>
<ResponseField name="offset" type="integer">Echo of the requested offset.</ResponseField>

<Tip>
  To create-if-missing and always end up with the user's ID: look up the email here, and if `items` is empty call `POST /v1/users` to provision them — or just call `POST /v1/users` directly, which returns the existing user (`200`) when the email is already present.
</Tip>

## Retrieve a user

<ParamField path="method" type="GET">
  `GET /v1/users/{user_id}`
</ParamField>

Returns the user. The user must have a membership in the API key's org.

```bash cURL theme={null}
curl -X GET "https://sandbox.api.coverbase.app/v1/users/cbuser_a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6" \
  -H "Authorization: Bearer ak_live_xxx"
```

Returns the [user object](#user-object). `role` and `status` reflect the current org membership.

| Status | Body                                                                   | When                                                              |
| ------ | ---------------------------------------------------------------------- | ----------------------------------------------------------------- |
| 404    | `{"detail": {"code": "user_not_found", "message": "User not found."}}` | No such user, or the user has no membership in the API key's org. |

## Update role / deprovision

<ParamField path="method" type="PATCH">
  `PATCH /v1/users/{user_id}`
</ParamField>

Changes a user's org role and/or (de)provisions them. Returns the refreshed [user object](#user-object).

### Request body

<ParamField body="role" type="string">
  New org role (one of the [role values](#org-roles)). Omit to leave unchanged.
</ParamField>

<ParamField body="is_archived" type="boolean">
  `true` deprovisions: archives the user and sets org membership to `inactive`. `false` reactivates: membership back to `active`. Omit to leave unchanged.
</ParamField>

### Examples

```bash Change role theme={null}
curl -X PATCH "https://sandbox.api.coverbase.app/v1/users/cbuser_a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6" \
  -H "Authorization: Bearer ak_live_xxx" \
  -H "Content-Type: application/json" \
  -d '{ "role": "org:member" }'
```

```bash Deprovision theme={null}
curl -X PATCH "https://sandbox.api.coverbase.app/v1/users/cbuser_a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6" \
  -H "Authorization: Bearer ak_live_xxx" \
  -H "Content-Type: application/json" \
  -d '{ "is_archived": true }'
```

### Behavior

* `is_archived: true` → user archived, membership `inactive`.
* `is_archived: false` → membership `active` (reactivation).
* `role` set → membership role updated.
* Combined (`role` + `is_archived`) is applied together.

### Error responses

| Status | Body                                                                   | When                                              |
| ------ | ---------------------------------------------------------------------- | ------------------------------------------------- |
| 404    | `{"detail": {"code": "user_not_found", "message": "User not found."}}` | No membership for this user in the API key's org. |
| 422    | Standard validation error                                              | `role` not a recognized value.                    |
