> For the complete documentation index, see [llms.txt](https://docs.nexus.xyz/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.nexus.xyz/api-reference/websocket/connect-web-socket.md).

# GET /ws

Per-account WebSocket stream.

`operationId`: `connectWebSocket`

**Authentication:** none declared, and the contract declares no document-level default — this operation carries its own signature in the request body. See the description above.

WebSocket endpoint for both public market data and per-account private channels. Requires a token from POST /ws/token:

```
wss://host/ws?token=YOUR_TOKEN
```

Tokens are single-use and expire after 60 seconds, and they are scoped to the network that minted them — mint the token on the same host you connect to.

Every client→server and server→client message is a JSON envelope tagged with an `op` field. Subscribe to one channel per message:

```json
{"op": "subscribe", "channel": "trades", "market": "BTC-USDX-PERP"}
{"op": "subscribe", "channel": "fills"}
```

Unsubscribe with `{"op": "unsubscribe", "channel": "...", "market": "..."}`.

**Public channels** (the token is always required to upgrade): `trades`, `book`, `candles`, `ticker` — each requires a `market` field.

`ticker` carries the same payload `GET /markets/{market_id}/ticker` returns — both are built by one function, so the REST and streaming views of a ticker cannot disagree. It is published once per book-poll cycle (currently 1 s) rather than per trade, because the 24h high, low, open and volume come from the materialized candles and a running notional, not from any single fill; a ticker projected off trades would move `last` and leave the 24h fields stale. Frames are emitted every cycle whether or not the values moved, as `book` is — diff against the previous frame if you only care about changes. `engine` is public and venue-wide, so it takes no `market` field: subscribe and unsubscribe are accepted and acked, but no frames are published on it yet. Treat it as reserved — do not build against a payload shape.

**Per-account channels** (scoped to the wallet that minted the token): `orders`, `fills`, `positions`, `balances`, `liquidations`.

**Server messages:**

* `{"op": "subscribed", "channel": "fills", "market": null, "seq_at_join": 42}` — subscribe ack; `seq_at_join` is the channel's current sequence at attach time, used as the reconnect cursor.
* `{"op": "unsubscribed", "channel": "fills", "market": null}` — unsubscribe ack.
* `{"op": "event", "channel": "fills", "market": null, "seq": 43, "payload": {...}}` — a delivered event; `seq` is monotonic per channel.
* `{"op": "out_of_sync", "channel": "fills", "market": null, "oldest_seq": 100}` — the requested `since` cursor fell behind the replay buffer; refetch state over REST and resubscribe with a fresh cursor.
* `{"op": "error", "message": "..."}` — invalid op, unknown channel, or bad message format.

**`liquidations` payloads:** the `payload` of an `event` on this channel is externally tagged — exactly one key, naming the engine event variant. See the `LiquidationEvent` schema for the full shape. Monetary fields are lossless decimal strings, never JSON numbers.

* `LiquidationAlert` — a pre-liquidation risk warning for the account. `severity` is `Warning`, `Critical` or `Imminent`, classified from `equity / maintenance_margin`: `Warning` in (1.2, 1.5], `Critical` in (1.05, 1.2], `Imminent` in (1.0, 1.05]. Nothing is emitted above 1.5 (safe) or at/below 1.0 (already liquidating). `market_id` is `null` for a portfolio-level alert over the whole cross-margin account — the only form the engine emits today — and names a market when the alert is scoped to one market.
* `PortfolioLiquidation` — terminal: the account's cross-margin positions have already been closed out. `closures` carries the per-market closes.

Alerts are **edge-triggered**: one frame per worsening severity transition, nothing while a severity holds, and nothing when the account recovers. Subscribing does not replay the current alert state — unlike `balances` and `positions`, this channel is not seeded with a snapshot on subscribe — and there is no REST read of it, so a reconnecting client sees nothing until the next worsening transition.

```json
{"op": "event", "channel": "liquidations", "market": null, "seq": 7, "payload": {"LiquidationAlert": {"account_id": "0x1111111111111111111111111111111111111111", "market_id": null, "severity": "Critical", "equity": "1150.00", "maintenance_margin": "1000.00", "sequence": 9412, "epoch": 3, "emitted_at": 1750000000000}}}
```

**Reconnection:** tokens are single-use — mint a new one via POST /ws/token before reconnecting. To resume a channel without gaps, pass the last `seq` you received:

```json
{"op": "subscribe", "channel": "fills", "since": 42}
```

**Control-plane limits.** The WebSocket ceilings are a resource class of their own: they are independent of the REST request budget and of the trading-action budget, and exhausting one does not affect the others. Per tier — `Pro`: 5 concurrent connections, 50 subscriptions, 10 inbound client frames per second; `MarketMaker`: 100, 1,000 and 50/s; gateway (`Unlimited`) keys are exempt from all three. Those connection figures are **per account**: a separate per-IP cap (5 by default) is applied first, at upgrade time, and binds on every tier including `Unlimited`, so one origin address cannot reach the per-account figure on its own. A connection refused there gets an HTTP `429` (`ws_conn_limit_exceeded`) at the upgrade rather than a close frame. A `2×` burst is tolerated above the sustained frame rate. The subscription ceiling is applied both per connection and across all of an account's connections at the same number, so opening more sockets does not buy more subscriptions; re-subscribing a key you already hold replaces it in place and is free. Exceeding it returns an `error` frame (`subscription_limit_exceeded`) rather than a `429` — there is no status code on this surface — and sustained inbound flooding closes the connection with `1008`. These figures are current per-deployment configuration, not part of this contract. See “Rate limits” in the API description.

## Parameters

| Name    | In    | Type     | Required | Description                           |
| ------- | ----- | -------- | -------- | ------------------------------------- |
| `token` | query | `string` | Yes      | Short-lived token from POST /ws/token |

## Responses

| Status | Body                     | Meaning                                  |
| ------ | ------------------------ | ---------------------------------------- |
| `101`  | — *(no schema declared)* | Switching Protocols — WebSocket upgrade. |

## Example

```bash
curl -X GET 'https://exchange.nexus.xyz/api/exchange/ws'
```

***

<sub>Generated from</sub> <sub></sub><sub>`eng/apps/exchange/api/openapi.json`</sub> <sub></sub><sub>(spec</sub> <sub></sub><sub>`0.9.27`</sub><sub>). Do not edit by hand — regenerate with</sub> <sub></sub><sub>`python3 product/docs/tools/api-reference/generate.py`</sub><sub>. Hand-authored context lives in the Guides pages.</sub>


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://docs.nexus.xyz/api-reference/websocket/connect-web-socket.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
