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

# Access

Last updated 29 October 2026

Give each program the access it needs to a stream, and no more, before you deploy it: every request to a private stream carries an API key of the stream’s account, or a short-lived token made from one.

A key is long-lived and stays on your server or in your secret store. This page shows how to send it, when to exchange it for a token that expires, how to give a web page a token for only the streams it shows, how to give a program a key that reaches only its own streams, and how to let anyone read a stream.

## Send the key

Send a key that acts as you as `Authorization: Bearer` with every request to the stream, as the [quickstart](https://tonbo.dev/docs/getting-started/quickstarts/stream.md#key) does:

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

The same key works at every region’s Streams origin: `https://streams.tonbo.dev`, `https://streams.ap-southeast-1.tonbo.dev`.

- A narrowed key keeps its narrowing: it reaches only the streams it names, and a read-only one never appends (see [Narrow a key to streams](#narrow)).
- A key that acts as an Agent, an identity of the account with its own keys and the role you give it, created on **Members**, is not accepted directly. Sent to a stream, it answers `401` with the code `exchange_required`: exchange it for a token first (see [A short-lived token](#token)).
- Revoking the key stops it at every stream within 60 seconds, usually within a few seconds.
- If Tonbo cannot check a key for a moment, the request answers `503` with `Retry-After`: send it again after that long. A token does not need that check.

## A short-lived token

Exchange the key for an access token when the key acts as an Agent, or when what a program holds should expire on its own, such as a credential a host writes to a file for another process. Send the key as a bearer token to `POST https://tonbo.dev/api/iam/token` with the Streams audience, which names the API the token is for (see [The audience](https://tonbo.dev/docs/platform/api-keys.md#audience)), and keep the `access_token` of the answer ([The token exchange](https://tonbo.dev/docs/platform/api-keys.md#exchange) lists every field):

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

Then send it in place of the key, as `Authorization: Bearer $TOKEN`. The audience is `https://streams.tonbo.dev` in every region, whichever origin serves the stream.

- Every key exchanges the same way, whether it acts as you or as an Agent. The token reaches what the key reaches, narrowing included.
- The token lasts 600 seconds, or less when the key expires sooner; its `expires_in` says how long. Exchange again before it expires, or when a request answers `401`. A long-running program exchanges in the background, every few minutes.
- Revoking the key stops new exchanges at once, and the streams refuse the tokens already exchanged from it shortly after.

## A token for a web page

Never put a key in a web page, or a token exchanged from one: it reaches everything the key does. Instead, your server mints a token for only the streams the page shows, for a few minutes, and the page calls the stream with it directly. With the key in `TONBO_API_KEY`, your server mints a read-only token for `events` alone, valid for 5 minutes:

```shell
PAGE_TOKEN=$(jq -n --arg key "$TONBO_API_KEY" '{
    grant_type: "urn:ietf:params:oauth:grant-type:token-exchange",
    subject_token: $key,
    subject_token_type: "urn:tonbo:params:oauth:token-type:api_key",
    streams: ["events"],
    read_only: true,
    expires_in: 300
  }' | curl -sS -X POST "https://tonbo.dev/api/iam/token" \
  -H "Content-Type: application/json" -d @- | jq -r .access_token)
```

1. Your server authenticates its own user, decides which streams that user may see, mints the token and returns its `access_token` and `expires_at` from an endpoint of its own. Call `https://tonbo.dev/api/iam/token` only from your server; it does not answer requests from a web page.
2. The page keeps the token in memory, not in `localStorage`, and asks your server for a new one shortly before `expires_at`.
3. The page sends it as `Authorization: Bearer`. Streams answer requests from every origin, and never take cookies. `EventSource` cannot send that header, so tail with `fetch`: a long-poll, or an SSE read whose body the page reads as it arrives. With a new token, reopen the tail from the last `Stream-Next-Offset` (see [Tail](https://tonbo.dev/docs/streams/read.md#tail)).

What the token reaches:

- Only what the request names, in `streams` and `stream_prefixes`, or `databases` and `database_prefixes`: up to 20 of each, and at least one name in all.
- Never more than the key. A name outside the key’s reach is refused with `403` and the code `narrowing_exceeds_subject`, never dropped, so the token reaches exactly what you asked for.
- Read-only unless you send `"read_only": false`, which a writable key alone may ask for; the page can then append to and close the streams it names.
- At most 600 seconds (`expires_in`, from 60 to 600, 600 when you leave it out), and never past the key’s own expiry.
- It acts as the same person or Agent as the key, so revoking the key stops it too; otherwise it ends at its expiry. Like the key, it reaches only the key’s account. A server can mint from a key that acts as an Agent of its account, which works here without an exchange.

[Tokens for a web page](https://tonbo.dev/docs/platform/api-keys.md#down-scope) lists every field and every refusal.

## Narrow a key to streams

Give each program a key of its own that reaches only its streams, so a leaked key exposes nothing else. A key that acts as you and is not narrowed reaches every stream and database you can in the account you created it in, and nothing of your other accounts.

**Console**

1. Open **API keys** and choose **New API key**.
2. Name the streams under **Streams**, or the start of their names under **Stream prefixes**.
3. Copy the key. It starts with `tbo_` and is shown once.

**CLI**

```shell
tonbo api-key create events-reader --read-only --stream events --expires-in 15_days
```

**API**

```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"}'
```

That key, read-only and narrowed to `events`, reads and tails `events`; an append answers `403`, and every other stream and every database answers `403` or `404`. Drop `--read-only` for a key that appends too. A key for a family of streams names their prefix instead:

```shell
tonbo api-key create run-writer --stream-prefix run-
```

- A key names up to 20 streams (`--stream`, `streams`) and up to 20 prefixes (`--stream-prefix`, `stream_prefixes`), by the names `tonbo stream list` shows. A stream need not exist yet: a key narrowed to `run-` reaches each run’s stream as it is created.
- The narrowing is fixed for the key’s life. A different one takes a new key.
- A key narrowed to streams reaches no database, unless it also names databases; then it reaches both.
- Through the Management API a narrowed key only reads: the account, and the streams it reaches. It never creates or deletes a stream; create them with a key that acts as you, or as an Agent.

To keep a program’s key independent of any person, create an Agent on **Members** and a key that acts as it, narrowed the same way, and exchange it for a token (see [A short-lived token](#token)).

## Public streams

An owner or admin can publish a stream (see [Public and private](https://tonbo.dev/docs/streams/manage.md#visibility)). Anyone with its URL then reads it, tails it with a long-poll or SSE, and sends `HEAD`, with no `Authorization` header at all. Appends still need a key or a token. Anonymous readers reach only the stream they were given: a stream deleted and created again under the same name is a new, private stream until it is published again.
