# WebSocket Market Streams Overview

Live prices, candles and statistics. Market streams are **public** — no API key, no signature, no
`login`. Account events are on a separate, authenticated connection:
[User Data Streams Overview](/user-data-streams.md).

## Endpoints

| Form | URL |
| - | - |
| Raw | `wss://trade-stream.yellowboxmarkets.com/ws` |
| Raw, pre-subscribed | `wss://trade-stream.yellowboxmarkets.com/ws/XAUUSD@tick` |
| Combined | `wss://trade-stream.yellowboxmarkets.com/stream?streams=XAUUSD@tick/BTCUSD@kline_1m` |

On the **raw** endpoint, payloads arrive exactly as documented below.

On the **combined** endpoint, every payload is wrapped:

```json
{"stream":"XAUUSD@tick","data":{"e":"tick","E":1789012345678,"s":"XAUUSD","b":"2331.15","a":"2331.42","l":"2331.15","v":8,"bd":1,"ad":-1,"T":1789012345678}}
```

Streams in the `streams=` query parameter are separated by `/`. URL-encode the value if a symbol
name contains a character that needs it.

> **Stream names are case-sensitive and are NOT lowercased.** Unlike Binance, the symbol part of a
> stream name is the exact MT5 symbol name. `XAUUSD@tick` is correct; `xauusd@tick` does not exist
> and subscribing to it returns an error frame. The stream-type suffix is spelled exactly as
> documented — `@tick`, `@bookTicker`, `@kline_1m`, `@ticker`, `@depth` — and is case-sensitive too.

## Subscribe protocol

Send JSON text frames.

### SUBSCRIBE

```json
{"method":"SUBSCRIBE","params":["XAUUSD@tick","BTCUSD@kline_1m"],"id":1}
```

```json
{"result":null,"id":1}
```

### UNSUBSCRIBE

```json
{"method":"UNSUBSCRIBE","params":["XAUUSD@tick"],"id":2}
```

```json
{"result":null,"id":2}
```

### LIST_SUBSCRIPTIONS

```json
{"method":"LIST_SUBSCRIPTIONS","id":3}
```

```json
{"result":["BTCUSD@kline_1m"],"id":3}
```

### Request fields

| Field | Type | Mandatory | Description |
| - | - | - | - |
| `method` | STRING | YES | `SUBSCRIBE`, `UNSUBSCRIBE` or `LIST_SUBSCRIPTIONS`. |
| `params` | ARRAY of STRING | conditional | Stream names. Required for `SUBSCRIBE` / `UNSUBSCRIBE`. |
| `id` | UNSIGNED INT | YES | Chosen by you. Echoed on the response so you can correlate. Reuse is allowed but makes correlation ambiguous. |

### Error frames

```json
{"code":-1121,"msg":"Invalid symbol.","id":1}
```

```json
{"code":-1120,"msg":"Invalid interval.","id":4}
```

```json
{"code":-1101,"msg":"Too many parameters sent for this endpoint.","id":5}
```

| Code | When |
| - | - |
| `-1121` | A stream name whose symbol does not exist (exact case), or that is malformed or of an unknown stream type. |
| `-1120` | A `kline_` stream with an unsupported interval. |
| `-1101` | The subscription would exceed 200 streams on the connection. |
| `-1102` | `id`, `method` or (for `SUBSCRIBE` / `UNSUBSCRIBE`) `params` missing or of the wrong type. When `id` itself is unusable the frame carries `"id":null`. |
| `-1020` | An unknown `method`. |
| `-1130` | The frame is not a JSON object. `"id":null`. |

An error frame never closes the connection. `UNSUBSCRIBE` of a stream you are not subscribed to is a
no-op and answers success.

A `SUBSCRIBE` with several `params` is **all-or-nothing**: if one name is invalid the whole frame is
rejected and nothing is subscribed. Subscribe in small batches so one bad symbol does not cost you
the rest.

Codes are the same ones documented in [Error Codes](/error-codes.md).

## Connection limits

| Limit | Value |
| - | - |
| Ping frame from the server | every **3 minutes** |
| Disconnect if no pong | after **10 minutes** without one |
| Maximum connection lifetime | **24 hours** |
| Streams per connection | **200** |
| Inbound messages per second | **10** |
| Concurrent connections per client IP | **20** |

- Reply to every WebSocket **ping** frame with a **pong**. Most client libraries do this
  automatically; verify yours does. A missed pong window costs you the connection.
- You may send unsolicited pong frames as a heartbeat; they are ignored and do not count toward the
  message rate.
- A connection is **closed at 24 hours** regardless of activity. Reconnect before then. Overlap the
  connections — open the new one, subscribe, then drop the old one — so you do not miss ticks.
- Exceeding 10 inbound messages per second closes the connection immediately. Batch stream names
  into one `SUBSCRIBE` rather than sending one frame per stream.
- Exceeding 200 streams returns `-1101` and subscribes nothing.
- A connection beyond the per-IP limit — or beyond the service's overall capacity — is refused
  **before** the WebSocket upgrade with HTTP 503 and `-1008`. Put your streams on fewer connections
  (up to 200 each) rather than opening one per symbol.
- A plain HTTP request (no WebSocket upgrade) to a stream URL is answered HTTP 400 with `-1020`.
- An inbound message larger than 64 KB closes the connection.
- On the **pre-subscribed** and **combined** URLs, an invalid stream name in the URL is answered with
  one error frame (as above, `"id":null`) and the connection is then closed — nothing is subscribed.

The server closes a connection with these WebSocket close codes:

| Close code | Reason | Cause |
| - | - | - |
| `1000` | `connection lifetime reached` | The 24-hour lifetime elapsed (or the service is restarting). |
| `1000` | `invalid stream name` | An invalid name in a pre-subscribed or combined URL. |
| `1008` | `message rate exceeded` | More than 10 inbound messages in one second. |
| `1009` | `frame too large` | An inbound message over 64 KB. |

There are **no rate-limit weight headers** on WebSocket. Market streams do not consume REST request
weight, which is exactly why you should use them instead of polling.

## Reconnect guidance

Market streams carry no state and are not replayed. On reconnect:

1. Re-subscribe to everything you had. `LIST_SUBSCRIPTIONS` on the **new** connection is the way to
   verify, not to discover.
2. Refresh anything you were accumulating from the stream: candles from
   `GET /v1/klines`, and any last-price cache from `GET /v1/ticker/price`.
3. Assume you missed ticks during the gap. Never derive position state from the tick stream — read
   it from [`GET /v1/positions`](/trade/open-positions.md).

Reconnect with backoff and jitter. A fleet of bots reconnecting in lockstep after a restart is
self-inflicted load.
