> ## Documentation Index
> Fetch the complete documentation index at: https://docs.coinversa.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Wire protocol

> Every frame on the Coinversa websocket — subscribe and unsubscribe acks, multi-wallet `users` lists and how they hash, ping, data frames carrying a monotonic source position as `seq`, the order-book snapshot, update and error frames, and the exact error strings the server sends.

Every frame is a single JSON object. Client frames carry a `method`; server frames carry a `channel`. The error strings on this page are **exact** — match them verbatim; they will not change.

## Subscribe

The acknowledgement echoes your subscription back and adds a 16-character hex `id` identifying that subscription on this connection.

```json theme={null}
→ {"method":"subscribe","subscription":{"type":"tradesByCoin","coin":"BTC"}}

← {"channel":"subscriptionResponse","data":{"method":"subscribe","subscription":{"coin":"BTC","type":"tradesByCoin"},"id":"bc0de255be1a5311"}}
```

The `id` is stable: the same type and arguments always hash to the same `id`. It is the first 16 hex characters of a SHA-256 over the type and its arguments, not a full digest. On the fill feeds argument values are matched **case-insensitively** and case-folded before hashing, so `BTC` and `btc` are one subscription. `l4Book`'s `coin` is one exception — it is **case-sensitive**, hashed as sent, and must be the node's own spelling (`BTC`, `xyz:GOLD`, `#28200`); see [Order book (L4)](/websocket/order-book#subscribing). `candle`'s and `allCandles`' `interval` is the other: it is hashed as sent, so `1m` and `1M` are two different subscriptions carrying two different intervals — one minute and 30 days. A candle's `coin` is **not** an exception; it folds like any other market name, and the frame's `s` echoes the market as the data spells it. `candle` also takes an optional `source`, which defaults to `trade` **before** the id is hashed, so omitting it and sending `"source":"trade"` are one subscription with one `id`, not two.

Arguments are strings or arrays of strings. The five [multi-wallet feeds](/websocket/feeds#multi-wallet-subscriptions) declare `users` as a list of up to 1,000; every other argument accepts at most one value. A `users` list is hashed as a **set** — sorted, case-folded and deduplicated — so `["0xb","0xa"]`, `["0xA","0xb"]` and `["0xa","0xb","0xa"]` all produce one `id` and one subscription. A one-element list hashes identically to the same argument sent as a bare string, which is also how a bare string is accepted: it becomes a one-item list.

```json theme={null}
→ {"method":"subscribe","subscription":{"type":"tradesByUsers","users":["0x9bad…1f07","0x28f0…4c19"]}}

← {"channel":"subscriptionResponse","data":{"method":"subscribe","subscription":{"type":"tradesByUsers","users":["0x9bad…1f07","0x28f0…4c19"]},"id":"<16 hex>"}}
```

The ack echoes your subscription object **verbatim**, so the list comes back in the order you sent it rather than in the normalised order that produced the `id`.

## Unsubscribe

Send the same subscription object back with `"method":"unsubscribe"`. The acknowledgement carries the **same** `id` the subscribe returned, so you can match the teardown to the setup. The slot frees immediately. Because a `users` list is a set, an unsubscribe listing the same wallets in any order releases the subscription.

```json theme={null}
→ {"method":"unsubscribe","subscription":{"type":"tradesByCoin","coin":"BTC"}}

← {"channel":"subscriptionResponse","data":{"method":"unsubscribe","subscription":{"coin":"BTC","type":"tradesByCoin"},"id":"bc0de255be1a5311"}}
```

## Ping

An application-level heartbeat, independent of the protocol-level ping frames your client library may already send. It is the only liveness probe a browser has.

```json theme={null}
→ {"method":"ping"}

← {"channel":"pong"}
```

## Data frames

`channel` is the subscription type, `seq` is a monotonic source position — the block height on every feed but the `leverageUpdates` and `twapOrders` families and mark-source `candle` — and `data` is an array of `[wallet, fill]` pairs on the fill feeds. The liquidation, leverage, TWAP-order and TWAP-status feeds put their own objects in `data`, `candle` puts a single OHLC object there, and `allCandles` an array of them; see [feeds and the fill object](/websocket/feeds#data-frames) for every shape. There is no subscription id on a data frame.

## Order book frames

`l4Book` speaks two frames of its own, both on channel `l4Book` and both carrying the `coin` as you sent it. The full contract — field meanings, the `data` layout, and the client rule — is on [Order book (L4)](/websocket/order-book).

**Snapshot** — exactly one, first, with `seq` equal to its `height`; `data` is the node's export of the whole book:

```json theme={null}
← {"channel":"l4Book","coin":"BTC","snapshot":true,"height":100,"time_ms":1757815848097,"seq":100,"data":{"book_orders":[[…],[…]],"untriggered_orders":[…]}}
```

**Update** — one per block after the snapshot, whether or not the block touched the coin; `height` and `prev` chain the blocks, and `seq` equals `height` on every one:

```json theme={null}
← {"channel":"l4Book","coin":"BTC","seq":103,"height":103,"prev":102,"time_ms":1757815848325,"partial":false,"data":{"raw_book_diffs":[…],"order_statuses":[…]}}
```

`l4Book` has no error frame of its own. A book subscription that is dropped is reported on the ordinary `error` channel, **without the coin on it** — see [when a book subscription is dropped](/websocket/order-book#when-a-book-subscription-is-dropped):

```json theme={null}
← {"channel":"error","data":"unknown coin"}
```

## TWAP status frames

`twapStatuses` and `twapStatusesByUser` speak two frames. The full contract — the event shape, the `status` union and the client rule for keeping the set — is on [TWAP statuses](/websocket/feeds#twap-statuses).

**Snapshot** — at most one, first, carrying `"snapshot": true` and `data` as an **object**. `seq` is the block height the gateway built it at, and `channel` is the type you subscribed to, so a by-user snapshot says `twapStatusesByUser`:

```json theme={null}
← {"channel":"twapStatuses","snapshot":true,"seq":1156436426,"data":{"complete":false,"coverage_from_ms":1789940000000,"twaps":[{"time":"2026-09-22T02:00:02.321152806","twap_id":2241180,"state":{…},"status":"activated"}]}}
```

Each element of `twaps` is the last status event for one live `twap_id`, identical in shape to an element of a live frame's `data`. `complete` says whether the gateway's coverage reaches back far enough to hold every activated TWAP; `coverage_from_ms` is the oldest block time it has applied. A `false` here is not an error — see [the snapshot on subscribe](/websocket/feeds#the-snapshot-on-subscribe).

**Live** — one per block that carried a matching event, with `data` as an **array** of status events:

```json theme={null}
← {"channel":"twapStatuses","seq":1156436431,"data":[{"time":"2026-09-22T02:04:11.887431002","twap_id":2241180,"state":{…},"status":"finished"}]}
```

The subscribe ack is sent **before** the snapshot, and these feeds have no buffer warm-up, so the snapshot follows the ack immediately. Frames buffered above the snapshot's `seq` are released after it, leaving no gap.

If the snapshot fails, the subscription is **removed** after its ack and one `{"channel":"error","data":"<reason>"}` follows; resubscribe. The reasons are [below](#errors-on-an-open-socket).

## Sequence numbers

`seq` is a **monotonic source position** — the place in the source stream the frame was produced from. It does not restart on reconnect, it is not per subscription, and it does not start at 1.

What the number *is* depends on the feed:

| Feeds                                                                                                                                                                                                                                                                                     | `seq` is                                                                                                                                       |
| ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
| `trades`, `tradesByCoin`, `tradesByUser`, `tradesByUsers`, `liquidations`, `liquidationsByUser`, `liquidationsByUsers`, `twapSliceFills`, `twapSliceFillsByUser`, `twapSliceFillsByUsers`, `twapStatuses`, `twapStatusesByUser`, `l4Book`, `candle` and `allCandles` on `source: "trade"` | The **source block height** — the height of the block the frame was produced from. The same number across all of them.                         |
| `leverageUpdates`, `leverageUpdatesByUser`, `leverageUpdatesByUsers`, `twapOrders`, `twapOrdersByUser`, `twapOrdersByUsers`, `candle` and `allCandles` on `source: "mark"`                                                                                                                | The replica block's **Unix timestamp in nanoseconds**, because the replica-command stream does not expose its height outside the JSON payload. |

<Warning>
  **The two halves of the TWAP product are on different scales.** `twapStatuses` and `twapStatusesByUser` carry the **source block height**, taken from the gateway's own block number — while `twapOrders` and its variants carry a **nanosecond** replica timestamp. They describe the same TWAPs and join on `twap_id`, but their `seq` values are about **nine orders of magnitude** apart and belong to different streams. Never hold one cursor across the pair, and never compare their numbers. This is the easiest fact on this page to get backwards.
</Warning>

`candle` and `allCandles` appear in both rows: they are the only feeds whose `seq` depends on an argument. Its default `source: "trade"` is built from fills and carries a block height; `source: "mark"` is built from the replica-command stream and carries a nanosecond timestamp. Two candle subscriptions on one connection can therefore be sequencing on different kinds of number **on the same `candle` channel**, and data frames carry no subscription id to tell them apart — see [candles](/websocket/feeds#candles).

Both only ever increase, and both are valid reconnect cursors for their own feed. What you must not do is compare one against the other, or hold a single "last seq" across a mixture of the two — a nanosecond timestamp is about nine orders of magnitude larger than a block height (a height is around 1.1 × 10⁹, a nanosecond stamp around 1.79 × 10¹⁸), so a shared counter would read every fill frame after a leverage frame as a replay.

<Warning>
  This changed. `seq` used to be a per-subscription counter that started at 1 and incremented by one per frame. A client that checks `seq === last + 1` now reports almost every frame on a filtered feed as a lost message. Check that `seq` **increases**, not that it increases by one.
</Warning>

A filtered feed sends no frame for a block that matched nothing, so `tradesByCoin` on a quiet coin legitimately skips hundreds of numbers between frames. That is the feed working.

What is broken is `seq` going **backwards**: the chain only moves forward, so a value below the last one you applied means frames were replayed or reordered.

<Warning>
  **`candle` is the one feed that legitimately repeats a `seq`.** A `source: "trade"` block that crosses an interval boundary emits **two frames on the same `seq`** — the sealed candle carrying `"closed": true`, then the newly opened one. The server declares this in the catalog (`repeatedSequence`), and it is the only feed that does. A client that treats any repeat as a fault will flag every sealed candle. Treat a repeat as a fault on every other feed; on `candle`, apply both frames in the order they arrive.
</Warning>

Because a data frame carries no subscription id, key any per-stream tracking on what the frame itself identifies: the `channel` plus the coin or wallet of its first fill. Two `tradesByCoin` subscriptions on one connection both arrive on channel `tradesByCoin`, and because `seq` is now a shared source position they will both report the same number for the same block — so tracking on the `channel` alone sees that as a **repeat**, which is exactly the fault this page tells you to act on. Key per subscription, not per channel.

That recipe works on the by-coin and by-user feeds, where every frame on one subscription carries the same coin or the same wallet. It does **not** work on the [multi-wallet feeds](/websocket/feeds#multi-wallet-subscriptions): on a `…ByUsers` subscription the wallet in the first entry changes from frame to frame, so keying on it manufactures a new stream almost every frame and never catches a rewind. Nothing in a frame distinguishes two `tradesByUsers` lists sharing a connection. So **hold at most one subscription of a given list feed per connection** — then the channel *is* the stream key for it and the ordinary rule applies. A second list of the same type belongs on a second connection, within your [connection cap](/websocket/limits).

`seq` is also your **reconnect cursor** — record the last one you applied and pass it back on resubscribe; see [Reconnecting](#reconnecting).

On `l4Book`, `seq` equals `height` on the snapshot and on every update, so the two agree. Gate the book on `prev` all the same: it names the exact predecessor block, which `seq` alone does not — see [seq versus prev](/websocket/order-book#seq-versus-prev).

## Reconnecting

Add `sequence` to the subscription and the server replays what you missed instead of starting you over:

```json theme={null}
→ {"method":"subscribe","subscription":{"type":"tradesByCoin","coin":"BTC","sequence":123456}}
```

The server sends **no snapshot**, replays every retained message with `seq` greater than yours, then continues live.

`sequence` is control data: it steers the subscribe without being hashed into the subscription id, so reconnecting with a cursor resumes the same subscription rather than opening a second one. The same is true of `snapshot`.

Retention is **10,000 messages per feed** per connection manager, and an empty feed group stays warm for **60 seconds**. Past either, the subscription is dropped:

| `data`                                     | What it means                                                                                   |
| ------------------------------------------ | ----------------------------------------------------------------------------------------------- |
| `sequence is not retained`                 | The feed group is holding nothing to replay — usually because it had gone cold and was dropped. |
| `sequence is older than retained history`  | The cursor predates the oldest message still held.                                              |
| `sequence and snapshot cannot be combined` | `snapshot: true` with a `sequence`. Pick one.                                                   |

In every case, subscribe again **without** `sequence` to rebuild from a fresh snapshot.

## Starting without a snapshot

To begin at live updates and skip initial state, pass `"snapshot": false`:

```json theme={null}
→ {"method":"subscribe","subscription":{"type":"l4Book","coin":"BTC","snapshot":false}}
```

Messages that arrive while you are joining are held until your subscribe ack is queued, then released with no snapshot ahead of them. On `l4Book` this gives you raw diffs, which **do not constitute a book** unless you already hold compatible state for that coin. On `twapStatuses` and `twapStatusesByUser` it gives you state changes with no live set under them, so you learn about a TWAP only when it next changes state.

Omitting `snapshot` defaults to sending one, unless you supplied a `sequence`. Three feeds have state to send — `l4Book`, `twapStatuses` and `twapStatusesByUser`; on every other feed a subscribe starts at the next matching block either way.

## Errors on an open socket

`{"channel":"error","data":"<string>"}` arrives **on an open socket** and never closes it — the connection keeps serving whatever it was already serving. This is the opposite of a [refused handshake](/websocket/overview#when-the-handshake-is-refused), which ends the connection before it exists.

| `data`                                  | What it means                                                                                                                                                                                                                                                                                                                                                                                            |
| --------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `bad request`                           | The frame did not parse as the server expects — malformed JSON, a non-string `type`, or an argument value that is neither a string nor an array of strings. A well-formed frame the server merely cannot act on gets one of the strings below.                                                                                                                                                           |
| `unknown method`                        | `method` is not `subscribe`, `unsubscribe` or `ping`. The retired `{"method":"auth"}` frame lands here: authentication happens on the upgrade, not in a frame.                                                                                                                                                                                                                                           |
| `unknown subscription type`             | `type` is not in the catalog. The type is case-sensitive — `tradesByCoin` works, `TRADESBYCOIN` does not. A subscribe carrying no `subscription` object, or a `subscription` with no `type`, never reaches this check and answers `bad request` instead.                                                                                                                                                 |
| `unknown argument <arg>`                | The subscription carried an argument this feed does not declare, naming it.                                                                                                                                                                                                                                                                                                                              |
| `missing argument <arg>`                | A required argument was absent or empty, naming it: `missing argument coin`, `missing argument user`. An **empty `users` array** lands here, not on `empty argument users`.                                                                                                                                                                                                                              |
| `empty argument <arg>`                  | The argument was present but its value — or one item of its list — is the empty string. The check is per item.                                                                                                                                                                                                                                                                                           |
| `too many values for argument <arg>`    | More values than the argument accepts. Only `users` accepts more than one, and its ceiling is **1,000 distinct wallets** after deduplication; every other argument is scalar.                                                                                                                                                                                                                            |
| `invalid argument <arg>`                | The argument was present but its value is not one this feed accepts, naming it. Today only `candle` and `allCandles` declare allowed values, so this is `invalid argument interval` for anything outside the [14 intervals](/websocket/feeds#intervals) — `{"type":"candle","coin":"BTC","interval":"2m"}` answers it — and `invalid argument source` for a `source` that is neither `trade` nor `mark`. |
| `too many subscriptions`                | This connection is at its per-connection subscription cap. The cap counts distinct subscriptions, so a repeat of one you already hold is free, and a `users` list of 1,000 wallets counts as **one**; unsubscribe one and the slot frees immediately.                                                                                                                                                    |
| `server at subscription capacity`       | A service-wide bound on unique subscriptions. Joining a subscription that already exists still works.                                                                                                                                                                                                                                                                                                    |
| `feed not in tier`                      | That subscription type is not included in your tier — the firehose and the Starter list feeds on Free, `l4Book` and the Pro list feeds below Pro. Check `connected.data.limits.feeds`.                                                                                                                                                                                                                   |
| `snapshot buffer did not fill`          | The server could not collect enough consecutive blocks to join a snapshot onto before giving up. The source has stalled; subscribe again.                                                                                                                                                                                                                                                                |
| `invalid sequence` / `invalid snapshot` | `sequence` was not a number, or `snapshot` was not a boolean.                                                                                                                                                                                                                                                                                                                                            |

A `twapStatuses` or `twapStatusesByUser` subscription whose **snapshot** fails is dropped the same way, after its ack, with one of these on the `error` channel. The frame names no feed, so a connection holding both TWAP-status subscriptions cannot tell which one ended from the frame alone:

| `data`                                        | What it means                                                                                                               |
| --------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------- |
| `twap gateway not configured`                 | This deployment has no TWAP gateway connection. Not retryable on your side.                                                 |
| `subscription not active`                     | The subscription had not reached the publisher within a second of the request. Subscribe again.                             |
| `snapshot timed out`                          | The gateway did not answer within the snapshot deadline.                                                                    |
| `gateway busy`                                | The gateway refused the request under load. Back off and subscribe again.                                                   |
| `gateway unavailable`                         | The gateway could not be reached.                                                                                           |
| `cancelled`                                   | The request was cancelled before it completed — usually the connection going away.                                          |
| `snapshot failed: <Code>` / `snapshot failed` | Any other gateway failure, with the gRPC status code when there is one.                                                     |
| `snapshot is older than retained history`     | The snapshot landed behind the oldest frame still buffered, so the two could not be joined without a hole. Subscribe again. |

In every case **the subscription no longer exists** — nothing is retried for you and no live frames follow. Subscribe again rather than waiting.

A book subscription that starts and then fails — an unknown coin, a snapshot that could not be fetched, a buffer that never filled — is **dropped** and reported here, on the ordinary `error` channel, as `{"channel":"error","data":"<reason>"}`. The frame does not name the coin, so a connection holding several books cannot tell which one ended from the frame alone. The reasons are listed under [when a book subscription is dropped](/websocket/order-book#when-a-book-subscription-is-dropped).

## Closing

Close the socket from your side with a normal closure (`1000`); the connection is returned to your account's pool at once. A silent disconnect — a laptop lid, a dropped network — holds its slot until keepalive notices, roughly 40 seconds, which is why a fast reconnect after a crash can meet a `429` briefly.
