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

# Additional roles API for managed databases

> Create, list, rename, reveal, rotate, revoke and delete up to ten additional PostgreSQL roles on a DBHost database with managed SQL roles.

A database with managed SQL roles can have up to ten additional roles next to
its `owner`, `runtime` and `readonly` logins. Each additional role has its own
generated username, password, access profile and connection limit. See
[Database roles](/guides/database-roles) for the profiles and how to grant a
Custom role access. For a single-login database that opted in to additional
roles, use the [additional roles API for existing databases](/api-reference/databases/additional-roles)
instead.

API access requires Pro or Business. Every endpoint on this page accepts a
full-account key or a key scoped to selected databases that includes this
database; see [Authentication](/api-reference/authentication). Every
response, including errors, is sent with `Cache-Control: no-store`. Only the
credentials endpoint returns a password.

| Method | Path | Purpose |
| - | - | - |
| `GET` | `/api/v1/databases/{id}/custom-roles` | List roles |
| `POST` | `/api/v1/databases/{id}/custom-roles` | Create a role |
| `PATCH` | `/api/v1/databases/{id}/custom-roles/{roleId}` | Rename a role |
| `POST` | `/api/v1/databases/{id}/custom-roles/{roleId}/credentials` | Reveal the password |
| `POST` | `/api/v1/databases/{id}/custom-roles/{roleId}/changes` | Rotate or revoke |
| `GET` | `/api/v1/databases/{id}/custom-roles/{roleId}/changes/{operationId}` | Read a change |
| `POST` | `/api/v1/databases/{id}/custom-roles/{roleId}/changes/{operationId}` | Reconcile a change |
| `POST` | `/api/v1/databases/{id}/custom-roles/{roleId}/delete` | Revoke and remove in one call |
| `DELETE` | `/api/v1/databases/{id}/custom-roles/{roleId}` | Remove a revoked role |
| `POST` | `/api/v1/databases/{id}/custom-roles/{roleId}/prepare` | Reconcile an unfinished create |
| `POST` | `/api/v1/databases/{id}/custom-roles/{roleId}/admit` | Reconcile an unfinished create |

`{id}` and `{roleId}` must be lowercase UUIDs. Endpoints that take no body
reject a non-empty body with `400 VALIDATION_ERROR`.

## The role object

```json theme={null}
{
  "roleId": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
  "profile": "custom",
  "displayName": "reporting",
  "accessProfile": "read_only",
  "username": "dbr_550e8400e29b41d4a716446655440000_c1",
  "ordinal": 1,
  "connectionLimit": 2,
  "generation": 1,
  "credentialGeneration": 1,
  "status": "ready",
  "operationId": null
}
```

| Field | Description |
| - | - |
| `roleId` | Role ID used in the paths on this page |
| `profile` | Always `custom` for an additional role |
| `displayName` | Your label for the role, 1–64 characters |
| `accessProfile` | `read_only`, `read_write` or `custom` |
| `username` | Generated PostgreSQL login name. It does not change on rename |
| `ordinal` | Sequence number of the role on this database; not reused |
| `connectionLimit` | The role's own PostgreSQL connection limit |
| `generation` | Role-set generation for the database |
| `credentialGeneration` | Increases by one each time the password changes |
| `status` | `reserved`, `ready`, `revoked`, `updating` or `expired` |
| `operationId` | List only: ID of the pending change while `status` is `updating`, otherwise `null` |

`reserved` means a create has not finished; see
[Reconcile an unfinished create](#reconcile-an-unfinished-create). `updating`
means a rotation or revocation is pending. `expired` means the credential's
validity period has ended; it can no longer be revealed or rotated.

## List roles

```http theme={null}
GET /api/v1/databases/{id}/custom-roles
Authorization: Bearer <api-key>
```

```json 200 OK theme={null}
{
  "roles": [
    {
      "roleId": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
      "profile": "custom",
      "displayName": "reporting",
      "accessProfile": "read_only",
      "username": "dbr_550e8400e29b41d4a716446655440000_c1",
      "ordinal": 1,
      "connectionLimit": 2,
      "generation": 1,
      "credentialGeneration": 1,
      "status": "ready",
      "operationId": null
    }
  ]
}
```

Roles are ordered by `ordinal`. Revoked roles are left out unless you add
`?includeRevoked=true`. Removed roles are never listed.

## Create a role

```http theme={null}
POST /api/v1/databases/{id}/custom-roles
Authorization: Bearer <api-key>
Idempotency-Key: <stable-unique-key>
Content-Type: application/json

{
  "displayName": "reporting",
  "accessProfile": "read_only",
  "connectionLimit": 2
}
```

The body must contain exactly these three fields:

| Field | Rules |
| - | - |
| `displayName` | String, 1–64 characters after trimming, no control characters, unique among the database's roles |
| `accessProfile` | `read_only`, `read_write` or `custom` |
| `connectionLimit` | Integer of 1 or more, checked against the database's role connection budget |

The `Idempotency-Key` header is required: 1–200 characters, without line
breaks or NUL characters, and not starting with `dbhost-internal-`.

A successful create finishes setup in the same call and returns the role as
`ready`. The response contains no password; use
[Reveal the password](#reveal-the-password).

```json 200 OK theme={null}
{
  "roleId": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
  "profile": "custom",
  "displayName": "reporting",
  "accessProfile": "read_only",
  "username": "dbr_550e8400e29b41d4a716446655440000_c1",
  "ordinal": 1,
  "connectionLimit": 2,
  "status": "ready"
}
```

All three profiles get `CONNECT` on the database and can log in immediately.
`read_only` also gets `SELECT` and `read_write` gets DML. A `custom` role gets
no table grants; grant access from your migrations as the owner role. DBHost
never grants an additional role `CREATE` or `TEMPORARY`.

Reuse the same idempotency key after a timeout or an uncertain result. The
same key with the same body returns the same role and resumes its setup; the
same key with a different body returns `409 MANAGED_ROLE_CONFLICT`.

Creation is refused with `409 MANAGED_ROLE_CONFLICT` when:

* the database already has ten roles. Revoked roles still count; delete one
  to free its slot
* the display name is already in use
* another role on the database is still `reserved`, or a credential change on
  the database is unfinished
* a restore of the database is in progress
* the database is not active
* the database would have four or more additional roles, or the new limit
  takes the total above the database's role budget, and the database has no
  effective [server connection cap](/api-reference/databases/connection-limits)

If the `CONNECT` grant was withdrawn during setup, the response is
`409 CONNECT_GRANT_REQUIRED` with the SQL the database owner can run, then
retry the same request:

```json 409 Conflict theme={null}
{
  "error": {
    "code": "CONNECT_GRANT_REQUIRED",
    "message": "Database owner must grant CONNECT before admitting this role.",
    "databaseName": "my-app",
    "username": "dbr_550e8400e29b41d4a716446655440000_c1",
    "grantSql": "GRANT CONNECT ON DATABASE \"my-app\" TO \"dbr_550e8400e29b41d4a716446655440000_c1\";"
  }
}
```

A database cloned from a backup that predates additional-role support
returns `409 CUSTOM_ROLE_REQUIRES_V3_CLONE`. Make a new clone from a newer
backup of the source database before adding roles.

## Rename a role

```http theme={null}
PATCH /api/v1/databases/{id}/custom-roles/{roleId}
Authorization: Bearer <api-key>
Content-Type: application/json

{"displayName":"bi-reporting"}
```

The body must contain exactly `displayName`, with the same rules as on
create. Only the label changes; the username, password and grants stay the
same. The role must be `ready` or `revoked`.

```json 200 OK theme={null}
{
  "roleId": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
  "displayName": "bi-reporting"
}
```

## Reveal the password

```http theme={null}
POST /api/v1/databases/{id}/custom-roles/{roleId}/credentials
Authorization: Bearer <api-key>
```

Send no body. The role must be `ready`, without a pending change, and not
expired. Each reveal is recorded in the account's audit log.

```json 200 OK theme={null}
{
  "roleId": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
  "profile": "custom",
  "displayName": "reporting",
  "accessProfile": "read_only",
  "username": "dbr_550e8400e29b41d4a716446655440000_c1",
  "ordinal": 1,
  "connectionLimit": 2,
  "generation": 1,
  "credentialGeneration": 1,
  "status": "ready",
  "host": "db.dbhost.app",
  "port": 6432,
  "database": "my-app",
  "tlsRequired": true,
  "password": "<password>"
}
```

Connect with TLS. Store the password in a secret manager and keep it out of
logs and source control.

## Rotate or revoke

```http theme={null}
POST /api/v1/databases/{id}/custom-roles/{roleId}/changes
Authorization: Bearer <api-key>
Idempotency-Key: <stable-unique-key>
Content-Type: application/json

{"kind":"rotate","confirmDisconnect":true}
```

The body must contain exactly `kind` (`rotate` or `revoke`) and
`confirmDisconnect: true`. The `Idempotency-Key` header follows the same rules
as on create.

<Warning>
  Rotation and revocation close the role's open connections. Open
  transactions on those connections roll back.
</Warning>

`rotate` sets a new password. `revoke` disables the login; the role keeps its
slot until you delete it. The role must be `ready`, and a rotation also needs
an unexpired credential. Changes are refused while a restore of the database
is in progress or another credential change on the database is unfinished.

```json 202 Accepted theme={null}
{
  "operationId": "22222222-2222-4222-8222-222222222222",
  "roleId": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
  "profile": "custom",
  "kind": "rotate",
  "status": "pending",
  "credentialGeneration": 2
}
```

The status code is `202` while `status` is `pending` and `200` when it is
`completed`. `credentialGeneration` is the generation the change produces.
Retry with the same key and body after an uncertain result; a different
`kind` on the same key is refused.

### Read a change

```http theme={null}
GET /api/v1/databases/{id}/custom-roles/{roleId}/changes/{operationId}
Authorization: Bearer <api-key>
```

Returns `200` with the same fields as above. It does not advance the change.

### Reconcile a change

```http theme={null}
POST /api/v1/databases/{id}/custom-roles/{roleId}/changes/{operationId}
Authorization: Bearer <api-key>
Content-Type: application/json

{"confirmDisconnect":true}
```

The body must be exactly `{"confirmDisconnect": true}`. DBHost replays the
recorded change and returns `202` while pending or `200` when completed. A
change that has already completed is not applied again.

## Delete a role

```http theme={null}
POST /api/v1/databases/{id}/custom-roles/{roleId}/delete
Authorization: Bearer <api-key>
Content-Type: application/json

{"confirm":true}
```

The body must be exactly `{"confirm": true}`. A `ready` role is revoked first,
which closes its open connections, then removed from PostgreSQL. A `revoked`
role is removed directly. Repeat the same call to continue a pending delete.

| Status | Body | Meaning |
| - | - | - |
| 200 | `{"roleId": "...", "status": "removed"}` | The role is gone and its slot is free |
| 202 | `{"roleId": "...", "status": "pending", "phase": "revoke", "operationId": "..."}` | Revocation is still running |
| 202 | `{"roleId": "...", "status": "pending", "phase": "remove"}` | Removal is still running |

DBHost does not cascade through your objects. If the role still owns objects
or holds grants, the role stays revoked and the response lists what to clean
up:

```json 409 Conflict theme={null}
{
  "error": {
    "code": "ROLE_DEPENDENCIES",
    "message": "Remove the listed customer grants or objects before retrying",
    "dependencies": [
      {
        "kind": "grant",
        "objectType": "table",
        "schema": "public",
        "name": "orders",
        "identity": "table public.orders"
      }
    ],
    "truncated": false
  }
}
```

`kind` is `grant`, `owned_object`, `policy`, `other` or `other_database`. At
most 20 entries are returned; `truncated: true` means there are more. Revoke
the grants or reassign the objects as the owner role, then call delete again.

### Remove a revoked role

```http theme={null}
DELETE /api/v1/databases/{id}/custom-roles/{roleId}
Authorization: Bearer <api-key>
```

Send no body. This removes a role that is already `revoked` and returns
`200` with `{"roleId": "...", "status": "removed"}`, or `202` with
`{"roleId": "...", "status": "pending"}` while removal runs. Remaining
dependencies return `409 ROLE_DEPENDENCIES` without the list; use the
`/delete` endpoint above to see it.

## Reconcile an unfinished create

A create that was interrupted can leave a role in `reserved`. Retrying the
original create with the same idempotency key and body is the simplest way to
finish it. The two step endpoints are also available. Both take no body.

`POST /api/v1/databases/{id}/custom-roles/{roleId}/prepare` creates the
PostgreSQL role and returns `202`:

```json 202 Accepted theme={null}
{
  "roleId": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
  "profile": "custom",
  "username": "dbr_550e8400e29b41d4a716446655440000_c1",
  "displayName": "reporting",
  "accessProfile": "read_only",
  "ordinal": 1,
  "status": "sql_ready"
}
```

`POST /api/v1/databases/{id}/custom-roles/{roleId}/admit` applies the
profile's grants and opens the login. It returns `200` with the same fields
and `"status": "ready"`, or `409 CONNECT_GRANT_REQUIRED` with `grantSql` as
shown under [Create a role](#create-a-role). Neither step is needed after a
successful create.

## Errors

| Status | Code | Description |
| - | - | - |
| 400 | `VALIDATION_ERROR` | Invalid ID, unexpected or missing body field, missing confirmation, or missing or invalid `Idempotency-Key` |
| 400 | `INVALID_JSON` | Request body is not valid JSON |
| 401 | `UNAUTHORIZED` | Missing or invalid API key |
| 403 | `PLAN_API_ACCESS_REQUIRED` | API access requires Pro or Business |
| 404 | `DATABASE_NOT_FOUND` | Database doesn't exist, isn't owned by this account or isn't in the key's scope |
| 409 | `MANAGED_ROLE_CONFLICT` | The role or database is not in a state that allows this action, or the idempotency key was used with a different request |
| 409 | `CONNECT_GRANT_REQUIRED` | The role needs `CONNECT`; run `grantSql` as the owner and retry |
| 409 | `CUSTOM_ROLE_REQUIRES_V3_CLONE` | The database is a clone from an older backup; create a new clone first |
| 409 | `ROLE_DEPENDENCIES` | The role still has grants or owns objects |
| 413 | `PAYLOAD_TOO_LARGE` | Request body is larger than the API limit |
| 429 | `RATE_LIMITED` | Too many requests; retry after the `Retry-After` header |
| 503 | `FEATURE_DISABLED` | Additional roles are temporarily unavailable |
| 503 | `MAINTENANCE` | DBHost is in a maintenance window; retry later |
| 500 | `INTERNAL_ERROR` | The request failed; retry the same request |

## Dashboard and CLI

The same actions are on the database detail page under **Additional roles**
on every plan. The CLI provides them as `dbhost managed custom-*` commands;
see [CLI](/cli).


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.