Read, tail and HEAD

Last updated 2 November 2026

View .md

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

ParameterValueRequiredWhat it does
offset queryoffsetOptional; -1 when omittedWhere 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 querylocal or leaderOptional; local when omittedlocal 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 querypositive integerOptional; 8 MiB when omittedThe 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 headerentity tagOptionalThe ETag of an earlier answer at the same offset: when nothing changed, the answer is 304 with no body. (protocol 8.1)

Answer

FieldValueWhenWhat it means
Content-Type headermedia typeOn 200The 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 headeroffsetOn 200 and 304Where to read next. It equals the request's offset when the answer is empty. (protocol 5.6)
Stream-Up-To-Date headertrueWhen the answer reached the endThe 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 headertrueWhen the stream is closed and the answer reaches its endNothing 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 headerstringSometimesSend it back as cursor on the next long-poll. (protocol 5.6)
ETag headerentity tagOn 200, but not for offset=nowNames 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

StatusWhat it means
200The bytes from the offset on, or an empty body at the end of the stream.
304Nothing changed since the ETag the request sent.
400A malformed offset, or a consistency that is not local or leader or is combined with live.
RefusalWhenSend again
401The 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
403The key is narrowed and does not reach this stream, or is read-only and the request appends.No
404The 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
410The offset is before the stream’s retention, which a snapshot replaced.From the answer’s offset
412The request carried Stream-Incarnation, and the stream was deleted and created again since.No
429The account sent more requests per second, or held more tails open at once, than its region allows.After Retry-After
503The 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

ParameterValueRequiredWhat it does
offset queryoffsetRequiredWhere 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 querylong-pollRequiredMakes the read wait at the end of the stream. (protocol 5.7)
timeout_ms queryinteger, 1 to 60000Optional; 1000 when omittedHow 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 querystringOptionalThe 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

FieldValueWhenWhat it means
Stream-Next-Offset headeroffsetOn 200 and 204Where to wait next. On a 204, the end of the stream. (protocol 5.7)
Stream-Up-To-Date headertrueOn 204, and on a 200 that reached the endThe reader holds everything appended so far. (protocol 5.7)
Stream-Cursor headerstringOn every answer while the stream is openSend it back as cursor on the next long-poll. Absent once the stream is closed. (protocol 5.7)
Stream-Closed headertrueWhen the stream is closedNothing 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

StatusWhat it means
200What was appended while it waited, as a read answers.
204The timeout passed with nothing appended, or the stream is closed at the end: read again from Stream-Next-Offset unless Stream-Closed is set.
400A missing or malformed offset or parameter.
RefusalWhenSend again
401The 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
403The key is narrowed and does not reach this stream, or is read-only and the request appends.No
404The 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
410The offset is before the stream’s retention, which a snapshot replaced.From the answer’s offset
412The request carried Stream-Incarnation, and the stream was deleted and created again since.No
429The account sent more requests per second, or held more tails open at once, than its region allows.After Retry-After
503The 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

ParameterValueRequiredWhat it does
offset queryoffsetRequiredWhere to start: the last streamNextOffset, -1, or now for only what is appended next. (protocol 5.8)
live querysseRequiredAnswers as an event stream instead of one body. (protocol 5.8)
max_bytes querypositive integerOptional; 8 MiB when omittedThe 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 headertext/event-streamOptionalThe answer is an event stream whether or not it is sent; the docs send it for clients and proxies that expect it.

Answer

FieldValueWhenWhat it means
Content-Type headertext/event-streamOn 200The answer is an event stream. (protocol 5.8)
Stream-Data-Content-Type headermedia typeOn 200What 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 headerbase64On a stream neither text/* nor JSONEach 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 eventtextFor each batchWhat 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 eventJSON objectAfter every data eventWhere the reader stands, in the fields below. Reconnect from its streamNextOffset. (protocol 5.8)
streamNextOffset control eventoffsetAlwaysThe offset after the bytes sent so far: where a new connection starts. (protocol 5.8)
streamCursor control eventstringWhile the stream is openThe cursor, as Stream-Cursor is for a long-poll. (protocol 5.8)
upToDate control eventtrueWhen the reader has everythingEverything appended so far has been sent. (protocol 5.8)
streamClosed control eventtrueWhen the stream is closedThe last event: the stream is closed and everything was sent. The server ends the connection; do not reconnect. (protocol 5.8)
error eventJSON error bodyWhen the tail failsThe 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 expiryThe 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

StatusWhat it means
200The 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.
400A missing or malformed offset or parameter.
RefusalWhenSend again
401The 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
403The key is narrowed and does not reach this stream, or is read-only and the request appends.No
404The 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
410The offset is before the stream’s retention, which a snapshot replaced.From the answer’s offset
412The request carried Stream-Incarnation, and the stream was deleted and created again since.No
429The account sent more requests per second, or held more tails open at once, than its region allows.After Retry-After
503The 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"

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

FieldValueWhenWhat it means
Content-Type headermedia typeOn 200The 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 headeroffsetOn 200The end of the stream, where the next append starts. A HEAD sees every append acknowledged before it was sent. (protocol 5.5)
Stream-Closed headertrueWhen the stream is closedAbsent while the stream is open. (protocol 5.5)
Stream-Retained-Offset headeroffsetOn 200The earliest offset still readable: reads before it answer 410. The start of the stream until retention advances. (extensions 2.4)
Stream-Snapshot-Offset headeroffsetWhen a snapshot existsThe 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 header64 lowercase hexadecimal digitsWhen a snapshot existsThe latest snapshot's digest, the one its publish answered. (extensions 2.4)
Stream-Previous-Incarnation headertokenAfter a rewrite of the stream's historyThe 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 headeroffsetWith Stream-Previous-IncarnationBelow 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 headerintegerOn 200The largest request body the stream accepts, 8 MiB today (Limits). Send no larger body. (extensions 10)
Ursula-Max-Snapshot-Bytes headerintegerOn 200The largest snapshot the stream accepts in parts. (extensions 10)
Stream-Read-Origin headeroriginNot sent todayAnother 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

StatusWhat it means
200The stream exists; its state is in the headers.
RefusalWhenSend again
401The 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
403The key is narrowed and does not reach this stream, or is read-only and the request appends.No
404The 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
412The request carried Stream-Incarnation, and the stream was deleted and created again since.No
429The account sent more requests per second, or held more tails open at once, than its region allows.After Retry-After
503The 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

ParameterValueRequiredWhat it does
token pathtokenRequiredThe 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 pathoffsetRequiredWhere 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 headerentity tagOptionalAn earlier answer's ETag: 304 when it still holds. (extensions 4.1)

Answer

FieldValueWhenWhat it means
Content-Type headermedia typeOn 200 and 304As for a read: application/x-ndjson on a JSON stream. A chunk may end inside a message. (extensions 4.1)
Stream-Offset headeroffsetOn 200 and 304The first offset the body holds: the one in the URL. (extensions 4.1)
Stream-Next-Offset headeroffsetOn 200 and 304The offset after the body's last byte: the next chunk's URL. (extensions 4.1)
Stream-Closed headertrueWhen the chunk ends at the end of a closed streamNothing more will come. (extensions 4.1)
Stream-Immutable headertrueWhen the answer can never changeEvery 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 headerentity tagOn 200 and 304Names the chunk, for If-None-Match. (extensions 4.1)

Statuses

StatusWhat it means
200The chunk.
304The ETag the request sent still names the chunk.
400A query on the URL, or a token or offset in a form the stream never gives.
416The stream now at this URL, after a recreate, ends before the offset.
RefusalWhenSend again
401The 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
403The key is narrowed and does not reach this stream, or is read-only and the request appends.No
404The 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
410The offset is before the stream’s retention, which a snapshot replaced.From the answer’s offset
412The request carried Stream-Incarnation, and the stream was deleted and created again since.No
429The account sent more requests per second, or held more tails open at once, than its region allows.After Retry-After
503The 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

ParameterValueRequiredWhat it does
token pathtokenRequiredThe stream's Stream-Incarnation, as for a chunk. (extensions 4.1)
snapshot_offset pathoffsetRequiredThe latest snapshot's offset, from Stream-Snapshot-Offset; any other answers 404. (extensions 4.1)

Answer

FieldValueWhenWhat it means
Stream-Snapshot-Offset headeroffsetOn 200, 206 and 304The snapshot's offset. (extensions 4.1)
Stream-Snapshot-Digest header64 lowercase hexadecimal digitsOn 200, 206 and 304The snapshot's digest, as its publish answered. (extensions 4.1)
Stream-Immutable headertrueOn 200 and 206A snapshot at its URL never changes. (extensions 4.1)

Statuses

StatusWhat it means
200The snapshot, with the headers of Read a snapshot; Range and If-None-Match work the same.
206The byte range the request's Range named.
RefusalWhenSend again
401The 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
403The key is narrowed and does not reach this stream, or is read-only and the request appends.No
404A snapshot read names an offset that is not the latest snapshot’s.At the latest offset
412The request carried Stream-Incarnation, and the stream was deleted and created again since.No
429The account sent more requests per second, or held more tails open at once, than its region allows.After Retry-After
503The 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"