<!-- Generated from TradeReady.io docs. Visit https://tradeready.io/docs for the full experience. -->

---
title: WebSocket Connection
description: Connect to the TradeReady WebSocket server for real-time streaming — auth, heartbeat, subscriptions, and error codes.
---

The WebSocket server delivers real-time price ticks, candle updates, private order fills, and portfolio snapshots without polling. A single connection can subscribe to up to 10 channels simultaneously.

---

## Connection URL

```
ws://your-host/ws/v1?api_key=ak_live_...
```

Or with a JWT token:

```
ws://your-host/ws/v1?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
```

Replace `your-host` with your deployed instance (e.g. `api.tradeready.io`). In local development: `ws://localhost:8000/ws/v1`.

> **Warning:**
> The WebSocket server authenticates on connection open. If the `api_key` or `token` is invalid or missing, the server closes the connection immediately with close code **4401**. There is no retry or challenge — obtain a valid key before connecting.

---

## Authentication

The WebSocket server performs its own authentication — it does not share sessions with the REST API middleware. Pass your credentials as a query parameter:

**API key (recommended):**
```
ws://localhost:8000/ws/v1?api_key=ak_live_Hx3kP9...
```

**JWT token:**
```
ws://localhost:8000/ws/v1?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
```

On authentication failure, the connection is closed with code `4401`. In your client, handle close code `4401` as a permanent failure (do not reconnect — obtain a valid key first).

---

## Message Format

All messages are JSON objects. Direction matters:

**Client → Server** (actions you send):
```json
{"action": "subscribe",   "channel": "ticker", "symbol": "BTCUSDT"}
{"action": "unsubscribe", "channel": "ticker", "symbol": "BTCUSDT"}
{"action": "pong"}
```

**Server → Client** (messages you receive):
```json
{"channel": "ticker", "symbol": "BTCUSDT", "data": {...}}
{"type": "ping"}
{"type": "error", "code": "INVALID_CHANNEL", "message": "Unknown channel."}
```

---

## Subscribe and Unsubscribe

```json
{"action": "subscribe",   "channel": "CHANNEL_NAME", ...channel_params}
{"action": "unsubscribe", "channel": "CHANNEL_NAME", ...channel_params}
```

On successful subscribe, the server begins streaming data for that channel. On unsubscribe, streaming stops and the subscription slot is freed.

**Example — subscribe to BTC price ticks:**
```json
{"action": "subscribe", "channel": "ticker", "symbol": "BTCUSDT"}
```

**Example — unsubscribe:**
```json
{"action": "unsubscribe", "channel": "ticker", "symbol": "BTCUSDT"}
```

---

## Subscription Cap

Each connection can hold a maximum of **10 active subscriptions**. Attempting to add an 11th subscription returns an error without closing the connection:

```json
{
  "type": "error",
  "code": "SUBSCRIPTION_LIMIT",
  "message": "Maximum of 10 subscriptions per connection reached."
}
```

To add more subscriptions beyond the cap, either unsubscribe from existing channels first or open a second WebSocket connection.

---

## Heartbeat

The server sends a ping every **30 seconds**. You must respond with a pong within **10 seconds** or the connection is closed.

> **Info:**
> This is an **application-level** heartbeat using JSON messages, not the WebSocket protocol-level ping/pong frames. Your client must send `{"action": "pong"}` as a text message — a WebSocket PONG frame is not sufficient.

**Server sends (every 30s):**
```json
{"type": "ping"}
```

**Client must respond within 10s:**
```json
{"action": "pong"}
```

If the server does not receive a pong within 10 seconds, it closes the connection and cleans up all subscriptions.

---

## Reconnection

On disconnect, reconnect with exponential back-off:

| Attempt | Wait before reconnect |
|---------|-----------------------|
| 1st reconnect | 1 second |
| 2nd reconnect | 2 seconds |
| 3rd reconnect | 4 seconds |
| 4th reconnect | 8 seconds |
| 5th+ | 60 seconds maximum |

After reconnecting, re-subscribe to all your previous channels. The server does not restore subscriptions automatically.

---

## WebSocket Error Messages

When an action fails, the server sends an error message and **does not close the connection**:

```json
{
  "type": "error",
  "code": "ERROR_CODE",
  "message": "Human-readable description."
}
```

### Error Codes

| Code | Cause | Resolution |
|------|-------|------------|
| `UNKNOWN_ACTION` | The `action` field is not `subscribe`, `unsubscribe`, or `pong` | Check the spelling and case of `action` |
| `INVALID_CHANNEL` | The `channel` field does not match any known channel | See [WebSocket Channels](/docs/websocket/channels) for valid names |
| `SUBSCRIPTION_LIMIT` | Attempted to add an 11th subscription | Unsubscribe from a channel before adding a new one |
| `INVALID_API_KEY` | Auth failed on connection (also sent as close code 4401) | Obtain a valid API key |

---

## Full Client Implementation

**Python SDK:**

The SDK's `AgentExchangeWS` handles connection, auth, heartbeat, reconnection, and subscriptions automatically:

```python
from agentexchange import AgentExchangeWS

ws = AgentExchangeWS(
    api_key="ak_live_...",
    base_url="ws://localhost:8000",
)

# Subscribe using decorators
@ws.on_ticker("BTCUSDT")
def handle_btc_price(msg):
    price = msg["data"]["price"]
    print(f"BTC: ${price}")

@ws.on_ticker("ETHUSDT")
def handle_eth_price(msg):
    price = msg["data"]["price"]
    print(f"ETH: ${price}")

@ws.on_order_update()
def handle_order(msg):
    order = msg["data"]
    print(f"Order {order['order_id']}: {order['status']}")

@ws.on_portfolio_update()
def handle_portfolio(msg):
    portfolio = msg["data"]
    print(f"Equity: ${portfolio['total_equity']}")

# Start streaming (blocks until stopped)
ws.run_forever()
```
**Python (raw websockets):**

```python
import asyncio
import json
import websockets

API_KEY = "ak_live_..."
WS_URL = f"ws://localhost:8000/ws/v1?api_key={API_KEY}"

async def main():
    reconnect_delay = 1

    while True:
        try:
            async with websockets.connect(WS_URL) as ws:
                print("Connected")
                reconnect_delay = 1  # Reset on successful connect

                # Subscribe to channels
                await ws.send(json.dumps({
                    "action": "subscribe",
                    "channel": "ticker",
                    "symbol": "BTCUSDT",
                }))
                await ws.send(json.dumps({
                    "action": "subscribe",
                    "channel": "orders",
                }))

                # Message loop
                async for raw_message in ws:
                    msg = json.loads(raw_message)

                    # Handle heartbeat
                    if msg.get("type") == "ping":
                        await ws.send(json.dumps({"action": "pong"}))
                        continue

                    # Handle errors
                    if msg.get("type") == "error":
                        print(f"Server error [{msg['code']}]: {msg['message']}")
                        continue

                    # Handle channel data
                    channel = msg.get("channel")
                    if channel == "ticker":
                        print(f"{msg['symbol']}: ${msg['data']['price']}")
                    elif channel == "orders":
                        order = msg["data"]
                        print(f"Order {order['order_id']}: {order['status']}")

        except websockets.exceptions.ConnectionClosedError as e:
            if e.code == 4401:
                print("Authentication failed. Check your API key.")
                return  # Do not reconnect

            print(f"Disconnected (code {e.code}). Reconnecting in {reconnect_delay}s...")

        except Exception as e:
            print(f"Error: {e}. Reconnecting in {reconnect_delay}s...")

        await asyncio.sleep(reconnect_delay)
        reconnect_delay = min(reconnect_delay * 2, 60)

asyncio.run(main())
```
**JavaScript:**

```javascript
const API_KEY = "ak_live_...";
const WS_URL = `ws://localhost:8000/ws/v1?api_key=${API_KEY}`;

let ws;
let reconnectDelay = 1000;
let subscriptions = [];

function connect() {
  ws = new WebSocket(WS_URL);

  ws.onopen = () => {
    console.log("Connected");
    reconnectDelay = 1000;

    // Resubscribe after reconnect
    subscriptions.forEach(sub => ws.send(JSON.stringify(sub)));
  };

  ws.onmessage = (event) => {
    const msg = JSON.parse(event.data);

    // Handle heartbeat
    if (msg.type === "ping") {
      ws.send(JSON.stringify({ action: "pong" }));
      return;
    }

    // Handle errors
    if (msg.type === "error") {
      console.error(`[${msg.code}] ${msg.message}`);
      return;
    }

    // Dispatch by channel
    switch (msg.channel) {
      case "ticker":
        console.log(`${msg.symbol}: $${msg.data.price}`);
        break;
      case "orders":
        console.log(`Order ${msg.data.order_id}: ${msg.data.status}`);
        break;
      case "portfolio":
        console.log(`Equity: $${msg.data.total_equity}`);
        break;
    }
  };

  ws.onclose = (event) => {
    if (event.code === 4401) {
      console.error("Authentication failed. Check your API key.");
      return; // Do not reconnect
    }
    console.log(`Disconnected. Reconnecting in ${reconnectDelay}ms...`);
    setTimeout(connect, reconnectDelay);
    reconnectDelay = Math.min(reconnectDelay * 2, 60000);
  };

  ws.onerror = (error) => {
    console.error("WebSocket error:", error);
  };
}

function subscribe(channel, params = {}) {
  const sub = { action: "subscribe", channel, ...params };
  subscriptions.push(sub);
  if (ws && ws.readyState === WebSocket.OPEN) {
    ws.send(JSON.stringify(sub));
  }
}

// Usage
connect();
subscribe("ticker", { symbol: "BTCUSDT" });
subscribe("orders");
subscribe("portfolio");
```

---

## Connection Limits

| Limit | Value |
|-------|-------|
| Max subscriptions per connection | 10 |
| Ping interval | 30 seconds |
| Pong timeout | 10 seconds |
| Auth failure close code | 4401 |

There is no documented limit on the number of simultaneous connections per API key, but avoid opening more connections than necessary.

---

## Related Pages

- [WebSocket Channels](/docs/websocket/channels) — channel reference (ticker, candles, orders, portfolio, battle)
- [Rate Limits](/docs/api/rate-limits) — WebSocket is not subject to HTTP rate limiting
- [Authentication](/docs/api/authentication) — obtaining API keys and JWT tokens
