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

# Quickstart: durable stream

Last updated 29 October 2026

From sign-in to a stream you append to, read, tail live and close, with `curl` and `jq`.

Run it in a terminal on your laptop or on any server with internet access. A stream is reached over HTTPS at a public address, with nothing to install beyond the CLI, `curl` and `jq`, and nothing to allowlist; the [Streams overview](https://tonbo.dev/docs/streams.md#where) says where else it runs.

## Sign in

Sign in at [tonbo.dev](https://tonbo.dev/login) with GitHub or Google. A new account asks you to choose a username: it names your personal account, which holds your databases, streams and keys, and appears in the console’s addresses. You can change it once every 90 days.

## Create a stream

Create a stream named `events` for JSON messages. Its content type is fixed for its life, and every append sends exactly it.

**Console**

1. Open **Streams** in the console.
2. Choose **Create stream**, name it `events` and set **Content type** to `application/json`.

**CLI**

```shell
npm i -g @tonbo/cli
tonbo login
tonbo stream create events --content-type application/json
```

**API**

With a key that acts as you in `TONBO_API_KEY` (the next step creates one):

```shell
curl -sS -X POST "https://api.tonbo.dev/v1/streams" \
  -H "Authorization: Bearer $TONBO_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{"name":"events","content_type":"application/json"}'
```

It answers `201` once the stream is active, or `202` while it is still being created.

The stream is creating for a moment, then active, at a URL on its region’s Streams origin:

```text
https://streams.tonbo.dev/<account-id>/s.events
```

## Create a key

Every request to the stream carries an API key. With the CLI (`npm i -g @tonbo/cli`, then `tonbo login`), create a key that acts as you:

```shell
tonbo api-key create events-quickstart --expires-in 15_days
```

`tonbo login` signs you in through a browser on the same machine. On a machine without one, such as a remote server, set `TONBO_API_KEY` to a key that acts as you and is not narrowed, created in the console under **API keys**, and the CLI uses it instead.

It prints the key once, as `TONBO_API_KEY=tbo_...`. This key acts as you and is not narrowed, so the CLI may keep using it: put it in your shell, and keep the stream’s URL in `STREAM`:

```shell
export TONBO_API_KEY=tbo_...
STREAM=$(tonbo stream show events --json | jq -r .url)
```

Each request below sends the key as `Authorization: Bearer $TONBO_API_KEY`. Keep the key on your machine or your server; to give a program a key that reaches only its own streams, or a web page a token that expires in minutes, see [Access](https://tonbo.dev/docs/streams/access.md).

## Append

Append two messages, each with a producer id, an epoch and the next sequence number, which name the writer so a retry applies once (see [A producer](https://tonbo.dev/docs/streams/concepts.md#producer)):

```shell
curl -sS -i -X POST "$STREAM" \
  -H "Authorization: Bearer $TONBO_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Producer-Id: quickstart" \
  -H "Producer-Epoch: 0" \
  -H "Producer-Seq: 0" \
  -d '{"n":0}'

curl -sS -i -X POST "$STREAM" \
  -H "Authorization: Bearer $TONBO_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Producer-Id: quickstart" \
  -H "Producer-Epoch: 0" \
  -H "Producer-Seq: 1" \
  -d '{"n":1}'
```

Each answers `200` with `Stream-Offset` and `Stream-Next-Offset`, where its message starts and ends. Send the second one again, as a client that never saw its answer would: it answers `204` with the same two offsets and appends nothing, so a retry never writes a message twice.

```shell
curl -sS -i -X POST "$STREAM" \
  -H "Authorization: Bearer $TONBO_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Producer-Id: quickstart" \
  -H "Producer-Epoch: 0" \
  -H "Producer-Seq: 1" \
  -d '{"n":1}'
```

## Read

Read the stream from the start:

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

It answers `200` with the two messages, one JSON value per line, and `Stream-Up-To-Date: true`. Its `Stream-Next-Offset` is the end of the stream: keep it for the next step.

```shell
NEXT=<the Stream-Next-Offset value>
```

## Tail it

Open a tail at the end, and it waits for what is appended next. Pick one mode and run it:

**SSE**

```shell
curl -sS -N "$STREAM?offset=$NEXT&live=sse" \
  -H "Authorization: Bearer $TONBO_API_KEY" \
  -H "Accept: text/event-stream"
```

The connection stays open and prints an `event: data` for each new message, then an `event: control` with the next offset.

**Long-poll**

```shell
curl -sS -i "$STREAM?offset=$NEXT&live=long-poll&timeout_ms=30000" \
  -H "Authorization: Bearer $TONBO_API_KEY"
```

It answers as soon as a message arrives, with that message, or `204` after 30 seconds without one.

While it waits, append the third message from a second terminal with the same `STREAM` and `TONBO_API_KEY`:

```shell
curl -sS -i -X POST "$STREAM" \
  -H "Authorization: Bearer $TONBO_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Producer-Id: quickstart" \
  -H "Producer-Epoch: 0" \
  -H "Producer-Seq: 2" \
  -d '{"n":2}'
```

The tail shows it at once. Stop an SSE tail with Ctrl-C.

## Close it

Closing is permanent: the stream keeps everything and stays readable, and refuses every append after it.

```shell
curl -sS -i -X POST "$STREAM" \
  -H "Authorization: Bearer $TONBO_API_KEY" \
  -H "Stream-Closed: true"
```

It answers `204` with `Stream-Closed: true`. From now on every read at the end says the stream is over: a read answers `200` with no messages and `Stream-Closed: true`, a long-poll answers `204` with `Stream-Closed: true` at once instead of waiting, and an SSE read gets a control event with `streamClosed: true` before the server ends it. An append answers `409`.

## Delete it

Deleting removes the stream and everything in it, and cannot be undone. The CLI asks you to type the name:

```shell
tonbo stream delete events
```

Through the API, the same is `DELETE https://api.tonbo.dev/v1/streams/events`. Once it is deleted, the stream’s URL answers `404` and the name is free again.

## Add it to a project

In a project of your own, with the program’s API key in `TONBO_API_KEY` where the project keeps its secrets, never in its code:

1. Create the stream with `tonbo stream create events --content-type application/json`, signed in with `tonbo login`, or in the console. `tonbo stream show events` prints `State:` `active` once it is ready, and `tonbo stream show events --json | jq -r .url` prints its URL: put it beside the key.
2. The program appends with a `POST` to the URL, with `Authorization: Bearer` and the key, the stream’s content type, and `Producer-Id`, `Producer-Epoch` and `Producer-Seq`, so a retry applies once (see [Append](https://tonbo.dev/docs/streams/append.md)).
3. It reads with a `GET` to the URL, from `?offset=-1` for the start (see [Read](https://tonbo.dev/docs/streams/read.md)).

## Next steps

- [Append](https://tonbo.dev/docs/streams/append.md): Exactly-once appends, batches, closing, and what each answer means.
- [Read and tail](https://tonbo.dev/docs/streams/read.md): Offsets, both tails, a closed stream, and reads you can cache.
- [Rules for streams](https://tonbo.dev/docs/streams/rules.md): What to do and what not to, before a program relies on a stream.
- [Access](https://tonbo.dev/docs/streams/access.md): Give a reader its own read-only key narrowed to one stream, give a web page a short-lived token, or publish a stream for anyone.
- [Manage streams](https://tonbo.dev/docs/streams/manage.md): Regions, content types, expiry, listing and deletion.
- [Limits](https://tonbo.dev/docs/streams/limits.md): Body and request limits, live reads, and streams per account.
