# Order book, ticker and trades


Source: https://docs.stxapp.io/websockets/channels/market-data/

Three topics carry market data, the same for every participant, in the same
[wire format](/websockets/channels/wire-format/) as the account channels and the
[REST API](/api/rest/).

| Topic | Carries | Filters |
| --- | --- | --- |
| `orderbook` | aggregated book levels | `market_ids` (**required**) |
| `ticker` | per-market price summary | `sports`, `competitions` |
| `trades` | executed trades, anonymous | `market_ids`, `event_ids` |

Each is a **single topic covering every market**, narrowed by the join payload.
Watching ten markets is one join, not ten.

## Field names

Money is a dollar string (`"0.3900"`), quantities are quantity strings
(`"100.00"`), and counts are plain integers. Field names are `snake_case`
throughout, and the offer side is called `offer`, not `ask`, the same names the
REST market payload uses.

## Filters

Every filter follows one contract:

- omitted, `null`, `[]`, or a list with no usable entry means **no filter**;
- unusable entries are dropped rather than rejecting the join;
- the join reply echoes what was actually applied, so compare it against what you
  sent to catch a typo;
- naming two filters **narrows**: a message must match both;
- `select_filters` (or `select_market_ids` on `orderbook`) changes them without
  rejoining.

`orderbook` is the one exception: it requires at least one valid `market_id`.

## `orderbook`

```
["1","1","orderbook","phx_join",{"market_ids":["<uuid>","<uuid>"]}]
```

An absent or unusable list is an error, not "no filter":

```json
{"status":"error","response":{"reason":"market_ids_required"}}
```

Pushes `"book"` on the matching engine's publish cadence, one message per market:

```json
{
  "market_id": "...",
  "bids": [{"price":"0.3900","quantity":"100.00","liquidity":"39.0000",
            "total_quantity":"100.00","total_liquidity":"39.0000"}],
  "offers": [],
  "timestamp": "2026-09-02T21:16:39.717812Z",
  "timestamp_us": 1788383799717812
}
```

Levels are best-first. `liquidity` is that level alone; `total_quantity` and
`total_liquidity` are cumulative through it.

:::caution[Every push is a full snapshot]
This is not a delta protocol. Replace the book you hold for that `market_id`
wholesale on each message rather than applying it incrementally.
:::

Change markets without rejoining:

```
["1","2","orderbook","select_market_ids",{"market_ids":["<uuid>"]}]
```

The non-empty requirement still applies; an unusable list leaves the current
selection in place and replies with an error.

## `ticker`

```
["1","1","ticker","phx_join",{}]
["1","1","ticker","phx_join",{"sports":["Football"],"competitions":["NFL"]}]
```

Pushes `"ticker"` for each market whose price, book top, volume or open interest
moved:

```json
{
  "market_id": "...", "market_symbol": "STXNFL-...",
  "event_id": "...", "event_symbol": "STXNFL-...",
  "sport": "Football", "competition": "NFL",
  "last_traded_price": "0.3900", "last_traded_quantity": "100.00",
  "best_bid": "0.3800", "best_bid_quantity": "250.00",
  "best_offer": "0.4000", "best_offer_quantity": "175.00",
  "bid_depth": 4, "offer_depth": 6,
  "open_interest": "1200.00", "total_volume": "48000.00",
  "timestamp": "...", "timestamp_us": 1788383799717812
}
```

`bid_depth` and `offer_depth` count price levels, so they are integers rather than
quantity strings. Any field can be `null` on a market that has not traded or has an
empty side of the book. `event_symbol` is `null` while the event list is still
warming.

Filter values are matched exactly as the market carries them, so `"Football"` and
`"football"` are different.

:::tip[No snapshot on join]
This is a change feed; nothing arrives until a market moves. Fetch
`GET /api/v1/markets` for the initial state, then keep it current from here.
:::

## `trades`

```
["1","1","trades","phx_join",{}]
["1","1","trades","phx_join",{"market_ids":["<uuid>"],"event_ids":["<uuid>"]}]
```

Pushes `"trade"`, one per execution:

```json
{
  "market_id": "...", "market_symbol": "STXNFL-...",
  "event_id": "...", "event_symbol": "STXNFL-...",
  "price": "0.3200", "quantity": "100.00", "action": "buy",
  "timestamp": "...", "timestamp_us": 1788383799717812
}
```

`action` is the **taker's** side: `"buy"` when the incoming order bought from the
book, `"sell"` when it sold into it.

This feed is anonymous: it carries no account, user or order identifier for any
market participant.

:::caution[`trades` is not `fills:{user_id}`]
`trades` is every trader's executions; [`fills:{user_id}`](/websockets/channels/fills/)
is yours. They differ by one letter and are not interchangeable.
:::
