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

# Rules: keys and errors

Last updated 29 October 2026

Check these rules before you hand a key or a token to a program or a web page: each keeps access narrow and errors handled.

The requests keep the stream’s URL in `STREAM`, a key in `TONBO_API_KEY`, offsets in `NEXT` and `OFFSET`, and the stream’s incarnation in `INCARNATION`, as [Append](https://tonbo.dev/docs/streams/append.md), [Read and tail](https://tonbo.dev/docs/streams/read.md) and [Snapshots and retention](https://tonbo.dev/docs/streams/snapshots.md) do.

### Exchange an Agent’s key before you send it

A stream takes a key that acts as you directly, but refuses a key that acts as an Agent with `401` and the code `exchange_required`. Exchange an Agent’s key for a token, which lasts at most 10 minutes, and exchange again before it expires (see [A short-lived token](https://tonbo.dev/docs/streams/access.md#token)).

**Do**

```shell
TOKEN=$(curl -sS -X POST "https://tonbo.dev/api/iam/token" \
  -H "Authorization: Bearer $AGENT_KEY" \
  -H "Content-Type: application/json" \
  -d '{"audience":"https://streams.tonbo.dev"}' | jq -r .access_token)
curl -sS -i "$STREAM?offset=-1" \
  -H "Authorization: Bearer $TOKEN"
```

**Don’t**

```shell
curl -sS -i "$STREAM?offset=-1" \
  -H "Authorization: Bearer $AGENT_KEY"
```

### Give each program a key narrowed to its streams

A key that acts as you and is not narrowed reaches every stream and database you can. Give a reader a read-only key narrowed to its stream, and a family of runs a key narrowed to their prefix (see [Narrow a key](https://tonbo.dev/docs/streams/access.md#narrow)).

**Do**

```shell
curl -sS -X POST "https://api.tonbo.dev/v1/api-keys" \
  -H "Authorization: Bearer $TONBO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"name":"events-reader","read_only":true,"streams":["events"],"expires_in":"15_days"}'
```

### Give a web page a token for its streams, never a key

A page should never hold an API key, or a token exchanged from one: either reaches everything the key does. Your server mints the page a read-only token for only the streams it shows, which lasts minutes (see [A token for a web page](https://tonbo.dev/docs/streams/access.md#browser)), or reads the stream itself and passes on what the page may see. A page can read and tail a public stream with no token (see [Public and private](https://tonbo.dev/docs/streams/manage.md#visibility)).

### Match a refusal’s code, never its message

A refusal’s JSON names a code, such as `STREAM_LIFECYCLE`, and a message written for people, which may change (see [Troubleshooting](https://tonbo.dev/docs/streams/troubleshooting.md)).

**Do**

```js
if (body.error.code === "STREAM_LIFECYCLE") createThroughTheApi();
```

**Don’t**

```js
if (body.error.message.includes("lifecycle")) createThroughTheApi();
```
