Access

Last updated 29 October 2026

View .md

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 does:

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).
  • 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).
  • 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), and keep the access_token of the answer (The token exchange lists every field):

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:

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).

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 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.

  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.

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:

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).

Public streams

An owner or admin can publish a stream (see Public and private). 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.