Snapshots and bootstrap
Last updated 2 November 2026
Every header and status of the requests that publish and read a stream’s snapshot, upload one in parts, advance its retention and bootstrap a reader.
These requests go to paths under the stream’s URL; the guide is Snapshots and retention. The headers every request shares, the error codes and how to tell whether a write applied are on the overview.
Publish a snapshot
Stores a summary of the stream up to an offset, which new readers start from instead of the beginning. It removes nothing from the stream.
PUT {stream}/snapshot/{snapshot_offset} · Protocol: extensions 2.2, extensions 2.1, extensions 8
Credential: A key or token that reaches the stream and is not read-only.
Request
| Parameter | Value | Required | What it does |
|---|---|---|---|
snapshot_offset path | offset | Required | The offset the snapshot summarizes the stream up to, excluded, URL-encoded: a Stream-Next-Offset an answer gave. On a JSON stream it must fall between two messages, which a Stream-Next-Offset always does, else 400 with NOT_JSON_BOUNDARY. Past the end of the stream answers 409, before the retained offset 410. (extensions 2.2) |
Content-Type header | media type | Optional; application/octet-stream when omitted | The snapshot's own content type, which need not be the stream's. Reads of the snapshot answer with it. (extensions 2.2) |
Answer
| Field | Value | When | What it means |
|---|---|---|---|
Stream-Snapshot-Offset header | offset | On 204 and 409 | On a 204, the published snapshot's offset. On a 409, the latest snapshot's, which the request did not replace. (extensions 2.2) |
Stream-Snapshot-Digest header | 64 lowercase hexadecimal digits | On 204 and 409 | The snapshot's digest: the BLAKE3 hash of its content type and body. A publish sent again answers the same digest. On a 409, the latest snapshot's. (extensions 2.2) |
Statuses
| Status | What it means |
|---|---|
204 | Published, or the same snapshot published again. It replaces the stream's earlier snapshot at once. |
400 | The offset is malformed or, on a JSON stream, not between two messages. |
409 | Another snapshot is the latest: a different body at the same offset, or the latest is at a later offset; the headers name it. A 409 that names no snapshot refuses an offset past the end of the stream. |
410 | The offset is before the stream's retained offset. |
503 | JSON_BOUNDARY_UNVERIFIED: the boundary could not be checked just now. Nothing was published: send it again after Retry-After. |
| 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 |
402 PAYMENT_REQUIRED | The account’s balance is zero. Appends pause; reads and tails continue. | After a top-up |
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 |
409 | Another snapshot body at the same offset, or a snapshot at an offset before the latest one. | No |
413 | The body is over the request body limit. | No |
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: Safe while the snapshot is the latest: the same offset, content type and body answer 204 with the same digest.
Example
curl -sS -i -X PUT "$STREAM/snapshot/$OFFSET" \
-H "Authorization: Bearer $TONBO_API_KEY" \
-H "Content-Type: application/json" \
-d '{"messages":3}'Stage a part of a snapshot
Uploads one part of a snapshot too large for one request; a commit publishes the parts together.
POST {stream}/snapshot/{snapshot_offset}/parts · Protocol: extensions 2.2
Credential: As for a publish.
Request
| Parameter | Value | Required | What it does |
|---|---|---|---|
snapshot_offset path | offset | Required | Checked as an offset and otherwise ignored: a part belongs to the stream, so a commit at another offset may use it. (extensions 2.2) |
Content-Length header | integer | Recommended | The part's size, at most Ursula-Max-Request-Bytes, 8 MiB today. (extensions 2.2) |
Answer
| Field | Value | When | What it means |
|---|---|---|---|
Stream-Snapshot-Part header | 1 to 64 visible ASCII characters | On 201 | The part's id, which the commit lists. Staging changes nothing a reader sees. (extensions 2.2) |
Ursula-Max-Staged-Parts header | integer | On 429 STAGED_PARTS_LIMIT | The stream already holds this many staged parts no commit used, 8,192: commit or discard them before staging more. Waiting does not help, so the answer has no Retry-After. (extensions 2.2) |
Ursula-Max-Staged-Bytes header | integer | On 429 STAGED_PARTS_LIMIT | The same for their bytes, 2 GiB. (extensions 2.2) |
Statuses
| Status | What it means |
|---|---|
201 | Staged. Sending it again stages another part, so keep the ids of the parts you use. |
429 | STAGED_PARTS_LIMIT, with the bound in its header: commit or discard parts first. A 429 with Retry-After is the account's rate instead (Rate limited). |
| 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 |
402 PAYMENT_REQUIRED | The account’s balance is zero. Appends pause; reads and tails continue. | After a top-up |
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 |
413 | The body is over the request body limit. | No |
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: Sending it again stages another part, which only costs space until a commit or a discard; unused parts are discarded after a day.
Example
curl -sS -i -X POST "$STREAM/snapshot/$OFFSET/parts" \
-H "Authorization: Bearer $TONBO_API_KEY" \
-H "Content-Type: application/octet-stream" \
-d '{"messages":3}'Each answer's Stream-Snapshot-Part is a part id; keep them in order for the commit.
Publish a snapshot from its parts
Publishes the snapshot made of staged parts, in the order the body lists them, as a publish of the whole would.
PUT {stream}/snapshot/{snapshot_offset} with Stream-Snapshot-Parts · Protocol: extensions 2.2, extensions 10
Credential: As for a publish.
Request
| Parameter | Value | Required | What it does |
|---|---|---|---|
Stream-Snapshot-Parts header | integer, 1 to 4096 | Required | How many part ids the body lists, each once. The body is the ids, each followed by one newline and nothing else. (extensions 2.2) |
Stream-Snapshot-Digest header | 64 lowercase hexadecimal digits | Required | The digest of the whole snapshot, which the stream computes over the assembled parts: a mismatch answers 400 with SNAPSHOT_DIGEST_MISMATCH and publishes nothing. (extensions 2.2) |
Content-Type header | media type | Optional; application/octet-stream when omitted | The snapshot's content type, as for a publish. (extensions 2.2) |
Answer
| Field | Value | When | What it means |
|---|---|---|---|
Ursula-Max-Snapshot-Bytes header | integer | On 413 SNAPSHOT_TOO_LARGE | The assembled snapshot is larger than the stream accepts; the header names the limit, which HEAD reports too. (extensions 10) |
Retry-After header | seconds | On 503 | A commit of this stream is still running, SNAPSHOT_COMMIT_IN_PROGRESS, or too many run at once: send the same commit again after it. (extensions 2.2) |
Statuses
| Status | What it means |
|---|---|
204 | Published, with the headers of a publish. |
400 | A digest that does not match, a part that is unknown or staged for another stream, or a body that does not list exactly Stream-Snapshot-Parts distinct ids. |
409 | As for a publish: another snapshot is the latest. It comes before the retained-offset check, so a superseded commit answers 409 even below retention. |
413 | SNAPSHOT_TOO_LARGE, with Ursula-Max-Snapshot-Bytes. |
503 | The commit is running or has to wait: send it again after Retry-After; it answers from the first once that finished. |
| 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 |
402 PAYMENT_REQUIRED | The account’s balance is zero. Appends pause; reads and tails continue. | After a top-up |
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 |
409 | Another snapshot body at the same offset, or a snapshot at an offset before the latest one. | No |
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: Safe: a commit sent again answers as a publish sent again does, and the parts are kept until its answer is final.
Example
curl -sS -i -X PUT "$STREAM/snapshot/$OFFSET" \
-H "Authorization: Bearer $TONBO_API_KEY" \
-H "Content-Type: application/json" \
-H "Stream-Snapshot-Parts: 2" \
-H "Stream-Snapshot-Digest: $DIGEST" \
-d '$PART_1
$PART_2
'DIGEST is the digest of the whole snapshot, and PART_1 and PART_2 the ids the stage answers named.
Discard staged parts
Discards every staged part of the stream, so an abandoned upload stops counting toward its bounds. It deletes neither the stream nor a published snapshot.
DELETE {stream}/snapshot/parts · Protocol: extensions 2.2
Credential: As for a publish.
Answer
| Field | Value | When | What it means |
|---|---|---|---|
Stream-Incarnation header | token | On 204 | The stream whose parts were discarded (Stream-Incarnation). (extensions 2.2) |
Statuses
| Status | What it means |
|---|---|
204 | Discarded. |
| 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: Safe. While a commit of the stream runs it may answer 503 with Retry-After: send it again after it.
Example
curl -sS -i -X DELETE "$STREAM/snapshot/parts" \
-H "Authorization: Bearer $TONBO_API_KEY"Read a snapshot
Reads the latest snapshot, or a byte range of it.
GET {stream}/snapshot/{snapshot_offset} · Protocol: extensions 2.5
Credential: As for a read.
Request
| Parameter | Value | Required | What it does |
|---|---|---|---|
snapshot_offset path | offset | Required | The latest snapshot's offset, which HEAD names in Stream-Snapshot-Offset. Any other answers 404: an earlier snapshot is gone once replaced. (extensions 2.5) |
Range header | bytes=first-last, bytes=first- or bytes=-length | Optional | One byte range of the snapshot, answered 206. A range that starts at or past its end answers 416. Several ranges, a range that does not parse, or If-Range, answer the whole snapshot. (extensions 2.5) |
If-Range header | entity tag | Ignored | With it the answer is the whole snapshot, 200, whatever Range asks. (extensions 2.5) |
If-None-Match header | entity tag | Optional | The snapshot's ETag: 304 with the same headers, ignoring any range. (extensions 2.5) |
Answer
| Field | Value | When | What it means |
|---|---|---|---|
Content-Type header | media type | On 200 and 206 | The snapshot's content type, as published. (extensions 2.5) |
Stream-Snapshot-Offset header | offset | On 200, 206 and 304 | The snapshot's offset. (extensions 2.5) |
Stream-Snapshot-Digest header | 64 lowercase hexadecimal digits | On 200, 206 and 304 | The digest of the whole snapshot, never of the range. (extensions 2.5) |
Stream-Next-Offset header | offset | On 200, 206 and 304 | Where the messages after the snapshot start: read on from it. (extensions 2.5) |
Stream-Up-To-Date header | true | When nothing follows the snapshot | The snapshot is at the end of the stream. (extensions 2.5) |
ETag header | entity tag | On 200, 206 and 304 | The snapshot's digest in quotes. (extensions 2.5) |
Accept-Ranges header | bytes | On every snapshot answer | The snapshot can be read in byte ranges. (extensions 2.5) |
Content-Range header | bytes first-last/total or bytes */total | On 206 and 416 | The range returned, and the snapshot's size. (extensions 2.5) |
Statuses
| Status | What it means |
|---|---|
200 | The snapshot. |
206 | The requested range. |
304 | The snapshot is the one the ETag named. |
416 | The range starts at or past the snapshot's end. |
| 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 |
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/snapshot/$OFFSET" \
-H "Authorization: Bearer $TONBO_API_KEY"Advance retention
Stops keeping, and paying for, the stream's history before an offset at or before its latest snapshot.
PUT {stream}/retention/{retained_offset} · Protocol: extensions 2.2
Credential: As for a publish.
Request
| Parameter | Value | Required | What it does |
|---|---|---|---|
retained_offset path | offset | Required | The new start of the stream, URL-encoded: at most the latest snapshot's offset and, on a JSON stream, between two messages; otherwise 400. An offset at or before the current retained offset changes nothing. (extensions 2.2) |
Answer
| Field | Value | When | What it means |
|---|---|---|---|
Stream-Retained-Offset header | offset | On success | The stream's retained offset after the request. From then on a read before it, -1 included, answers 410. (extensions 2.2) |
Statuses
| Status | What it means |
|---|---|
204 | Advanced, or already at or past the offset. |
400 | The offset is past the latest snapshot, or not between two messages of a JSON stream. |
503 | On a JSON stream, the boundary could not be checked just now. Nothing changed: send it again after Retry-After. |
| 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: retention only moves forward.
Example
curl -sS -i -X PUT "$STREAM/retention/$OFFSET" \
-H "Authorization: Bearer $TONBO_API_KEY"Bootstrap
Gives a new reader the stream's current state in one answer: its latest snapshot and the messages after it.
GET {stream}/bootstrap · Protocol: extensions 3.1, extensions 3.2, extensions 3.3, extensions 3.4
Credential: As for a read.
Request
| Parameter | Value | Required | What it does |
|---|---|---|---|
live query | none | Not accepted | Bootstrap never tails, and takes no query parameter; live answers 400. To go live, open a tail at its Stream-Next-Offset. (extensions 3.3) |
Answer
| Field | Value | When | What it means |
|---|---|---|---|
Content-Type header | multipart/mixed; boundary=… | On 200 | A body of parts, each with its own Content-Type: first the latest snapshot, or an empty application/octet-stream part when there is none, then the messages after it, one part per message on a JSON stream and one or more per part on any other. (extensions 3.2) |
Stream-Snapshot-Offset header | offset | On 200 | The snapshot's offset, or -1 without one. (extensions 3.2) |
Stream-Next-Offset header | offset | On 200 | Where the answer ends: read or tail on from it. (extensions 3.2) |
Stream-Up-To-Date header | true | When the answer reached the end | The answer holds the whole stream. Absent, it stopped short, after 8 MiB of messages or where older messages are only in object storage: read on from Stream-Next-Offset before treating the state as current. (extensions 3.2) |
Statuses
| Status | What it means |
|---|---|
200 | The snapshot and the messages after it, complete or partial. |
| 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. A reader that gets 410 from a read bootstraps to rebuild its state, then reads on from the answer's Stream-Next-Offset.
Example
curl -sS -i "$STREAM/bootstrap" \
-H "Authorization: Bearer $TONBO_API_KEY"