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 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
401with the codeexchange_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
503withRetry-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_insays how long. Exchange again before it expires, or when a request answers401. 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)- Your server authenticates its own user, decides which streams that user may see, mints the token and returns its
access_tokenandexpires_atfrom an endpoint of its own. Callhttps://tonbo.dev/api/iam/tokenonly from your server; it does not answer requests from a web page. - The page keeps the token in memory, not in
localStorage, and asks your server for a new one shortly beforeexpires_at. - The page sends it as
Authorization: Bearer. Streams answer requests from every origin, and never take cookies.EventSourcecannot send that header, so tail withfetch: a long-poll, or an SSE read whose body the page reads as it arrives. With a new token, reopen the tail from the lastStream-Next-Offset(see Tail).
What the token reaches:
- Only what the request names, in
streamsandstream_prefixes, ordatabasesanddatabase_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
403and the codenarrowing_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.
- Open API keys and choose New API key.
- Name the streams under Streams, or the start of their names under Stream prefixes.
- Copy the key. It starts with
tbo_and is shown once.
tonbo api-key create events-reader --read-only --stream events --expires-in 15_dayscurl -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:
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 namestonbo stream listshows. A stream need not exist yet: a key narrowed torun-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.