> Fetch the index of every Tonbo docs page as Markdown at https://tonbo.dev/llms.txt

# 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](https://tonbo.dev/docs/streams/append.md). The headers every request shares, the error codes and how to tell whether a write applied are on the [overview](https://tonbo.dev/docs/reference/streams.md).

## 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](https://ursula.tonbo.io/docs/specs/durable-stream/#52-append-to-stream), [protocol 5.2.1](https://ursula.tonbo.io/docs/specs/durable-stream/#521-idempotent-producers), [protocol 7.1](https://ursula.tonbo.io/docs/specs/durable-stream/#71-json-mode), [extensions JSON Message Text](https://ursula.tonbo.io/docs/specs/extensions/#json-message-text), [extensions 5.1](https://ursula.tonbo.io/docs/specs/extensions/#51-keys), [extensions 5.2](https://ursula.tonbo.io/docs/specs/extensions/#52-write-identity), [extensions 5.3](https://ursula.tonbo.io/docs/specs/extensions/#53-producer-write-answers), [extensions 5.4](https://ursula.tonbo.io/docs/specs/extensions/#54-epoch-floor), [extensions 5.6](https://ursula.tonbo.io/docs/specs/extensions/#56-client-rules), [extensions 5.8](https://ursula.tonbo.io/docs/specs/extensions/#58-stream-seq-conflicts), [extensions 10](https://ursula.tonbo.io/docs/specs/extensions/#10-server-metadata)

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](https://ursula.tonbo.io/docs/specs/durable-stream/#52-append-to-stream)) |
| `Content-Length` header | integer | Recommended | The body's size, which is at most 8 MiB ([Limits](https://tonbo.dev/docs/streams/limits.md#body)): 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](https://ursula.tonbo.io/docs/specs/extensions/#10-server-metadata)) |
| `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](https://ursula.tonbo.io/docs/specs/durable-stream/#52-append-to-stream)) |
| `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](https://ursula.tonbo.io/docs/specs/durable-stream/#521-idempotent-producers)) |
| `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](https://ursula.tonbo.io/docs/specs/durable-stream/#521-idempotent-producers)) |
| `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](https://ursula.tonbo.io/docs/specs/durable-stream/#521-idempotent-producers)) |
| `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](https://ursula.tonbo.io/docs/specs/extensions/#52-write-identity)) |
| `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](https://ursula.tonbo.io/docs/specs/durable-stream/#52-append-to-stream)) |
| `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](#close). Without producer headers, an append-and-close to a stream already closed answers `409`. ([protocol 5.2](https://ursula.tonbo.io/docs/specs/durable-stream/#52-append-to-stream)) |

### 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](https://ursula.tonbo.io/docs/specs/extensions/#53-producer-write-answers)) |
| `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](https://ursula.tonbo.io/docs/specs/extensions/#53-producer-write-answers)) |
| `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](https://ursula.tonbo.io/docs/specs/durable-stream/#521-idempotent-producers)) |
| `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](https://ursula.tonbo.io/docs/specs/durable-stream/#521-idempotent-producers)) |
| `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](https://ursula.tonbo.io/docs/specs/durable-stream/#521-idempotent-producers)) |
| `Producer-Received-Seq` header | integer | On 409 of a sequence gap | The sequence number the request carried. ([protocol 5.2.1](https://ursula.tonbo.io/docs/specs/durable-stream/#521-idempotent-producers)) |
| `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](https://ursula.tonbo.io/docs/specs/extensions/#53-producer-write-answers)) |
| `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](https://ursula.tonbo.io/docs/specs/extensions/#53-producer-write-answers)) |
| `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](https://ursula.tonbo.io/docs/specs/durable-stream/#52-append-to-stream)) |
| `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](https://ursula.tonbo.io/docs/specs/durable-stream/#52-append-to-stream)) |
| `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](https://ursula.tonbo.io/docs/specs/extensions/#58-stream-seq-conflicts)) |

### 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](https://tonbo.dev/docs/reference/streams.md#errors)). Fix it before sending it again. |

| Refusal | When | Send again |
| --- | --- | --- |
| [`401`](https://tonbo.dev/docs/streams/troubleshooting/stream.md#unauthorized) | 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`](https://tonbo.dev/docs/streams/troubleshooting/stream.md#payment-required) | The account’s balance is zero. Appends pause; reads and tails continue. | After a top-up |
| [`403`](https://tonbo.dev/docs/streams/troubleshooting/stream.md#forbidden) | The key is narrowed and does not reach this stream, or is read-only and the request appends. | No |
| [`403` with `Producer-Outcome`](https://tonbo.dev/docs/streams/troubleshooting/stream.md#producer-replaced) | The append’s producer epoch is older than one the stream has seen: a newer instance of the writer took over. | No |
| [`404`](https://tonbo.dev/docs/streams/troubleshooting/stream.md#not-found) | 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`](https://tonbo.dev/docs/streams/troubleshooting/stream.md#content-type) | The append’s Content-Type is not the stream’s. | With the stream’s content type |
| [`409` with `Stream-Closed: true`](https://tonbo.dev/docs/streams/troubleshooting/stream.md#closed) | The stream is closed, and closing is final. | No |
| [`409` `PRODUCER_SEQ_CONFLICT`](https://tonbo.dev/docs/streams/troubleshooting/stream.md#sequence) | The append’s Producer-Seq skips ahead of the next one the stream expects. | With the expected number |
| [`412`](https://tonbo.dev/docs/streams/troubleshooting/stream.md#incarnation) | The request carried Stream-Incarnation, and the stream was deleted and created again since. | No |
| [`413`](https://tonbo.dev/docs/streams/troubleshooting/stream.md#too-large) | The body is over the request body limit. | No |
| [`429`](https://tonbo.dev/docs/streams/troubleshooting/stream.md#rate-limited) | The account sent more requests per second, or held more tails open at once, than its region allows. | After Retry-After |
| [`503`](https://tonbo.dev/docs/streams/troubleshooting/stream.md#unavailable) | 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](https://tonbo.dev/docs/reference/streams.md#outcomes)). Without them, a repeat may append twice.

### Example

```shell
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](https://ursula.tonbo.io/docs/specs/durable-stream/#53-close-stream), [protocol 5.2.1](https://ursula.tonbo.io/docs/specs/durable-stream/#521-idempotent-producers), [extensions 5.3](https://ursula.tonbo.io/docs/specs/extensions/#53-producer-write-answers)

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](https://ursula.tonbo.io/docs/specs/durable-stream/#53-close-stream)) |
| `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](https://ursula.tonbo.io/docs/specs/durable-stream/#521-idempotent-producers)) |
| `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](https://ursula.tonbo.io/docs/specs/durable-stream/#521-idempotent-producers)) |
| `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](https://ursula.tonbo.io/docs/specs/durable-stream/#521-idempotent-producers)) |

### 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](https://ursula.tonbo.io/docs/specs/extensions/#53-producer-write-answers)) |
| `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](https://ursula.tonbo.io/docs/specs/extensions/#53-producer-write-answers)) |
| `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](https://ursula.tonbo.io/docs/specs/durable-stream/#521-idempotent-producers)) |
| `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](https://ursula.tonbo.io/docs/specs/durable-stream/#521-idempotent-producers)) |
| `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](https://ursula.tonbo.io/docs/specs/durable-stream/#521-idempotent-producers)) |
| `Producer-Received-Seq` header | integer | On 409 of a sequence gap | The sequence number the request carried. ([protocol 5.2.1](https://ursula.tonbo.io/docs/specs/durable-stream/#521-idempotent-producers)) |
| `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](https://ursula.tonbo.io/docs/specs/extensions/#53-producer-write-answers)) |
| `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](https://ursula.tonbo.io/docs/specs/extensions/#53-producer-write-answers)) |
| `Stream-Next-Offset` header | offset | On 204 | The stream's final offset, unchanged by the close. ([protocol 5.3](https://ursula.tonbo.io/docs/specs/durable-stream/#53-close-stream)) |
| `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](https://ursula.tonbo.io/docs/specs/durable-stream/#53-close-stream)) |

### Statuses

| Status | What it means |
| --- | --- |
| `204` | Closed, or already closed: a close sent again answers the same. |

| Refusal | When | Send again |
| --- | --- | --- |
| [`401`](https://tonbo.dev/docs/streams/troubleshooting/stream.md#unauthorized) | 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`](https://tonbo.dev/docs/streams/troubleshooting/stream.md#forbidden) | The key is narrowed and does not reach this stream, or is read-only and the request appends. | No |
| [`404`](https://tonbo.dev/docs/streams/troubleshooting/stream.md#not-found) | 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`](https://tonbo.dev/docs/streams/troubleshooting/stream.md#incarnation) | The request carried Stream-Incarnation, and the stream was deleted and created again since. | No |
| [`429`](https://tonbo.dev/docs/streams/troubleshooting/stream.md#rate-limited) | The account sent more requests per second, or held more tails open at once, than its region allows. | After Retry-After |
| [`503`](https://tonbo.dev/docs/streams/troubleshooting/stream.md#unavailable) | 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

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