Snapshots and bootstrap

Last updated 2 November 2026

View .md

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

ParameterValueRequiredWhat it does
snapshot_offset pathoffsetRequiredThe 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 headermedia typeOptional; application/octet-stream when omittedThe snapshot's own content type, which need not be the stream's. Reads of the snapshot answer with it. (extensions 2.2)

Answer

FieldValueWhenWhat it means
Stream-Snapshot-Offset headeroffsetOn 204 and 409On 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 header64 lowercase hexadecimal digitsOn 204 and 409The 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

StatusWhat it means
204Published, or the same snapshot published again. It replaces the stream's earlier snapshot at once.
400The offset is malformed or, on a JSON stream, not between two messages.
409Another 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.
410The offset is before the stream's retained offset.
503JSON_BOUNDARY_UNVERIFIED: the boundary could not be checked just now. Nothing was published: send it again after Retry-After.
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
402 PAYMENT_REQUIREDThe account’s balance is zero. Appends pause; reads and tails continue.After a top-up
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
409Another snapshot body at the same offset, or a snapshot at an offset before the latest one.No
413The body is over the request body limit.No
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: 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

ParameterValueRequiredWhat it does
snapshot_offset pathoffsetRequiredChecked 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 headerintegerRecommendedThe part's size, at most Ursula-Max-Request-Bytes, 8 MiB today. (extensions 2.2)

Answer

FieldValueWhenWhat it means
Stream-Snapshot-Part header1 to 64 visible ASCII charactersOn 201The part's id, which the commit lists. Staging changes nothing a reader sees. (extensions 2.2)
Ursula-Max-Staged-Parts headerintegerOn 429 STAGED_PARTS_LIMITThe 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 headerintegerOn 429 STAGED_PARTS_LIMITThe same for their bytes, 2 GiB. (extensions 2.2)

Statuses

StatusWhat it means
201Staged. Sending it again stages another part, so keep the ids of the parts you use.
429STAGED_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).
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
402 PAYMENT_REQUIREDThe account’s balance is zero. Appends pause; reads and tails continue.After a top-up
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
413The body is over the request body limit.No
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: 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

ParameterValueRequiredWhat it does
Stream-Snapshot-Parts headerinteger, 1 to 4096RequiredHow 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 header64 lowercase hexadecimal digitsRequiredThe 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 headermedia typeOptional; application/octet-stream when omittedThe snapshot's content type, as for a publish. (extensions 2.2)

Answer

FieldValueWhenWhat it means
Ursula-Max-Snapshot-Bytes headerintegerOn 413 SNAPSHOT_TOO_LARGEThe assembled snapshot is larger than the stream accepts; the header names the limit, which HEAD reports too. (extensions 10)
Retry-After headersecondsOn 503A 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

StatusWhat it means
204Published, with the headers of a publish.
400A 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.
409As for a publish: another snapshot is the latest. It comes before the retained-offset check, so a superseded commit answers 409 even below retention.
413SNAPSHOT_TOO_LARGE, with Ursula-Max-Snapshot-Bytes.
503The commit is running or has to wait: send it again after Retry-After; it answers from the first once that finished.
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
402 PAYMENT_REQUIREDThe account’s balance is zero. Appends pause; reads and tails continue.After a top-up
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
409Another snapshot body at the same offset, or a snapshot at an offset before the latest one.No
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: 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

FieldValueWhenWhat it means
Stream-Incarnation headertokenOn 204The stream whose parts were discarded (Stream-Incarnation). (extensions 2.2)

Statuses

StatusWhat it means
204Discarded.
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: 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

ParameterValueRequiredWhat it does
snapshot_offset pathoffsetRequiredThe 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 headerbytes=first-last, bytes=first- or bytes=-lengthOptionalOne 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 headerentity tagIgnoredWith it the answer is the whole snapshot, 200, whatever Range asks. (extensions 2.5)
If-None-Match headerentity tagOptionalThe snapshot's ETag: 304 with the same headers, ignoring any range. (extensions 2.5)

Answer

FieldValueWhenWhat it means
Content-Type headermedia typeOn 200 and 206The snapshot's content type, as published. (extensions 2.5)
Stream-Snapshot-Offset headeroffsetOn 200, 206 and 304The snapshot's offset. (extensions 2.5)
Stream-Snapshot-Digest header64 lowercase hexadecimal digitsOn 200, 206 and 304The digest of the whole snapshot, never of the range. (extensions 2.5)
Stream-Next-Offset headeroffsetOn 200, 206 and 304Where the messages after the snapshot start: read on from it. (extensions 2.5)
Stream-Up-To-Date headertrueWhen nothing follows the snapshotThe snapshot is at the end of the stream. (extensions 2.5)
ETag headerentity tagOn 200, 206 and 304The snapshot's digest in quotes. (extensions 2.5)
Accept-Ranges headerbytesOn every snapshot answerThe snapshot can be read in byte ranges. (extensions 2.5)
Content-Range headerbytes first-last/total or bytes */totalOn 206 and 416The range returned, and the snapshot's size. (extensions 2.5)

Statuses

StatusWhat it means
200The snapshot.
206The requested range.
304The snapshot is the one the ETag named.
416The range starts at or past the snapshot's end.
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
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/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

ParameterValueRequiredWhat it does
retained_offset pathoffsetRequiredThe 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

FieldValueWhenWhat it means
Stream-Retained-Offset headeroffsetOn successThe stream's retained offset after the request. From then on a read before it, -1 included, answers 410. (extensions 2.2)

Statuses

StatusWhat it means
204Advanced, or already at or past the offset.
400The offset is past the latest snapshot, or not between two messages of a JSON stream.
503On a JSON stream, the boundary could not be checked just now. Nothing changed: send it again after Retry-After.
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: 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

ParameterValueRequiredWhat it does
live querynoneNot acceptedBootstrap 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

FieldValueWhenWhat it means
Content-Type headermultipart/mixed; boundary=…On 200A 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 headeroffsetOn 200The snapshot's offset, or -1 without one. (extensions 3.2)
Stream-Next-Offset headeroffsetOn 200Where the answer ends: read or tail on from it. (extensions 3.2)
Stream-Up-To-Date headertrueWhen the answer reached the endThe 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

StatusWhat it means
200The snapshot and the messages after it, complete or partial.
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. 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"