> Fetch the index of every Tonbo docs page as Markdown at https://tonbo.dev/llms.txt

# Management API: API keys

Last updated 2 November 2026

Keys of the account's members, people and Agents. [The overview](https://tonbo.dev/docs/reference/management.md) covers what every operation shares: authentication, roles, errors, pagination, idempotency and If-Match.

- [`GET /api-keys`](#list-api-keys) List your keys, or every key of the account for an owner or admin
- [`POST /api-keys`](#create-api-key) Create a key; its secret is in this answer only
- [`GET /api-keys/{key_id}`](#get-api-key) Read one key
- [`DELETE /api-keys/{key_id}`](#revoke-api-key) Revoke a key and every token minted from it
- [Objects](#objects): [ApiKey](#schema-api-key) and [ApiKeyWithSecret](#schema-api-key-with-secret)

## List your keys, or every key of the account for an owner or admin

`GET /v1/api-keys` · CLI: `tonbo api-key list`

Needs [`api_keys.own`](https://tonbo.dev/docs/reference/management.md#capability-api-keys-own), which `owner`, `admin` and `member` hold. A narrowed key may not.

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `cursor` | query | string | no | Opaque cursor tied to the original query shape. At least 1 characters. |
| `limit` | query | integer | no | The most items one page holds, 1 to 200; 50 when omitted. A page may hold fewer; read on with `next_cursor` until it is null. Default `50`. 1 to 200. |

### Response

| Status | Answer |
| --- | --- |
| `200` | Live keys |

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `data` | array of [ApiKey](https://tonbo.dev/docs/reference/management/api-keys.md#schema-api-key) | yes |  |
| `next_cursor` | string or null | yes | Opaque cursor of the next page; null on the last page. |

| Header | Status | Description |
| --- | --- | --- |
| `WWW-Authenticate` | 401 | Authentication scheme required by this API. |
| `Retry-After` | 429 | Seconds before the client should retry. |

**Example 200 body**

```json
{
  "data": [
    {
      "id": "3f1c2a9e-5b7d-4c8e-9f10-2a3b4c5d6e7f",
      "name": "<name>",
      "member": "agent.3f1c2a9e-5b7d-4c8e-9f10-2a3b4c5d6e7f",
      "member_kind": "user",
      "last4": "<last4>",
      "read_only": false,
      "buckets": [
        "<buckets>"
      ],
      "databases": [
        "<databases>"
      ],
      "database_prefixes": [
        "<database_prefixes>"
      ],
      "streams": [
        "<streams>"
      ],
      "stream_prefixes": [
        "<stream_prefixes>"
      ],
      "expires_at": "2026-11-02T09:30:00.000Z",
      "last_used_at": "2026-11-02T09:30:00.000Z",
      "created_at": "2026-11-02T09:30:00.000Z"
    }
  ],
  "next_cursor": "<next_cursor>"
}
```

### Errors

| Status | Code | When | Send again | What to do |
| --- | --- | --- | --- | --- |
| `401` | [`exchange_required`](https://tonbo.dev/docs/reference/management.md#exchange_required) | The key acts as an Agent, and an Agent’s key is never sent itself. | No | Exchange the key for a token at the endpoint the answer’s `token_endpoint` names, and send the token as `Authorization: Bearer`. ([more](https://tonbo.dev/docs/databases/troubleshooting/manage.md#exchange_required)) |
| `401` | [`unauthenticated`](https://tonbo.dev/docs/reference/management.md#unauthenticated) | The request carries no key or token, or one that is invalid, expired or revoked. | No | Send a valid key, or a token that has not expired, as `Authorization: Bearer`. ([more](https://tonbo.dev/docs/databases/troubleshooting/manage.md#unauthenticated)) |
| `403` | [`forbidden`](https://tonbo.dev/docs/reference/management.md#forbidden) | The key’s role does not hold the capability the operation needs, the key is narrowed away from the resource, or the account already holds as many of the resource as it may. | No | Use a key whose role holds the capability, or one that is not narrowed; delete what you no longer need, or ask for a higher limit. ([more](https://tonbo.dev/docs/databases/troubleshooting/manage.md#forbidden)) |
| `422` | [`validation_error`](https://tonbo.dev/docs/reference/management.md#validation_error) | A parameter, header or body field breaks its rule; `errors` names each field. | No | Correct the fields `errors` names; the operation’s reference gives each one’s rule. ([more](https://tonbo.dev/docs/databases/troubleshooting/manage.md#validation_error)) |
| `429` | [`rate_limited`](https://tonbo.dev/docs/reference/management.md#rate_limited) | The key or the account sent more requests than its rate allows. | After Retry-After | Wait for the seconds Retry-After names, then send the request again; spread requests out if it recurs. ([more](https://tonbo.dev/docs/databases/troubleshooting/manage.md#rate_limited)) |

### Retries

- A read: sending it again changes nothing.
- `429` carry `Retry-After`: wait that many seconds, then send the same request again.

**Request**

```shell
curl -sS "https://api.tonbo.dev/v1/api-keys" \
  -H "Authorization: Bearer $TONBO_API_KEY"
```

## Create a key; its secret is in this answer only

`POST /v1/api-keys` · CLI: `tonbo api-key create`

Needs [`api_keys.own`](https://tonbo.dev/docs/reference/management.md#capability-api-keys-own), which `owner`, `admin` and `member` hold. A narrowed key may not.

A personal key acts as you; an Agent key (`member: agent.<agent id>`), which owners and admins create, acts as the Agent, whose role may not exceed yours. `read_only`, and either `buckets` or any of `databases`, `database_prefixes`, `streams` and `stream_prefixes`, narrow the key for its whole life: a different narrowing takes a new key. An access token never creates a key, and neither does a narrowed key.

### Request body

`application/json`, required. Unknown fields are refused.

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `name` | string | yes | A name to tell the key apart, 1 to 80 characters once spaces are trimmed; unique, ignoring case, among the member's live keys, so a second live key with the name answers 409. 1 to 80 characters. |
| `member` | string | no | Omit for your own personal key; `agent.<agent id>` for an Agent key. Matches `^[a-z0-9._-]{1,64}$`. |
| `read_only` | boolean | no | Narrow the key to read-only actions, for its whole life. Default `false`. |
| `buckets` | array of string | no | Narrow the key to these registered buckets of the account, for its whole life. Never with databases, database_prefixes, streams or stream_prefixes. At most 20 items. No duplicates. |
| `databases` | array of string | no | Narrow the key to these databases, for its whole life; a name need not exist yet. Never with buckets. At most 20 items. No duplicates. |
| `database_prefixes` | array of string | no | Narrow the key to the databases whose name starts with one of these, for its whole life. Never with buckets. At most 20 items. No duplicates. |
| `streams` | array of string | no | Narrow the key to these raw streams, by the name the streams API shows, for its whole life; a name need not exist yet. Never with buckets. A key narrowed to both databases and streams reaches both. At most 20 items. No duplicates. |
| `stream_prefixes` | array of string | no | Narrow the key to the raw streams whose name starts with one of these, for its whole life; each at most 83 bytes, the longest stream name. Never with buckets. At most 20 items. No duplicates. |
| `expires_in` | one of `"15_days"`, `"1_month"`, `"6_months"`, `"1_year"` or `"never"` | no | How long the key lives from now; it never expires when omitted. Default `"never"`. |

### Response

| Status | Answer |
| --- | --- |
| `201` | The key and its secret |

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `data` | [ApiKeyWithSecret](https://tonbo.dev/docs/reference/management/api-keys.md#schema-api-key-with-secret) | yes |  |

| Header | Status | Description |
| --- | --- | --- |
| `WWW-Authenticate` | 401 | Authentication scheme required by this API. |
| `Retry-After` | 429 | Seconds before the client should retry. |

**Example 201 body**

```json
{
  "data": {
    "id": "3f1c2a9e-5b7d-4c8e-9f10-2a3b4c5d6e7f",
    "name": "<name>",
    "member": "agent.3f1c2a9e-5b7d-4c8e-9f10-2a3b4c5d6e7f",
    "member_kind": "user",
    "last4": "<last4>",
    "read_only": false,
    "buckets": [
      "<buckets>"
    ],
    "databases": [
      "<databases>"
    ],
    "database_prefixes": [
      "<database_prefixes>"
    ],
    "streams": [
      "<streams>"
    ],
    "stream_prefixes": [
      "<stream_prefixes>"
    ],
    "expires_at": "2026-11-02T09:30:00.000Z",
    "last_used_at": "2026-11-02T09:30:00.000Z",
    "created_at": "2026-11-02T09:30:00.000Z",
    "key": "<key>"
  }
}
```

### Errors

| Status | Code | When | Send again | What to do |
| --- | --- | --- | --- | --- |
| `401` | [`exchange_required`](https://tonbo.dev/docs/reference/management.md#exchange_required) | The key acts as an Agent, and an Agent’s key is never sent itself. | No | Exchange the key for a token at the endpoint the answer’s `token_endpoint` names, and send the token as `Authorization: Bearer`. ([more](https://tonbo.dev/docs/databases/troubleshooting/manage.md#exchange_required)) |
| `401` | [`unauthenticated`](https://tonbo.dev/docs/reference/management.md#unauthenticated) | The request carries no key or token, or one that is invalid, expired or revoked. | No | Send a valid key, or a token that has not expired, as `Authorization: Bearer`. ([more](https://tonbo.dev/docs/databases/troubleshooting/manage.md#unauthenticated)) |
| `403` | [`forbidden`](https://tonbo.dev/docs/reference/management.md#forbidden) | The key’s role does not hold the capability the operation needs, the key is narrowed away from the resource, or the account already holds as many of the resource as it may. | No | Use a key whose role holds the capability, or one that is not narrowed; delete what you no longer need, or ask for a higher limit. ([more](https://tonbo.dev/docs/databases/troubleshooting/manage.md#forbidden)) |
| `404` | [`not_found`](https://tonbo.dev/docs/reference/management.md#not_found) | No live resource of the key’s account has that name or id, or the key is narrowed away from it. | No | Check the name, and that the key belongs to the account that holds it; list the resources to see what exists. ([more](https://tonbo.dev/docs/databases/troubleshooting/manage.md#not_found)) |
| `409` | [`conflict`](https://tonbo.dev/docs/reference/management.md#conflict) | The name is held by another live resource of the account, the Idempotency-Key already created another resource or one since deleted, or the resource’s current state does not allow the change. `detail` says which, and `errors` names the field. | No | Pick another name or a new Idempotency-Key, or read the resource again and act on its current state. ([more](https://tonbo.dev/docs/databases/troubleshooting/manage.md#conflict)) |
| `422` | [`validation_error`](https://tonbo.dev/docs/reference/management.md#validation_error) | A parameter, header or body field breaks its rule; `errors` names each field. | No | Correct the fields `errors` names; the operation’s reference gives each one’s rule. ([more](https://tonbo.dev/docs/databases/troubleshooting/manage.md#validation_error)) |
| `429` | [`rate_limited`](https://tonbo.dev/docs/reference/management.md#rate_limited) | The key or the account sent more requests than its rate allows. | After Retry-After | Wait for the seconds Retry-After names, then send the request again; spread requests out if it recurs. ([more](https://tonbo.dev/docs/databases/troubleshooting/manage.md#rate_limited)) |

### Retries

- No Idempotency-Key: the secret is never stored, so a replay cannot return it; a live key name is unique, so a blind retry answers 409 instead of creating a second key.
- `429` carry `Retry-After`: wait that many seconds, then send the same request again.

**Request**

```shell
curl -sS "https://api.tonbo.dev/v1/api-keys" \
  -H "Authorization: Bearer $TONBO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"name":"laptop"}'
```

## Read one key

`GET /v1/api-keys/{key_id}` · CLI: `tonbo api-key show`

Needs [`api_keys.own`](https://tonbo.dev/docs/reference/management.md#capability-api-keys-own), which `owner`, `admin` and `member` hold. A narrowed key may not.

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `key_id` | path | string (uuid) | yes | The key's `id`, as `GET /v1/api-keys` lists it. |

### Response

| Status | Answer |
| --- | --- |
| `200` | The key |

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `data` | [ApiKey](https://tonbo.dev/docs/reference/management/api-keys.md#schema-api-key) | yes |  |

| Header | Status | Description |
| --- | --- | --- |
| `ETag` | 200 | Current resource version for optional conditional updates. |
| `WWW-Authenticate` | 401 | Authentication scheme required by this API. |
| `Retry-After` | 429 | Seconds before the client should retry. |

**Example 200 body**

```json
{
  "data": {
    "id": "3f1c2a9e-5b7d-4c8e-9f10-2a3b4c5d6e7f",
    "name": "<name>",
    "member": "agent.3f1c2a9e-5b7d-4c8e-9f10-2a3b4c5d6e7f",
    "member_kind": "user",
    "last4": "<last4>",
    "read_only": false,
    "buckets": [
      "<buckets>"
    ],
    "databases": [
      "<databases>"
    ],
    "database_prefixes": [
      "<database_prefixes>"
    ],
    "streams": [
      "<streams>"
    ],
    "stream_prefixes": [
      "<stream_prefixes>"
    ],
    "expires_at": "2026-11-02T09:30:00.000Z",
    "last_used_at": "2026-11-02T09:30:00.000Z",
    "created_at": "2026-11-02T09:30:00.000Z"
  }
}
```

### Errors

| Status | Code | When | Send again | What to do |
| --- | --- | --- | --- | --- |
| `401` | [`exchange_required`](https://tonbo.dev/docs/reference/management.md#exchange_required) | The key acts as an Agent, and an Agent’s key is never sent itself. | No | Exchange the key for a token at the endpoint the answer’s `token_endpoint` names, and send the token as `Authorization: Bearer`. ([more](https://tonbo.dev/docs/databases/troubleshooting/manage.md#exchange_required)) |
| `401` | [`unauthenticated`](https://tonbo.dev/docs/reference/management.md#unauthenticated) | The request carries no key or token, or one that is invalid, expired or revoked. | No | Send a valid key, or a token that has not expired, as `Authorization: Bearer`. ([more](https://tonbo.dev/docs/databases/troubleshooting/manage.md#unauthenticated)) |
| `403` | [`forbidden`](https://tonbo.dev/docs/reference/management.md#forbidden) | The key’s role does not hold the capability the operation needs, the key is narrowed away from the resource, or the account already holds as many of the resource as it may. | No | Use a key whose role holds the capability, or one that is not narrowed; delete what you no longer need, or ask for a higher limit. ([more](https://tonbo.dev/docs/databases/troubleshooting/manage.md#forbidden)) |
| `404` | [`not_found`](https://tonbo.dev/docs/reference/management.md#not_found) | No live resource of the key’s account has that name or id, or the key is narrowed away from it. | No | Check the name, and that the key belongs to the account that holds it; list the resources to see what exists. ([more](https://tonbo.dev/docs/databases/troubleshooting/manage.md#not_found)) |
| `429` | [`rate_limited`](https://tonbo.dev/docs/reference/management.md#rate_limited) | The key or the account sent more requests than its rate allows. | After Retry-After | Wait for the seconds Retry-After names, then send the request again; spread requests out if it recurs. ([more](https://tonbo.dev/docs/databases/troubleshooting/manage.md#rate_limited)) |

### Retries

- A read: sending it again changes nothing.
- `429` carry `Retry-After`: wait that many seconds, then send the same request again.

**Request**

```shell
curl -sS "https://api.tonbo.dev/v1/api-keys/<key_id>" \
  -H "Authorization: Bearer $TONBO_API_KEY"
```

## Revoke a key and every token minted from it

`DELETE /v1/api-keys/{key_id}` · CLI: `tonbo api-key revoke`

Needs [`api_keys.own`](https://tonbo.dev/docs/reference/management.md#capability-api-keys-own), which `owner`, `admin` and `member` hold. A narrowed key may not.

Your own key, or any key of the account for an owner or admin. A narrowed key may revoke itself (`tonbo logout`).

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `key_id` | path | string (uuid) | yes | The key's `id`, as `GET /v1/api-keys` lists it. |

### Response

| Status | Answer |
| --- | --- |
| `200` | The revocation |

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `data` | object | yes |  |
| `data.id` | string (uuid) | yes | A UUID in lowercase hex with hyphens. |
| `data.revoked_at` | string (date-time) | yes | An RFC 3339 time; answers give it in UTC. |

| Header | Status | Description |
| --- | --- | --- |
| `WWW-Authenticate` | 401 | Authentication scheme required by this API. |
| `Retry-After` | 429 | Seconds before the client should retry. |

**Example 200 body**

```json
{
  "data": {
    "id": "3f1c2a9e-5b7d-4c8e-9f10-2a3b4c5d6e7f",
    "revoked_at": "2026-11-02T09:30:00.000Z"
  }
}
```

### Errors

| Status | Code | When | Send again | What to do |
| --- | --- | --- | --- | --- |
| `401` | [`exchange_required`](https://tonbo.dev/docs/reference/management.md#exchange_required) | The key acts as an Agent, and an Agent’s key is never sent itself. | No | Exchange the key for a token at the endpoint the answer’s `token_endpoint` names, and send the token as `Authorization: Bearer`. ([more](https://tonbo.dev/docs/databases/troubleshooting/manage.md#exchange_required)) |
| `401` | [`unauthenticated`](https://tonbo.dev/docs/reference/management.md#unauthenticated) | The request carries no key or token, or one that is invalid, expired or revoked. | No | Send a valid key, or a token that has not expired, as `Authorization: Bearer`. ([more](https://tonbo.dev/docs/databases/troubleshooting/manage.md#unauthenticated)) |
| `403` | [`forbidden`](https://tonbo.dev/docs/reference/management.md#forbidden) | The key’s role does not hold the capability the operation needs, the key is narrowed away from the resource, or the account already holds as many of the resource as it may. | No | Use a key whose role holds the capability, or one that is not narrowed; delete what you no longer need, or ask for a higher limit. ([more](https://tonbo.dev/docs/databases/troubleshooting/manage.md#forbidden)) |
| `404` | [`not_found`](https://tonbo.dev/docs/reference/management.md#not_found) | No live resource of the key’s account has that name or id, or the key is narrowed away from it. | No | Check the name, and that the key belongs to the account that holds it; list the resources to see what exists. ([more](https://tonbo.dev/docs/databases/troubleshooting/manage.md#not_found)) |
| `429` | [`rate_limited`](https://tonbo.dev/docs/reference/management.md#rate_limited) | The key or the account sent more requests than its rate allows. | After Retry-After | Wait for the seconds Retry-After names, then send the request again; spread requests out if it recurs. ([more](https://tonbo.dev/docs/databases/troubleshooting/manage.md#rate_limited)) |

### Retries

- It takes no Idempotency-Key; its description above says what sending it again does.
- `429` carry `Retry-After`: wait that many seconds, then send the same request again.

**Request**

```shell
curl -sS -X DELETE "https://api.tonbo.dev/v1/api-keys/<key_id>" \
  -H "Authorization: Bearer $TONBO_API_KEY"
```

## Objects

### ApiKey

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string (uuid) | yes | The key's id, which `GET` and `DELETE /v1/api-keys/{key_id}` name. |
| `name` | string | yes | The key's name, unique, ignoring case, among its member's live keys. |
| `member` | string | yes | Who the key acts as, with that member's current role: a person's user id, or `agent.<agent id>` for an Agent key. Matches `^[a-z0-9._-]{1,64}$`. |
| `member_kind` | one of `"user"` or `"agent"` | yes | `user` for a person's key, `agent` for an Agent key. |
| `last4` | string | yes | The secret's last four characters, to tell keys apart; the secret itself is shown only when the key is created. 4 to 4 characters. |
| `read_only` | boolean | yes | Whether the key is narrowed to read-only actions. |
| `buckets` | array of string | yes | The buckets the key is narrowed to; empty when it is not narrowed by bucket. |
| `databases` | array of string | yes | The database names the key is narrowed to. A key whose five narrowing lists are all empty reaches everything its member does; otherwise it reaches only what the lists name. |
| `database_prefixes` | array of string | yes | The database name prefixes the key is narrowed to. |
| `streams` | array of string | yes | The stream names the key is narrowed to, without `s.`. |
| `stream_prefixes` | array of string | yes | The stream name prefixes the key is narrowed to. |
| `expires_at` | string (date-time) or null | yes | When the key stops working; null when it never expires. |
| `last_used_at` | string (date-time) or null | yes | Recorded at most once a minute. |
| `created_at` | string (date-time) | yes | When the key was created. |

**Example ApiKey**

```json
{
  "id": "3f1c2a9e-5b7d-4c8e-9f10-2a3b4c5d6e7f",
  "name": "<name>",
  "member": "agent.3f1c2a9e-5b7d-4c8e-9f10-2a3b4c5d6e7f",
  "member_kind": "user",
  "last4": "<last4>",
  "read_only": false,
  "buckets": [
    "<buckets>"
  ],
  "databases": [
    "<databases>"
  ],
  "database_prefixes": [
    "<database_prefixes>"
  ],
  "streams": [
    "<streams>"
  ],
  "stream_prefixes": [
    "<stream_prefixes>"
  ],
  "expires_at": "2026-11-02T09:30:00.000Z",
  "last_used_at": "2026-11-02T09:30:00.000Z",
  "created_at": "2026-11-02T09:30:00.000Z"
}
```

### ApiKeyWithSecret

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string (uuid) | yes | The key's id, which `GET` and `DELETE /v1/api-keys/{key_id}` name. |
| `name` | string | yes | The key's name, unique, ignoring case, among its member's live keys. |
| `member` | string | yes | Who the key acts as, with that member's current role: a person's user id, or `agent.<agent id>` for an Agent key. Matches `^[a-z0-9._-]{1,64}$`. |
| `member_kind` | one of `"user"` or `"agent"` | yes | `user` for a person's key, `agent` for an Agent key. |
| `last4` | string | yes | The secret's last four characters, to tell keys apart; the secret itself is shown only when the key is created. 4 to 4 characters. |
| `read_only` | boolean | yes | Whether the key is narrowed to read-only actions. |
| `buckets` | array of string | yes | The buckets the key is narrowed to; empty when it is not narrowed by bucket. |
| `databases` | array of string | yes | The database names the key is narrowed to. A key whose five narrowing lists are all empty reaches everything its member does; otherwise it reaches only what the lists name. |
| `database_prefixes` | array of string | yes | The database name prefixes the key is narrowed to. |
| `streams` | array of string | yes | The stream names the key is narrowed to, without `s.`. |
| `stream_prefixes` | array of string | yes | The stream name prefixes the key is narrowed to. |
| `expires_at` | string (date-time) or null | yes | When the key stops working; null when it never expires. |
| `last_used_at` | string (date-time) or null | yes | Recorded at most once a minute. |
| `created_at` | string (date-time) | yes | When the key was created. |
| `key` | string | yes | The secret, shown once. `tbo_` followed by 50 base-58 characters, the last six a checksum. Matches `^tbo_[1-9A-HJ-NP-Za-km-z]{50}$`. |

**Example ApiKeyWithSecret**

```json
{
  "id": "3f1c2a9e-5b7d-4c8e-9f10-2a3b4c5d6e7f",
  "name": "<name>",
  "member": "agent.3f1c2a9e-5b7d-4c8e-9f10-2a3b4c5d6e7f",
  "member_kind": "user",
  "last4": "<last4>",
  "read_only": false,
  "buckets": [
    "<buckets>"
  ],
  "databases": [
    "<databases>"
  ],
  "database_prefixes": [
    "<database_prefixes>"
  ],
  "streams": [
    "<streams>"
  ],
  "stream_prefixes": [
    "<stream_prefixes>"
  ],
  "expires_at": "2026-11-02T09:30:00.000Z",
  "last_used_at": "2026-11-02T09:30:00.000Z",
  "created_at": "2026-11-02T09:30:00.000Z",
  "key": "<key>"
}
```
