Read, tail and HEAD
Last updated 2 November 2026
Every parameter, header and status of a request that reads a stream: a read, a long-poll, an SSE tail, HEAD and the reads a cache can keep.
Reads are GET and HEAD on the stream’s URL, and change nothing, so any of them may be sent again; the guide is Read and tail. The headers every request shares and the error codes are on the overview.
Read
Reads the stream from an offset to its end, or as far as one answer carries, and says where to read next.
GET {stream}?offset=… · Protocol: protocol 5.6, protocol 6, protocol 7.1, protocol 8.1, extensions 9.2, extensions 9.3, extensions 9.4
Credential: A key or token that reaches the stream, read-only or not; none for a public stream.
Request
| Parameter | Value | Required | What it does |
|---|---|---|---|
offset query | offset | Optional; -1 when omitted | Where to start: -1, the start of the stream; now, its current end, which answers at once with nothing and the end as Stream-Next-Offset; or a Stream-Next-Offset an earlier answer gave. An offset before the stream's retention answers 410 (Offsets). (protocol 5.6) |
consistency query | local or leader | Optional; local when omitted | local reads from the replica that answers, which may not yet hold the latest acknowledged appends. leader sees every append acknowledged before the request was sent, for a reader that must observe its own write. Any other value, or leader with live, answers 400. (extensions 9.2) |
max_bytes query | positive integer | Optional; 8 MiB when omitted | The most bytes one answer returns; a larger value counts as 8 MiB, and a value that is not an integer is ignored. On a JSON stream it counts bytes of the newline-delimited answer, so an answer may end inside a message: keep the incomplete last line until the next answer completes it. On a tail it bounds each answer or each data event. (extensions 9.3) |
If-None-Match header | entity tag | Optional | The ETag of an earlier answer at the same offset: when nothing changed, the answer is 304 with no body. (protocol 8.1) |
Answer
| Field | Value | When | What it means |
|---|---|---|---|
Content-Type header | media type | On 200 | The stream's content type, except on a JSON stream, whose reads are application/x-ndjson: one message per line, each ending in a newline, as it was stored. (extensions 9.4) |
Stream-Next-Offset header | offset | On 200 and 304 | Where to read next. It equals the request's offset when the answer is empty. (protocol 5.6) |
Stream-Up-To-Date header | true | When the answer reached the end | The answer holds everything the stream had when it was made. Absent, the answer stopped short, at 8 MiB or at max_bytes: read again from Stream-Next-Offset. It is not the end of the stream: only Stream-Closed says nothing more will come. (protocol 5.6) |
Stream-Closed header | true | When the stream is closed and the answer reaches its end | Nothing more will ever be appended: the reader is done. A read that stops short of the end of a closed stream does not carry it. (protocol 5.6) |
Stream-Cursor header | string | Sometimes | Send it back as cursor on the next long-poll. (protocol 5.6) |
ETag header | entity tag | On 200, but not for offset=now | Names this answer's bytes, and changes when the stream closes: send it as If-None-Match to read the same offset again without its body. (protocol 5.6) |
Statuses
| Status | What it means |
|---|---|
200 | The bytes from the offset on, or an empty body at the end of the stream. |
304 | Nothing changed since the ETag the request sent. |
400 | A malformed offset, or a consistency that is not local or leader or is combined with live. |
| Refusal | When | Send again |
|---|---|---|
401 | The key or token is missing, expired or revoked, or the request sent a key that acts as an Agent, which answers the code exchange_required. | With a valid key or a new token |
403 | The key is narrowed and does not reach this stream, or is read-only and the request appends. | No |
404 | The stream does not exist yet, is still being created, or was deleted or expired; a request with a missing or expired token may answer the same. | Once it is active |
410 | The offset is before the stream’s retention, which a snapshot replaced. | From the answer’s offset |
412 | The request carried Stream-Incarnation, and the stream was deleted and created again since. | No |
429 | The account sent more requests per second, or held more tails open at once, than its region allows. | After Retry-After |
503 | The region cannot take the request, or check an API key sent directly, for a moment. | Unchanged, after Retry-After |
Sending it again: Always safe: a read changes nothing.
Example
curl -sS -i "$STREAM?offset=-1" \
-H "Authorization: Bearer $TONBO_API_KEY"Long-poll
At the end of the stream, waits until something is appended or the timeout passes, then answers once.
GET {stream}?offset=…&live=long-poll · Protocol: protocol 5.7, extensions 9.1, protocol 8.1
Credential: As for a read. Each long-poll held open counts toward the account's 16 open tails in the region (Limits).
Request
| Parameter | Value | Required | What it does |
|---|---|---|---|
offset query | offset | Required | Where to wait: the last Stream-Next-Offset, or now to wait for only what is appended next. Before the end, the answer is at once, as a read. (protocol 5.7) |
live query | long-poll | Required | Makes the read wait at the end of the stream. (protocol 5.7) |
timeout_ms query | integer, 1 to 60000 | Optional; 1000 when omitted | How long to wait, in milliseconds, before answering 204. A larger value counts as 60000, and one that is not a number as the default. Every other read ignores it. (extensions 9.1) |
cursor query | string | Optional | The last Stream-Cursor an answer gave. Send it on every long-poll after the first, so a cache in front never serves the same empty answer twice. (protocol 5.7) |
Answer
| Field | Value | When | What it means |
|---|---|---|---|
Stream-Next-Offset header | offset | On 200 and 204 | Where to wait next. On a 204, the end of the stream. (protocol 5.7) |
Stream-Up-To-Date header | true | On 204, and on a 200 that reached the end | The reader holds everything appended so far. (protocol 5.7) |
Stream-Cursor header | string | On every answer while the stream is open | Send it back as cursor on the next long-poll. Absent once the stream is closed. (protocol 5.7) |
Stream-Closed header | true | When the stream is closed | Nothing more will come. At the end of a closed stream the long-poll answers 204 with it at once, without waiting. (protocol 5.7) |
Statuses
| Status | What it means |
|---|---|
200 | What was appended while it waited, as a read answers. |
204 | The timeout passed with nothing appended, or the stream is closed at the end: read again from Stream-Next-Offset unless Stream-Closed is set. |
400 | A missing or malformed offset or parameter. |
| Refusal | When | Send again |
|---|---|---|
401 | The key or token is missing, expired or revoked, or the request sent a key that acts as an Agent, which answers the code exchange_required. | With a valid key or a new token |
403 | The key is narrowed and does not reach this stream, or is read-only and the request appends. | No |
404 | The stream does not exist yet, is still being created, or was deleted or expired; a request with a missing or expired token may answer the same. | Once it is active |
410 | The offset is before the stream’s retention, which a snapshot replaced. | From the answer’s offset |
412 | The request carried Stream-Incarnation, and the stream was deleted and created again since. | No |
429 | The account sent more requests per second, or held more tails open at once, than its region allows. | After Retry-After |
503 | The region cannot take the request, or check an API key sent directly, for a moment. | Unchanged, after Retry-After |
Sending it again: Always safe. After a dropped connection, a 408 or a 5xx, wait a moment and long-poll again from the last Stream-Next-Offset; after a 429 or 503, at least its Retry-After.
Example
curl -sS -i "$STREAM?offset=$NEXT&live=long-poll&timeout_ms=30000" \
-H "Authorization: Bearer $TONBO_API_KEY"Tail with Server-Sent Events
Keeps one connection open and sends what is appended as Server-Sent Events, each batch followed by a control event with the next offset.
GET {stream}?offset=…&live=sse · Protocol: protocol 5.8, protocol 6, extensions 9.3, extensions 9.5
Credential: As for a read, sent with the request, so a client that can set Authorization (not a browser's EventSource) or a public stream. Each open tail counts toward the account's 16 open tails in the region.
Request
| Parameter | Value | Required | What it does |
|---|---|---|---|
offset query | offset | Required | Where to start: the last streamNextOffset, -1, or now for only what is appended next. (protocol 5.8) |
live query | sse | Required | Answers as an event stream instead of one body. (protocol 5.8) |
max_bytes query | positive integer | Optional; 8 MiB when omitted | The most bytes one data event carries. A text stream's events carry at least 4 bytes, so each holds a whole character. (protocol 5.8) |
Accept header | text/event-stream | Optional | The answer is an event stream whether or not it is sent; the docs send it for clients and proxies that expect it. |
Answer
| Field | Value | When | What it means |
|---|---|---|---|
Content-Type header | text/event-stream | On 200 | The answer is an event stream. (protocol 5.8) |
Stream-Data-Content-Type header | media type | On 200 | What a data event's payload is once decoded: application/x-ndjson for a JSON stream, the stream's content type otherwise. (protocol 5.8) |
Stream-Sse-Data-Encoding header | base64 | On a stream neither text/* nor JSON | Each data event is the standard base64 of a range of the stream's bytes: join its lines, remove the line breaks, then decode. (extensions 9.5) |
data event | text | For each batch | What was appended. Its data: lines are transport framing, not message boundaries: on a JSON stream, join them and split messages at each newline, keeping an incomplete last line for the next event. (protocol 5.8) |
control event | JSON object | After every data event | Where the reader stands, in the fields below. Reconnect from its streamNextOffset. (protocol 5.8) |
streamNextOffset control event | offset | Always | The offset after the bytes sent so far: where a new connection starts. (protocol 5.8) |
streamCursor control event | string | While the stream is open | The cursor, as Stream-Cursor is for a long-poll. (protocol 5.8) |
upToDate control event | true | When the reader has everything | Everything appended so far has been sent. (protocol 5.8) |
streamClosed control event | true | When the stream is closed | The last event: the stream is closed and everything was sent. The server ends the connection; do not reconnect. (protocol 5.8) |
error event | JSON error body | When the tail fails | The tail ended with the error body of Error codes, such as the stream being deleted. Reconnect, and the answer to the reconnect decides. (protocol 5.8) |
credential-expired event | {"reason":"credential_expired"} | At the token's expiry | The token the tail was opened with has expired, and the connection ends. It is not the stream's end: reconnect from the last streamNextOffset with a fresh token. |
Statuses
| Status | What it means |
|---|---|
200 | The event stream. A proxy may close a quiet connection, since the server sends no heartbeat: reconnect from the last streamNextOffset unless the last control event had streamClosed. |
400 | A missing or malformed offset or parameter. |
| Refusal | When | Send again |
|---|---|---|
401 | The key or token is missing, expired or revoked, or the request sent a key that acts as an Agent, which answers the code exchange_required. | With a valid key or a new token |
403 | The key is narrowed and does not reach this stream, or is read-only and the request appends. | No |
404 | The stream does not exist yet, is still being created, or was deleted or expired; a request with a missing or expired token may answer the same. | Once it is active |
410 | The offset is before the stream’s retention, which a snapshot replaced. | From the answer’s offset |
412 | The request carried Stream-Incarnation, and the stream was deleted and created again since. | No |
429 | The account sent more requests per second, or held more tails open at once, than its region allows. | After Retry-After |
503 | The region cannot take the request, or check an API key sent directly, for a moment. | Unchanged, after Retry-After |
Sending it again: Always safe. Reconnect from the last streamNextOffset after a dropped connection, an error event or a credential-expired event; after a 429 or 503, wait at least its Retry-After.
Example
curl -sS -N "$STREAM?offset=$NEXT&live=sse" \
-H "Authorization: Bearer $TONBO_API_KEY" \
-H "Accept: text/event-stream"HEAD
Reads the stream's state without its contents: its end, whether it is closed, its retention, its latest snapshot and its limits.
HEAD {stream} · Protocol: protocol 5.5, extensions 2.4, extensions 4.4, extensions 10
Credential: As for a read.
Answer
| Field | Value | When | What it means |
|---|---|---|---|
Content-Type header | media type | On 200 | The stream's content type, which every append sends: application/json for a JSON stream, though its reads are application/x-ndjson. (protocol 5.5) |
Stream-Next-Offset header | offset | On 200 | The end of the stream, where the next append starts. A HEAD sees every append acknowledged before it was sent. (protocol 5.5) |
Stream-Closed header | true | When the stream is closed | Absent while the stream is open. (protocol 5.5) |
Stream-Retained-Offset header | offset | On 200 | The earliest offset still readable: reads before it answer 410. The start of the stream until retention advances. (extensions 2.4) |
Stream-Snapshot-Offset header | offset | When a snapshot exists | The offset of the latest snapshot, which Read a snapshot takes. HEAD and bootstrap are the only ways to learn it. (extensions 2.4) |
Stream-Snapshot-Digest header | 64 lowercase hexadecimal digits | When a snapshot exists | The latest snapshot's digest, the one its publish answered. (extensions 2.4) |
Stream-Previous-Incarnation header | token | After a rewrite of the stream's history | The stream's Stream-Incarnation before its history was last rewritten, as by a restore. A reader that held it keeps what it read below Stream-Inherited-Offset; any other reader treats what it read as void. (extensions 2.4) |
Stream-Inherited-Offset header | offset | With Stream-Previous-Incarnation | Below this offset the stream's bytes are the same as under the previous incarnation; at or above it they may differ. (extensions 2.4) |
Ursula-Max-Request-Bytes header | integer | On 200 | The largest request body the stream accepts, 8 MiB today (Limits). Send no larger body. (extensions 10) |
Ursula-Max-Snapshot-Bytes header | integer | On 200 | The largest snapshot the stream accepts in parts. (extensions 10) |
Stream-Read-Origin header | origin | Not sent today | Another origin that serves the cacheable reads of this stream. Tonbo's regions send none; a client that finds one may ignore it, and sends only those reads there. (extensions 4.4) |
Statuses
| Status | What it means |
|---|---|
200 | The stream exists; its state is in the headers. |
| Refusal | When | Send again |
|---|---|---|
401 | The key or token is missing, expired or revoked, or the request sent a key that acts as an Agent, which answers the code exchange_required. | With a valid key or a new token |
403 | The key is narrowed and does not reach this stream, or is read-only and the request appends. | No |
404 | The stream does not exist yet, is still being created, or was deleted or expired; a request with a missing or expired token may answer the same. | Once it is active |
412 | The request carried Stream-Incarnation, and the stream was deleted and created again since. | No |
429 | The account sent more requests per second, or held more tails open at once, than its region allows. | After Retry-After |
503 | The region cannot take the request, or check an API key sent directly, for a moment. | Unchanged, after Retry-After |
Sending it again: Always safe.
Example
curl -sS -I "$STREAM" \
-H "Authorization: Bearer $TONBO_API_KEY"Read a cacheable chunk
Reads the stream from an offset to the next 4 MiB boundary, or to its end, at a URL whose answer, once marked, never changes.
GET {stream}/incarnation/{token}/chunk/{offset} · Protocol: extensions 4.1, extensions 4.2, extensions 4.3
Credential: As for a read. Only an answer to a request without a credential, of a public stream, may be kept by a shared cache.
Request
| Parameter | Value | Required | What it does |
|---|---|---|---|
token path | token | Required | The stream's Stream-Incarnation, from HEAD or any answer. Once the stream is deleted and created again, the URL answers 412. A Stream-Incarnation request header changes nothing here. (extensions 4.1) |
offset path | offset | Required | Where the chunk starts, URL-encoded. The chunks of every reader meet at the same 4 MiB boundaries, so after one answer every reader requests the same URLs. A query on the URL answers 400. (extensions 4.1) |
If-None-Match header | entity tag | Optional | An earlier answer's ETag: 304 when it still holds. (extensions 4.1) |
Answer
| Field | Value | When | What it means |
|---|---|---|---|
Content-Type header | media type | On 200 and 304 | As for a read: application/x-ndjson on a JSON stream. A chunk may end inside a message. (extensions 4.1) |
Stream-Offset header | offset | On 200 and 304 | The first offset the body holds: the one in the URL. (extensions 4.1) |
Stream-Next-Offset header | offset | On 200 and 304 | The offset after the body's last byte: the next chunk's URL. (extensions 4.1) |
Stream-Closed header | true | When the chunk ends at the end of a closed stream | Nothing more will come. (extensions 4.1) |
Stream-Immutable header | true | When the answer can never change | Every later answer for this URL is the same, so it can be kept for good. A chunk that ends at the open end of the stream is not marked: read on from there with an ordinary read. (extensions 4.3) |
ETag header | entity tag | On 200 and 304 | Names the chunk, for If-None-Match. (extensions 4.1) |
Statuses
| Status | What it means |
|---|---|
200 | The chunk. |
304 | The ETag the request sent still names the chunk. |
400 | A query on the URL, or a token or offset in a form the stream never gives. |
416 | The stream now at this URL, after a recreate, ends before the offset. |
| Refusal | When | Send again |
|---|---|---|
401 | The key or token is missing, expired or revoked, or the request sent a key that acts as an Agent, which answers the code exchange_required. | With a valid key or a new token |
403 | The key is narrowed and does not reach this stream, or is read-only and the request appends. | No |
404 | The stream does not exist yet, is still being created, or was deleted or expired; a request with a missing or expired token may answer the same. | Once it is active |
410 | The offset is before the stream’s retention, which a snapshot replaced. | From the answer’s offset |
412 | The request carried Stream-Incarnation, and the stream was deleted and created again since. | No |
429 | The account sent more requests per second, or held more tails open at once, than its region allows. | After Retry-After |
503 | The region cannot take the request, or check an API key sent directly, for a moment. | Unchanged, after Retry-After |
Sending it again: Always safe.
Example
curl -sS -i "$STREAM/incarnation/$INCARNATION/chunk/$OFFSET" \
-H "Authorization: Bearer $TONBO_API_KEY"Read the snapshot at a cacheable URL
Reads the latest snapshot at a URL that names the stream's incarnation, so its answer can be kept for good.
GET {stream}/incarnation/{token}/snapshot/{snapshot_offset} · Protocol: extensions 4.1, extensions 4.3
Credential: As for a cacheable chunk.
Request
| Parameter | Value | Required | What it does |
|---|---|---|---|
token path | token | Required | The stream's Stream-Incarnation, as for a chunk. (extensions 4.1) |
snapshot_offset path | offset | Required | The latest snapshot's offset, from Stream-Snapshot-Offset; any other answers 404. (extensions 4.1) |
Answer
| Field | Value | When | What it means |
|---|---|---|---|
Stream-Snapshot-Offset header | offset | On 200, 206 and 304 | The snapshot's offset. (extensions 4.1) |
Stream-Snapshot-Digest header | 64 lowercase hexadecimal digits | On 200, 206 and 304 | The snapshot's digest, as its publish answered. (extensions 4.1) |
Stream-Immutable header | true | On 200 and 206 | A snapshot at its URL never changes. (extensions 4.1) |
Statuses
| Status | What it means |
|---|---|
200 | The snapshot, with the headers of Read a snapshot; Range and If-None-Match work the same. |
206 | The byte range the request's Range named. |
| Refusal | When | Send again |
|---|---|---|
401 | The key or token is missing, expired or revoked, or the request sent a key that acts as an Agent, which answers the code exchange_required. | With a valid key or a new token |
403 | The key is narrowed and does not reach this stream, or is read-only and the request appends. | No |
404 | A snapshot read names an offset that is not the latest snapshot’s. | At the latest offset |
412 | The request carried Stream-Incarnation, and the stream was deleted and created again since. | No |
429 | The account sent more requests per second, or held more tails open at once, than its region allows. | After Retry-After |
503 | The region cannot take the request, or check an API key sent directly, for a moment. | Unchanged, after Retry-After |
Sending it again: Always safe.
Example
curl -sS -i "$STREAM/incarnation/$INCARNATION/snapshot/$OFFSET" \
-H "Authorization: Bearer $TONBO_API_KEY"