# WebSockets

The WebSocket gateway sends live market data and trading updates. A client subscribes to streams on one persistent connection. Messages use JSON and short field names.

```text
wss://ws.meridiantest.net/v1/stream
```

Meridian Perpetuals mainnet is not yet live. Use the published host from [API Hosts](/protocol-reference/api-hosts) when another environment becomes available.

:::info
Meridian uses the native WebSocket gateway described on this page.
:::

## Connection

Connections have a maximum lifetime of 239 minutes. The server closes the connection with code `1000` at the end of this period. Implement automatic reconnection and restore subscriptions after a disconnect.

The server sends protocol-level ping frames automatically. A client must respond according to its WebSocket library. The server can also close idle or backpressured connections.

## Subscribe

Send a `subscribe` event for each required stream. Market-data streams use a product symbol; account streams use a subaccount ID.

:::warning
There is a per-connection limit on subaccount subscriptions.

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

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

## Market Data

### `L2Book`

Provides L2 book depth updates for a specific product.

```json
// Subscription message payload
{
  "event": "subscribe",
  "data": {
    "type": "L2Book",
    "symbol": "<string>" // For example: "BTCUSD" or "ETHUSD"
  }
}

// Response message
{
  "e": "L2Book",
  "t": <epoch>,
  "data": {
    "s": "<string>",
    "t": <epoch>,
    "pt": Optional<epoch>,
    "a": [[price: string, quantity: string]],
    "b": [[price: string, quantity: string]]
  }
}
```

The gateway sends `L2Book` events at a configured interval.

* `e` - event name
* `t` - server timestamp (epoch in milliseconds)
* `data` - L2 Book price levels details
  * `s` - symbol, for example `BTCUSD`
  * `t` - calculated book timestamp (epoch in milliseconds)
  * `pt` - previous calculated book timestamp in Unix milliseconds. This field is optional.
    * Compare `pt` with the prior `data.t` value to detect a missed update.
  * `a` - asks, array of `[price, qty]` pairs
  * `b` - bids, array of `[price, qty]` pairs

:::warning
A successful `L2Book` subscription sends the current book with up to 100 price levels per side. Subsequent messages contain changed price levels with absolute quantities. A zero quantity removes the level.
:::

### `Ticker`

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

```json
// Subscription message payload
{
  "event": "subscribe",
  "data": {
    "type": "Ticker",
    "symbol": "<string>" // For example: "BTCUSD" or "ETHUSD"
  }
}

// Response message
{
  "e": "Ticker",
  "t": <epoch>,
  "data": {
    "s": "<string>",
    "t": Optional<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>"
  }
}
```

The gateway sends `Ticker` events at a configured interval.

* `e` - event name `Ticker`
* `t` - server timestamp this message was emitted at (epoch in milliseconds)
* `data` - Real time ticker data
  * `s` - symbol, for example `BTCUSD`
  * `t` - calculated best-bid and best-ask book timestamp, when available
  * `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

:::warning
The gateway can skip a `Ticker` update when `bidPx` and `askPx` are both unavailable. Decimal values use a maximum precision of nine decimal places.
:::

### `TradeFill`

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

```json
// Subscription message payload
{
  "event": "subscribe",
  "data": {
    "type": "TradeFill",
    "symbol": "<string>" // For example: "BTCUSD" or "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 sent when trades occur. The `sz` and `sd` fields show the taker's quantity and side.

* `e` - event name
* `t` - server timestamp this message was emitted at (epoch in milliseconds)
* `data`
  * `s` - symbol of product traded
  * `t` - timestamp trade fills happened (epoch in milliseconds)
  * `d` - array of fills that occurred on the product
    * `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

## Account Events

### `SubaccountLiquidation`

Provides an update when a subaccount is liquidated.

```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>"
      }
    ]
  }
}
```

The event lists the positions that liquidation closes. Liquidation applies to the affected quote-token margin pool; positions and balances in another pool can remain open. Resolve each position's `s` ticker through `GET /v1/product?ticker=...`, then use the returned `quoteTokenAddress` to identify the pool.

* `e` - event name
* `t` - server timestamp this message was emitted at (epoch in milliseconds)
* `data` - Liquidation subaccount data
  * `sid` - ID of the liquidated subaccount
  * `t` - liquidation time in Unix milliseconds
  * `d` - an array of liquidated positions:
    * `s` - product ticker
    * `px` - mark price at the time of liquidation
    * `sz` - position size at liquidation (positive of long, negative if short)

### `PositionUpdate`

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

```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>",
        "pfee": "<string>",
        "lpx": "Optional<string>"
      }
    ]
  }
}
```

**`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, for example `ETHUSD` or `BTCUSD`
    * `sd` - position side (BUY or SELL)
    * `sz` - position size
    * `cost` - position cost basis in USD
    * `rpnl` - realized PnL in USD
    * `fpnl` - accrued funding in USD; positive when paid and negative when received
    * `fee` - fees accrued in USD
    * `pfee` - position fees accrued in USD
    * `lpx` - liquidation price (only set if liquidated)

:::info
A funding-rate update does not by itself emit `PositionUpdate`. When accrued funding is realized by a later position change, the event's `fpnl` includes it.
:::

### `OrderUpdate`

Provides updates about order status changes for a specific subaccount.

```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>"
      }
    ]
  }
}
```

**`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, for example `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` - rejection reason, such as `CausesImmediateLiquidation`, `OrderIncreasesPosition`, or `MarketOrderReachedMaxSlippage`
      * See: `OrderDto.rejectedReason` for the full list of possible values

### `OrderFill`

Notifies when orders are filled for a specific subaccount.

```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>
      }
    ]
  }
}
```

**`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, for example `BTCUSD`
    * `sid` - subaccount ID (UUID)
    * `ro` - reduce only
    * `fee` - fee in USD
    * `m` - is maker
    * `t` - created at timestamp (epoch in milliseconds)

### `TokenTransfer`

Provides deposit, withdrawal, and conversion updates for a specific subaccount.

```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>",
    "toTName": "Optional<string>",
    "toTAddr": "Optional<hex>"
  }
}
```

**`TOKEN_TRANSFER`** events are published for a subaccount when a deposit, withdrawal, or conversion 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` - transfer-event time in Unix milliseconds
  * `sid` - subaccount ID that owns this transfer
  * `tName` - token name
  * `tAddr` - token contract address
  * `typ` - transfer type: `"DEPOSIT"`, `"WITHDRAW"`, or `"CONVERT"`
  * `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
  * `toTName` - destination token name for a conversion (optional)
  * `toTAddr` - destination token address for a conversion (optional)

## Unsubscribe

To unsubscribe from a stream, send the subscription payload again. Set `event` to `unsubscribe`. You can also close the connection to end all subscriptions. A new connection consumes rate-limit points. See [System Limits](/developer-guides/trading-api/system-limits).

```json
{
  "event": "unsubscribe",
  "data": {
    "type": "Ticker",
    "symbol": "<string>" // For example: "BTCUSD" or "ETHUSD"
  }
}
```

## Ping and Pong

The server sends WebSocket ping frames. Most client libraries handle the related pong frames automatically. The gateway also supports an optional application-level `ping` event. Use it for a liveness check or a latency measurement.

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

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

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

:::info
This mechanism is optional. It is not required to maintain the WebSocket connection.
:::

## Error Responses

The server sends an error response for the related `subscribe` or `unsubscribe` event. All error responses have this form:

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

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

| Error Code                    | Description                                                                   |
| ----------------------------- | ----------------------------------------------------------------------------- |
| `UNKNOWN_PRODUCT`             | The provided symbol does not match any known product.                         |
| `UNKNOWN_SUBACCOUNT`          | The provided subaccount ID does not match a known subaccount.                 |
| `VALIDATION_ERROR`            | The request payload failed validation, for example missing or invalid fields. |
| `SUBSCRIPTION_FAILED`         | The server was unable to subscribe to the requested topic.                    |
| `UNSUBSCRIBE_FAILED`          | The server was unable to unsubscribe from the requested topic.                |
| `RATE_LIMIT`                  | Too many requests, the client has been rate-limited.                          |
| `INTERNAL_ERROR`              | An unexpected server-side error occurred.                                     |
| `SUBSCRIPTION_LIMIT_EXCEEDED` | Too many subaccount subscriptions on this connection.                         |

:::success
A successful request returns `{ "ok": true }`.
:::
