# WebSockets

The WebSocket gateway provides real-time streaming access to market data and trading updates on the Ethereal exchange. Clients subscribe to channels over a persistent connection and receive plain JSON payloads with short-form keys, optimized for low latency, minimal overhead, and fast parsing.

<table><thead><tr><th width="370.79296875">URL</th><th>Status</th></tr></thead><tbody><tr><td><code>wss://ws2.ethereal.trade/v1/stream</code></td><td>Mainnet</td></tr><tr><td><code>wss://ws2.etherealtest.net/v1/stream</code></td><td>Testnet</td></tr><tr><td><code>wss://ws.ethereal.trade/v1/stream</code></td><td>Deprecated (Socketio)</td></tr></tbody></table>

{% hint style="info" %}
Native WebSocket (v2) replaces socketio. Visit [Socket.io (deprecated)](/developer-guides/trading-api/websockets/socket.io-deprecated.md) for socket.io based streams.
{% endhint %}

### Subscription

The subscription gateway offers multiple data streams for real-time and periodic updates. Some channels e.g. `OrderUpdate` push messages immediately as they occur, while others e.g. `L2Book` emit messages at fixed intervals. Payload formats and message shapes are documented below.

#### Connection Behavior

Connections have a maximum lifetime of approximately **4 hours**, after which they are closed with ***code 1000***. Clients are encouraged to implement automatic reconnection to handle this.

Idle connections with no message activity between both parties are closed after a period of inactivity with ***code 1006***. It is the *responsibility of the client to send WebSocket ping frames periodically* to prevent idle disconnects (we recommend once every 30s).

{% code overflow="wrap" expandable="true" %}

```javascript
// Example client-side heartbeat (javascript)
const WebSocket = require("ws");
const ws = new WebSocket("wss://ws2.ethereal.trade/v1/stream");

let pingInterval;

ws.on("open", () => {
  pingInterval = setInterval(() => {
    if (ws.readyState === WebSocket.OPEN) {
      ws.ping();
    }
  }, 30_000);
});

ws.on("close", () => {
  clearInterval(pingInterval);
});
```

{% endcode %}

{% hint style="warning" %}
There is a per-connection limit on subaccount subscriptions.&#x20;

Each `(subaccountId, streamType)` pair counts as one subscription. Market data channels (`L2Book`, `Ticker`, `TradeFill`) are not subject to this limit.&#x20;

Exceeding the limit returns `{ ok: false, code: "SUBSCRIPTION_LIMIT_EXCEEDED" }`
{% endhint %}

#### `L2Book`&#x20;

Provides L2 book depth updates for a specific product.

<pre class="language-json" data-expandable="true"><code class="lang-json"><strong>// Subscription message payload
</strong>{
  "event": "subscribe",
  "data": {
    "type": "L2Book",
    "symbol": "&#x3C;string>" // e.g. "BTCUSD", "ETHUSD"
  }
}

// Response message
{
  "e": "L2Book",
  "t": &#x3C;epoch>,
  "data": {
    "s": "&#x3C;string>",
    "t": &#x3C;epoch>,
    "pt": Optional&#x3C;epoch>,
    "a": [[price: string, quantity: string]],
    "b": [[price: string, quantity: string]]
  }
}
</code></pre>

**`L2_BOOK`** events are emitted on a configurable fixed interval (as of writing, this is configured to be *once every 200ms*).

* `e` - event name
* `t` - server timestamp (epoch in milliseconds)
* `data`  - L2 Book price levels details
  * `s` - symbol e.g. BTCUSD
  * `t` - calculated book timestamp (epoch in milliseconds)&#x20;
  * `pt` - previous calculated book timestamp (epoch in milliseconds) - optional
    * Using both the `pt` and `data.t` you can infer whether or not any events were missed during connection or during consumption
  * `a` - asks, array of `[price, qty]` pairs
  * `b` - bids, array of `[price, qty]` pairs

{% hint style="warning" %}
A `L2Book` message of the current book **(up to 100 price levels per side)** is emitted back as an initial snapshot on connection. Every subsequent message is a price level diff with absolute quantities. A zero quantity price diff indicates that this level has been removed.
{% endhint %}

#### `TICKER`

Delivers real-time ticker data feeds for a specified product.

{% code expandable="true" %}

```json
// Subscription message payload
{
  "event": "subscribe",
  "data": {
    "type": "Ticker",
    "symbol": "<string>" // e.g. "BTCUSD", "ETHUSD"
  }
}

// Response message
{
  "e": "Ticker",
  "t": <epoch>,
  "data": {
    "s": "<string>",
    "t": <epoch>, 
    "bidPx": "Optional<string>",
    "askPx": "Optional<string>",
    "bidAmt": "Optional<string>",
    "askAmt": "Optional<string>",
    "markPx": "Optional<string>",
    "markPx24h": "Optional<string>",
    "oi": "Optional<string>",
    "fr1h": "Optional<string>",
    "vol24h": "Optional<string>"
  }
}
```

{% endcode %}

**`TICKER`** events are emitted on a configurable fixed interval (currently configured to be *once every second*).

* `e` - event name `Ticker`
* `t` - server timestamp this message was emitted at (epoch in milliseconds)
* `data` - Real time ticker data
  * `s` - symbol e.g. BTCUSD
  * `t` - calculated best bid / ask book timestamp
  * `bidPx` - best bid price
  * `askPx` - best ask price
  * `bidAmt` - total quantity at the best bid
  * `askAmt` - total quantity at the best ask
  * `markPx` - current mark price
  * `markPx24h` - 24h mark price
  * `oi` - open interest
  * `fr1h` - projected funding rate at the end of the hour
  * `vol24h` - past 24 hours volume&#x20;

{% hint style="warning" %}
In extreme cases, **`Ticker`** will skip publishing if **both** **`bidPx`** & **`askPx`** are not available. All values are returned as decimals (9 precision).
{% endhint %}

#### `TRADE_FILL`

Provides a stream of trades that have occurred filtered by product.

```json
// Subscription message payload
{
  "event": "subscribe",
  "data": {
    "type": "TradeFill",
    "symbol": "<string>" // e.g. "BTCUSD", "ETHUSD"
  }
}

// Response message
{
    "e": "TradeFill",
    "t": <epoch>,
    "data": {
        "s": "<symbol>",
        "t": <epoch>,
        "d":[{
            "id": "<uuid>",
            "px": "<string>",
            "sz": "<string>",
            "sd": 0|1,
            "sids": ["<uuid>", "<uuid>"]
        }]
    }
}
```

**`TRADE_FILL`** events are streamed in real-time as they occur and from the perspective of the taker (i.e. `sz`, `sd`).

* `e` - event name
* `t` - server timestamp this message was emitted at (epoch in milliseconds)
* `data`&#x20;
  * `s` - symbol of product traded
  * `t` - timestamp trade fills happened (epoch in milliseconds)
  * `d` - array of fills that occurred on the product
    * `s` - symbol e.g. BTCUSD where the trade fill occurred on
    * `id` - trade fill identifier
    * `px` - execution price
    * `sz` - quantity traded
    * `sd` - side (`0=BUY` or `1=SELL`) from the perspective of the taker
    * `sids` - tuple of the taker subaccount id and the maker subaccount id

#### `SUBACCOUNT_LIQUIDATION`

Provides an update when a subaccount is liquidated.

{% code expandable="true" %}

```json
// Subscription message payload
{
  "event": "subscribe",
  "data": {
    "type": "SubaccountLiquidation",
    "subaccountId": "<uuid>"
  }
}

// Response message
{
  "e": "SubaccountLiquidation",
  "t": <epoch>,
  "data": {
    "sid": "<uuid>",
    "t": <epoch>,
    "d": [
      {
        "s": "<string>",
        "px": "<string>",
        "sz": "<string>"
      }
    ]
  }
}
```

{% endcode %}

When a subaccount is liquidated, all positions are transferred to the insurance fund and derisked at a later time. `SubaccountLiquidation` events are emitted in real-time.

* `e` - event name
* `t` - server timestamp this message was emitted at (epoch in milliseconds)
* `data` - Liquidation subaccount data&#x20;
  * `sid` - `id` of liquidated sub-account
  * `t` - timestamp sub-account was liquidated (epoch in miliseconds)
  * `d` - An array of positions liquidated (subaccount may have one or many positions at liquidation):
    * `price` - mark price at the time of liquidation
    * `sz` - position size at liquidation (positive of long, negative if short)

#### **`POSITION_UPDATE`**

Provides real-time updates to open positions for a specific subaccount.

{% code overflow="wrap" expandable="true" %}

```json
// Subscription message payload
{
  "event": "subscribe",
  "data": {
    "type": "PositionUpdate",
    "subaccountId": "<uuid>"
  }
}

// Response message
{
  "e": "PositionUpdate",
  "t": <epoch>,
  "data": {
    "t": <epoch>,
    "d": [
      {
        "id": "<uuid>",
        "sid": "<uuid>",
        "s": "<string>",
        "sd": 0|1,
        "sz": "<string>",
        "cost": "<string>",
        "rpnl": "<string>",
        "fpnl": "<string>",
        "fee": "<string>",
        "lpx": "Optional<string>"
      }
    ]
  }
}
```

{% endcode %}

**`POSITION_UPDATE`** events are emitted in real-time, published per-subaccount whenever a position is opened, increased/reduced, or closed.

* `e` - event name
* `t` - server timestamp (epoch in milliseconds)
* `data` - position update details
  * `t` - update timestamp (epoch in milliseconds)
  * `d` - array of position updates
    * `id` - position ID (UUID)
    * `sid` - subaccount ID (UUID)
    * `s` - ticker symbol (e.g. ETHUSD or BTCUSD)
    * `sd` - position side (BUY or SELL)
    * `sz` - position size
    * `cost` - position value in USD (`quantity * average entry price`)
    * `rpnl` - realized PnL in USD
    * `fpnl` - funding in USD (charged and applied to position, negative if paid)
    * `fee` - fees accrued in USD
    * `lpx` - liquidation price (only set if liquidated)

{% hint style="info" %}
Funding charges do not trigger `PositionUpdate` events.
{% endhint %}

#### **`ORDER_UPDATE`**&#x20;

Provides updates about order status changes for a specific subaccount.

{% code expandable="true" %}

```json
// Subscription message payload
{
  "event": "subscribe",
  "data": {
    "type": "OrderUpdate",
    "subaccountId": "<uuid>"
  }
}

// Response message
{
  "e": "OrderUpdate",
  "t": <epoch>,
  "data": {
    "t": <epoch>,
    "d": [
      {
        "id": "<uuid>",
        "cloid": "Optional<string>",
        "otyp": "LIMIT" | "MARKET",
        "qty": "<string>",
        "aqty": "<string>",
        "fill": "<string>",
        "px": "Optional<string>",
        "sd": 0|1,
        "s": "<string>",
        "sid": "<uuid>",
        "sn": "<string>",
        "st": "<string>",
        "t": <epoch>,
        "ro": boolean,
        "cl": boolean,
        "tif": "Optional<string>",
        "et": <epoch>,
        "po": Optional<boolean>,
        "spx": "Optional<string>",
        "styp": Optional<number>,
        "spxtyp": Optional<number>,
        "tr": "<string>",
        "gtyp": Optional<number>,
        "gid": "Optional<uuid>",
        "rr": "Optional<string>"
      }
    ]
  }
}
```

{% endcode %}

**`ORDER_UPDATE`** events are emitted in real-time, published per-subaccount whenever an order's state changes.

* `e` - event name
* `t` - server timestamp (epoch in milliseconds)
* `data` - order update details
  * `t` - update timestamp (epoch in milliseconds)
  * `d` - array of order updates
    * `id` - order ID (UUID)
    * `cloid` - client order ID
    * `otyp` - order type
    * `qty` - original quantity
    * `aqty` - available (remaining) quantity
    * `fill` - filled amount
    * `px` - limit price - optional, omitted for market orders
    * `sd` - side (`BUY=0`, `SELL=1`)
    * `s` - symbol e.g. BTCUSD
    * `sid` - subaccount ID (UUID)
    * `sn` - sender (signer EVM address)
      * Account or linked signer address that originally placed this order
    * `st` - order status (enum, same status as `OrderDto.status`)
      * One of: `NEW, PENDING, FILLED_PARTIAL, FILLED, REJECTED, CANCELED, EXPIRED`
    * `t` - order created timestamp (epoch in milliseconds)
    * `ro` - reduce only (boolean)
    * `cl` - close (boolean)
    * `tif` - time in force
    * `et` - expires at (epoch in seconds)
    * `po` - post only
    * `spx` - stop price
    * `styp` - stop type
    * `spxtyp` - stop price type
    * `tr` - triggered state
    * `gtyp` - group contingency type
    * `gid` - group ID (UUID)
    * `rr` - reason the order was rejected e.g. `CausesImmediateLiquidation`, `OrderIncreasesPosition`, `MarketOrderReachedMaxSlippage`, etc.
      * See: `OrderDto.rejectedReason` for the full list of possible values

#### `ORDER_FILL`

Notifies when orders are filled for a specific subaccount.

{% code expandable="true" %}

```json
// Subscription message payload
{
  "event": "subscribe",
  "data": {
    "type": "OrderFill",
    "subaccountId": "<uuid>"
  }
}

// Response message
{
  "e": "OrderFill",
  "t": <epoch>,
  "data": {
    "t": <epoch>,
    "d": [
      {
        "id": "<uuid>",
        "oid": "<uuid>",
        "cloid": "Optional<string>",
        "px": "<string>",
        "sz": "<string>",
        "typ": "LIMIT" | "MARKET",
        "sd": 0|1,
        "s": "<string>",
        "sid": "<uuid>",
        "ro": boolean,
        "fee": "<string>",
        "m": boolean,
        "t": <epoch>
      }
    ]
  }
}
```

{% endcode %}

**`ORDER_FILL`** events are emitted in real-time as they occur, published per-subaccount whenever an order is filled (both maker and taker sides receive their own event).

* `e` - event name
* `t` - server timestamp (epoch in milliseconds)
* `data` - order fill details
  * `t` - fill timestamp (epoch in milliseconds)
  * `d` - array of order fills
    * `id` - fill ID (UUID)
    * `oid` - order ID (UUID)
    * `cloid` - client order ID - optional
    * `px` - fill price
    * `sz` - filled quantity
    * `typ` - order type
    * `sd` - side
    * `s` - symbol e.g. BTCUSD
    * `sid` - subaccount ID (UUID)
    * `ro` - reduce only
    * `fee` - fee in USD
    * `m` - is maker
    * `t` - created at timestamp (epoch in milliseconds)

#### `TOKEN_TRANSFER`

Provides updates for deposits / withdrawals for a specific subaccount.

{% code expandable="true" %}

```json
// Subscription message payload
{
  "event": "subscribe",
  "data": {
    "type": "TokenTransfer",
    "subaccountId": "<uuid>"
  }
}

// Response message
{
  "e": "TokenTransfer",
  "t": <epoch>,
  "data": {
    "t": <epoch>,
    "id": "<uuid>",
    "sid": "<uuid>",
    "tName": "<string>",
    "tAddr": "<hex>",
    "typ": "<string>",
    "st": "<string>",
    "amt": "<string>",
    "fee": "<string>",
    "iniBk": "Optional<string>",
    "finBk": "Optional<string>",
    "iniTx": "Optional<hex>",
    "finTx": "Optional<hex>",
    "lzAddr": "Optional<hex>",
    "lzEid": "Optional<integer>"
  }
}
```

{% endcode %}

**`TOKEN_TRANSFER`** events are published per-subaccount when a deposit or withdrawal state changes.

* `e` - event name
* `t` - server timestamp this message was emitted at (epoch in milliseconds)
* `data` - token transfer details
  * `id` - unique identifier of the transfer
  * `t` - timestamp token transfer event (epoch in miliseconds)
  * `sid` - subaccount id that owns this transfer&#x20;
  * `tName` - token name
  * `tAddr` - token contract address
  * `typ` - transfer type, one of: `"DEPOSIT"`, `"WITHDRAW"`
  * `st` - transfer status, one of: `"SUBMITTED"`, `"PENDING"`, `"COMPLETED"`, `"REJECTED"`
  * `amt` - transfer amount
  * `fee` - transaction fee
  * `iniBk` - block number when the transfer was initiated (optional)
  * `finBk` - block number when the transfer was finalized (optional)
  * `iniTx` - Transaction hash of the initiation transaction (optional)
  * `finTx` - Transaction hash of the finalization transaction (optional)
  * `lzAddr` - LayerZero destination address for cross-chain bridge transfers (optional)
  * `lzEid` - LayerZero endpoint id identifying the destination chain&#x20;

#### Unsubscribing from Channels

To `unsubscribe` from a channel, send the same payload used to subscribe but with `unsubscribe` as the `event` type. Alternatively, disconnecting your entire connection will end all subscriptions. Note that reconnecting will consume rate limits. See [System Limits](/developer-guides/trading-api/system-limits.md) for more details.

```json
{
  "event": "unsubscribe",
  "data": {
    "type": "MarketPrice",
    "symbol": "<string>" // e.g. "BTCUSD", "ETHUSD"
  }
}
```

#### Ping / Pong

WebSocket connections include protocol-level ping/pong frames that are handled automatically and are not exposed at the application layer. However, some clients may require explicit liveness checks or latency measurement. In these scenarios, the gateway supports an application-level `ping` event:

```json
// Request message payload
{
  "event": "ping"
}

// Response message
{
  "e": "pong",
  "t": <epoch>
}
```

* `e` - `pong` response from a previous `ping`&#x20;
* `t` - server timestamp this message was emitted at (epoch in milliseconds)

{% hint style="info" %}
This mechanism is optional and is not required to maintain the WebSocket connection. ***Note that is not yet available on mainnet.***
{% endhint %}

#### Exception Handling

Error responses are returned directly as a reply to the originating event (e.g. `subscribe`, `unsubscribe`). They follow a unified shape:

```json
{
  "ok": false,
  "code": "UNKNOWN_PRODUCT"
}
```

* `ok` indicates whether the request succeeded
* `code` a machine-readable error code present when `ok` is `false`

<table><thead><tr><th width="258.19921875">Error codes</th><th>Description</th></tr></thead><tbody><tr><td><code>UNKNOWN_PRODUCT</code></td><td>The provided symbol does not match any known product.</td></tr><tr><td><code>VALIDATION_ERROR</code></td><td>The request payload failed validation (e.g. missing or invalid fields).</td></tr><tr><td><code>SUBSCRIPTION_FAILED</code></td><td>The server was unable to subscribe to the requested topic.</td></tr><tr><td><code>UNSUBSCRIBE_FAILED</code></td><td>The server was unable to unsubscribe from the requested topic.</td></tr><tr><td><code>RATE_LIMIT</code></td><td>Too many requests, the client has been rate-limited.</td></tr><tr><td><code>INTERNAL_ERROR</code></td><td>An unexpected server-side error occurred.</td></tr><tr><td><code>SUBSCRIPTION_LIMIT_EXCEEDED</code></td><td>Too many subaccount subscriptions on this connection.</td></tr></tbody></table>

{% hint style="success" %}
A successful response would simply just return `{ "ok": true, ... }`
{% endhint %}


---

# Agent Instructions: 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:

```
GET https://docs.ethereal.trade/developer-guides/trading-api/websockets.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
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.
