Append and close
Last updated 2 November 2026
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
| Parameter | Value | Required | What it does |
|---|---|---|---|
Content-Type header | media type | Required with a body | Exactly 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 header | integer | Recommended | The 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 header | chunked | Optional | A 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 header | string | Optional; all three or none | Names 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 header | integer, 0 to 2^53 − 1 | With Producer-Id | The 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 header | integer, 0 to 2^53 − 1 | With Producer-Id | The 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 header | 64 lowercase hexadecimal digits | Optional, with the producer headers | The 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 header | string | Optional | A 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 header | true | Optional | Closes 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
| Field | Value | When | What it means |
|---|---|---|---|
Stream-Offset header | offset | On 200 and 204 with producer headers | Where 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 header | token | On 403 of a producer write, and on some 204 | Why 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 header | integer | On 200, 204 and every producer 403 | On 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 header | integer | On 200 and 204 | The 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 header | integer | On 409 and 400 of a sequence gap | The sequence number the stream expects next: send the unacknowledged writes again from it, in order. (protocol 5.2.1) |
Producer-Received-Seq header | integer | On 409 of a sequence gap | The sequence number the request carried. (protocol 5.2.1) |
Producer-Closed-Epoch header | integer | On a claim that closed an epoch, and on epoch-closed | The epoch that a newer claim, or a rewrite of the stream's history, closed. (extensions 5.3) |
Producer-Closed-Seq header | integer | With Producer-Closed-Epoch | Every write of the closed epoch up to this number applied, and none above it ever will. (extensions 5.3) |
Stream-Next-Offset header | offset | On every success, and on 409 of a closed stream | Where 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 header | true | When the stream is closed | This 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 header | string | On 409 STREAM_SEQ_CONFLICT | The stream's last accepted Stream-Seq, as it was sent: read on from your last offset, then retry after it. (extensions 5.8) |
Statuses
| Status | What it means |
|---|---|
200 | Applied: a new write with producer headers, with its receipt (Stream-Offset, Stream-Next-Offset). |
204 | Without 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. |
400 | The 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. |
| 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 |
403 with Producer-Outcome | The append’s producer epoch is older than one the stream has seen: a newer instance of the writer took over. | 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 CONTENT_TYPE_MISMATCH | The append’s Content-Type is not the stream’s. | With the stream’s content type |
409 with Stream-Closed: true | The stream is closed, and closing is final. | No |
409 PRODUCER_SEQ_CONFLICT | The append’s Producer-Seq skips ahead of the next one the stream expects. | With the expected number |
412 | The request carried Stream-Incarnation, and the stream was deleted and created again since. | No |
413 | The body is over the request body limit. | 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: 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
| Parameter | Value | Required | What it does |
|---|---|---|---|
Stream-Closed header | true | Required | With an empty body, closes the stream. Content-Type may be left out; it is ignored. (protocol 5.3) |
Producer-Id header | string | Optional, with the other two | The 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 header | integer, 0 to 2^53 − 1 | Optional, with the other two | The writer's current epoch, as for an append. (protocol 5.2.1) |
Producer-Seq header | integer, 0 to 2^53 − 1 | Optional, with the other two | The writer's next sequence number, as for an append. (protocol 5.2.1) |
Answer
| Field | Value | When | What it means |
|---|---|---|---|
Stream-Offset header | offset | On 200 and 204 with producer headers | Where the close's own bytes start: the end of the stream, since a close appends nothing. (extensions 5.3) |
Producer-Outcome header | token | On 403 of a producer write, and on some 204 | Why 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 header | integer | On 200, 204 and every producer 403 | On 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 header | integer | On 200 and 204 | The 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 header | integer | On 409 and 400 of a sequence gap | The sequence number the stream expects next: send the unacknowledged writes again from it, in order. (protocol 5.2.1) |
Producer-Received-Seq header | integer | On 409 of a sequence gap | The sequence number the request carried. (protocol 5.2.1) |
Producer-Closed-Epoch header | integer | On a claim that closed an epoch, and on epoch-closed | The epoch that a newer claim, or a rewrite of the stream's history, closed. (extensions 5.3) |
Producer-Closed-Seq header | integer | With Producer-Closed-Epoch | Every write of the closed epoch up to this number applied, and none above it ever will. (extensions 5.3) |
Stream-Next-Offset header | offset | On 204 | The stream's final offset, unchanged by the close. (protocol 5.3) |
Stream-Closed header | true | On 204 | The stream is closed. Closing cannot be undone: the stream stays readable until it is deleted. (protocol 5.3) |
Statuses
| Status | What it means |
|---|---|
204 | Closed, or already closed: a close sent again answers the same. |
| 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 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"