Append and close

Last updated 2 November 2026

View .md

Every header, producer answer and status of a request that appends to a stream or closes it.

An append is a POST to the stream’s URL. Its body is the message, and its producer headers make it safe to send again; the guide is Append. The headers every request shares, the error codes and how to tell whether a write applied are on the overview.

Append

Appends the body to the end of the stream: one message, or on a JSON stream one message per element of a top-level array.

POST {stream} · Protocol: protocol 5.2, protocol 5.2.1, protocol 7.1, extensions JSON Message Text, extensions 5.1, extensions 5.2, extensions 5.3, extensions 5.4, extensions 5.6, extensions 5.8, extensions 10

Credential: A key or token that reaches the stream and is not read-only. A public stream still takes one to append.

Request

ParameterValueRequiredWhat it does
Content-Type headermedia typeRequired with a bodyExactly the stream's content type, as tonbo stream show prints it and HEAD reports it. Any other answers 409 with the code CONTENT_TYPE_MISMATCH and appends nothing. On a JSON stream (application/json), the body must be valid JSON nested at most 127 deep, else 400; a top-level array appends one message per element, and an empty array answers 400. (protocol 5.2)
Content-Length headerintegerRecommendedThe body's size, which is at most 8 MiB (Limits): a larger one answers 413 with Ursula-Max-Request-Bytes before it is sent. A body sent without it is refused only once it crosses the limit, and the client may then see the connection reset instead. (extensions 10)
Transfer-Encoding headerchunkedOptionalA body sent as it is produced. It is appended as one write once it ends, under the same limit as any other body. (protocol 5.2)
Producer-Id headerstringOptional; all three or noneNames the writer, such as a host or a job, and is kept per stream. Send it with Producer-Epoch and Producer-Seq, or none of the three: some of them answer 400, and an empty value too. With them, the append is keyed by the three and applies once however often it is sent. A stream keeps up to 4,096 producers, and forgets one that has not appended for 7 days. (protocol 5.2.1)
Producer-Epoch headerinteger, 0 to 2^53 − 1With Producer-IdThe writer's session: start at 0, and add one when the writer restarts without knowing its last sequence number. It never goes below the last epoch the stream saw from this producer: an older one answers 403 with Producer-Outcome and applies nothing. A higher epoch starts its sequence at 0 again; at any other sequence number it answers 400 with Producer-Expected-Seq: 0. With Stream-Incarnation, an epoch at or below one the stream has forgotten answers 403 with Producer-Outcome: producer-expired. (protocol 5.2.1)
Producer-Seq headerinteger, 0 to 2^53 − 1With Producer-IdThe request's number within the epoch: 0 first, then one more per request, whatever the number of messages in it. The next number applies (200). A number at or below the last applied one is a repeat: 204, nothing appended. A number that skips ahead answers 409 with Producer-Expected-Seq; send the writes from that number again, in order, with their own numbers. (protocol 5.2.1)
Producer-Digest header64 lowercase hexadecimal digitsOptional, with the producer headersThe BLAKE3-256 hash of the body, which makes the body part of the write: the same key sent later with another digest answers 403 with Producer-Outcome: key-conflict and applies nothing, and a digest that does not match the body answers 400 with the code PRODUCER_DIGEST_MISMATCH. Without it, two different bodies under one key cannot be told apart. (extensions 5.2)
Stream-Seq headerstringOptionalA compare-and-set: the append applies only when this value sorts after the last Stream-Seq the stream accepted, byte by byte, and otherwise answers 409 with the code STREAM_SEQ_CONFLICT and Stream-Last-Seq. It is kept per stream, separate from producer sequence numbers. Pad numbers to one width, since 10 sorts before 9. (protocol 5.2)
Stream-Closed headertrueOptionalCloses the stream with this append as its last message, in one step. With an empty body it is a close. Without producer headers, an append-and-close to a stream already closed answers 409. (protocol 5.2)

Answer

FieldValueWhenWhat it means
Stream-Offset headeroffsetOn 200 and 204 with producer headersWhere this write's bytes start. With Stream-Next-Offset it is the write's receipt, the same on a repeat of the newest write. (extensions 5.3)
Producer-Outcome headertokenOn 403 of a producer write, and on some 204Why a producer write was refused, or how a repeat applied. epoch-closed;by=claim: a newer epoch of this writer took over, and Producer-Closed-Seq says which of this epoch's writes applied. epoch-closed;by=history: the stream's history was rewritten, as by a restore, since the epoch began: claim a higher epoch and send again what came after Producer-Closed-Seq. stale-epoch: an older epoch, of which nothing more can be said. key-conflict: the key holds another write. producer-expired: the stream forgot the epoch. On a 204, receipt-evicted: the write applied, but the stream no longer keeps its range. A 403 without it comes from access control, not from the producer rules. (extensions 5.3)
Producer-Epoch headerintegerOn 200, 204 and every producer 403On success the request's epoch. On a 403, the producer's current epoch, or the stream's forgotten-epoch floor with producer-expired: a restarted writer claims a higher one. (protocol 5.2.1)
Producer-Seq headerintegerOn 200 and 204The highest sequence number applied in this epoch, which may be above the request's own when several requests are in flight. (protocol 5.2.1)
Producer-Expected-Seq headerintegerOn 409 and 400 of a sequence gapThe sequence number the stream expects next: send the unacknowledged writes again from it, in order. (protocol 5.2.1)
Producer-Received-Seq headerintegerOn 409 of a sequence gapThe sequence number the request carried. (protocol 5.2.1)
Producer-Closed-Epoch headerintegerOn a claim that closed an epoch, and on epoch-closedThe epoch that a newer claim, or a rewrite of the stream's history, closed. (extensions 5.3)
Producer-Closed-Seq headerintegerWith Producer-Closed-EpochEvery write of the closed epoch up to this number applied, and none above it ever will. (extensions 5.3)
Stream-Next-Offset headeroffsetOn every success, and on 409 of a closed streamWhere the appended bytes end: the offset a reader reads from next to see what follows. On a 409 of a closed stream, the stream's final offset. (protocol 5.2)
Stream-Closed headertrueWhen the stream is closedThis append closed the stream, or it was already closed. On a 409, the stream is closed and the append applied nothing. (protocol 5.2)
Stream-Last-Seq headerstringOn 409 STREAM_SEQ_CONFLICTThe stream's last accepted Stream-Seq, as it was sent: read on from your last offset, then retry after it. (extensions 5.8)

Statuses

StatusWhat it means
200Applied: a new write with producer headers, with its receipt (Stream-Offset, Stream-Next-Offset).
204Without producer headers: applied, with Stream-Next-Offset. With them: a repeat of a write that already applied, with the same receipt, or with Producer-Outcome: receipt-evicted and no range. Either way the write is in the stream once.
400The request is malformed and applied nothing: some but not all producer headers, a new epoch above sequence 0, an empty body without Stream-Closed, invalid JSON, an empty JSON array, or a Producer-Digest that does not match. The error code names which (Error codes). Fix it before sending it again.
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
403 with Producer-OutcomeThe append’s producer epoch is older than one the stream has seen: a newer instance of the writer took over.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
409 CONTENT_TYPE_MISMATCHThe append’s Content-Type is not the stream’s.With the stream’s content type
409 with Stream-Closed: trueThe stream is closed, and closing is final.No
409 PRODUCER_SEQ_CONFLICTThe append’s Producer-Seq skips ahead of the next one the stream expects.With the expected number
412The request carried Stream-Incarnation, and the stream was deleted and created again since.No
413The body is over the request body limit.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: With producer headers an append is keyed by the three, so when its answer is lost, or is a 408 or a 5xx whose code proves nothing, send the same request again unchanged, headers and body: it applies once (Write outcomes). Without them, a repeat may append twice.

Example

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}'

Close

Closes the stream without appending: every later append is refused, and readers at the end learn that nothing more will come.

POST {stream} with Stream-Closed: true · Protocol: protocol 5.3, protocol 5.2.1, extensions 5.3

Credential: As for an append.

Request

ParameterValueRequiredWhat it does
Stream-Closed headertrueRequiredWith an empty body, closes the stream. Content-Type may be left out; it is ignored. (protocol 5.3)
Producer-Id headerstringOptional, with the other twoThe writer that closes, as for an append: with the three producer headers the close is keyed like an append, at the writer's next Producer-Seq, so sending it again is safe and a repeat answers 204 with Stream-Closed: true. (protocol 5.2.1)
Producer-Epoch headerinteger, 0 to 2^53 − 1Optional, with the other twoThe writer's current epoch, as for an append. (protocol 5.2.1)
Producer-Seq headerinteger, 0 to 2^53 − 1Optional, with the other twoThe writer's next sequence number, as for an append. (protocol 5.2.1)

Answer

FieldValueWhenWhat it means
Stream-Offset headeroffsetOn 200 and 204 with producer headersWhere the close's own bytes start: the end of the stream, since a close appends nothing. (extensions 5.3)
Producer-Outcome headertokenOn 403 of a producer write, and on some 204Why a producer write was refused, or how a repeat applied. epoch-closed;by=claim: a newer epoch of this writer took over, and Producer-Closed-Seq says which of this epoch's writes applied. epoch-closed;by=history: the stream's history was rewritten, as by a restore, since the epoch began: claim a higher epoch and send again what came after Producer-Closed-Seq. stale-epoch: an older epoch, of which nothing more can be said. key-conflict: the key holds another write. producer-expired: the stream forgot the epoch. On a 204, receipt-evicted: the write applied, but the stream no longer keeps its range. A 403 without it comes from access control, not from the producer rules. (extensions 5.3)
Producer-Epoch headerintegerOn 200, 204 and every producer 403On success the request's epoch. On a 403, the producer's current epoch, or the stream's forgotten-epoch floor with producer-expired: a restarted writer claims a higher one. (protocol 5.2.1)
Producer-Seq headerintegerOn 200 and 204The highest sequence number applied in this epoch, which may be above the request's own when several requests are in flight. (protocol 5.2.1)
Producer-Expected-Seq headerintegerOn 409 and 400 of a sequence gapThe sequence number the stream expects next: send the unacknowledged writes again from it, in order. (protocol 5.2.1)
Producer-Received-Seq headerintegerOn 409 of a sequence gapThe sequence number the request carried. (protocol 5.2.1)
Producer-Closed-Epoch headerintegerOn a claim that closed an epoch, and on epoch-closedThe epoch that a newer claim, or a rewrite of the stream's history, closed. (extensions 5.3)
Producer-Closed-Seq headerintegerWith Producer-Closed-EpochEvery write of the closed epoch up to this number applied, and none above it ever will. (extensions 5.3)
Stream-Next-Offset headeroffsetOn 204The stream's final offset, unchanged by the close. (protocol 5.3)
Stream-Closed headertrueOn 204The stream is closed. Closing cannot be undone: the stream stays readable until it is deleted. (protocol 5.3)

Statuses

StatusWhat it means
204Closed, or already closed: a close sent again answers the same.
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 close of a closed stream answers 204 again.

Example

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