> ## Documentation Index
> Fetch the complete documentation index at: https://docs.stratalerts.com/llms.txt
> Use this file to discover all available pages before exploring further.

# WebSocket API

> Connect to the StratAlerts WebSocket API to receive pushed price updates, TFC state changes, and alert events as they happen — no polling required.

## Overview for Real-Time Streaming

The StratAlerts WebSocket API gives you a persistent, bidirectional connection to the same real-time data stream that powers the scanner. Instead of polling REST endpoints for the latest state, you open one connection and subscribe to the channels you need — price quotes, TFC state changes, in-force alerts, and simultaneous break alerts — then receive events pushed to you as they fire. Use the WebSocket API when your integration needs live data; use the REST API when you need historical snapshots or one-time lookups.

## Endpoint

All WebSocket connections go to a single endpoint:

```text theme={null}
wss://app.stratalerts.com/ws/market/v1
```

## Authentication

You authenticate at connection time by passing your API key in the HTTP upgrade request. The server validates the key before completing the WebSocket handshake — no separate auth message is needed after connecting.

Pass your key using either of these headers:

| Header          | Format                |
| --------------- | --------------------- |
| `Authorization` | `Bearer YOUR_API_KEY` |
| `X-API-Key`     | `YOUR_API_KEY`        |

Your key must have the `ws:connect` scope in addition to any channel-specific scopes. If the key is missing or invalid, the connection is rejected with close code `4401`. If the key lacks the required scopes, it is rejected with close code `4403`.

<Note>
  API keys are managed in your account at [app.stratalerts.com](https://app.stratalerts.com). Each key is issued with a specific set of scopes — if you cannot connect or subscribe to a channel, check that your key includes the required scope.
</Note>

## One connection per account

Each account may have only one active WebSocket connection at a time. If you open a second connection while an existing one is live, the server evicts the older connection by closing it with code `4409` before completing the new handshake. The new connection then proceeds normally.

Design your client to handle close code `4409` as a signal that it was replaced — typically this means you should not attempt to reconnect immediately, since a newer instance of your application is already connected.

## Usage metering

Each outbound WebSocket event is metered as a `ws_message` usage row on your account. This applies to all data events pushed to your connection — quotes, state changes, and alerts. Metered usage counts toward your API billing alongside REST request usage.

## Message envelope

Every message the server sends — including acknowledgments and data events — uses the same JSON envelope:

```json theme={null}
{
  "type": "event_type_here",
  "ts": "2026-04-10T14:35:00.123456+00:00",
  "seq": "42",
  "data": {}
}
```

| Field  | Type   | Description                                                                                   |
| ------ | ------ | --------------------------------------------------------------------------------------------- |
| `type` | string | Event type (e.g. `quote`, `alert.in_force`, `subscribed`)                                     |
| `ts`   | string | ISO 8601 server timestamp for this message                                                    |
| `seq`  | string | Monotonically increasing integer string; increments for every message sent on this connection |
| `data` | object | Event-specific payload                                                                        |

The `seq` field is a string representation of an integer that starts at `1` and increments with each message. You can use it to detect dropped messages if you are logging or buffering events.

## Subscribing to channels

After the connection is established, send a subscribe message to begin receiving events from one or more channels:

```json theme={null}
{
  "op": "subscribe",
  "topics": [
    { "channel": "quotes", "symbols": ["AAPL", "TSLA"] },
    { "channel": "states", "symbols": ["AAPL"] },
    { "channel": "alerts.in_force" },
    { "channel": "alerts.simultaneous_breaks" }
  ]
}
```

The server responds with a `subscribed` acknowledgment listing the channels it accepted:

```json theme={null}
{
  "type": "subscribed",
  "ts": "2026-04-10T14:35:00.123456+00:00",
  "seq": "1",
  "data": {
    "channels": ["quotes", "alerts.in_force"]
  }
}
```

Channels that require a symbols list (`quotes`, `states`) are subscribed per symbol. Channels that are account-wide (`alerts.in_force`, `alerts.simultaneous_breaks`) do not take a symbols list — once subscribed, you receive all events globally.

<Warning>
  `symbols` must be a JSON array of ticker strings — for example, `["AAPL", "TSLA"]`. If you pass a string such as `"AAPL"`, the server treats the payload as malformed and subscribes you to **zero** symbols on that topic. The channel still appears in the `subscribed` acknowledgment, but you will not receive any events until you resubscribe with a proper array. Always wrap symbols in an array, even when subscribing to one ticker.
</Warning>

<Note>
  If your key is missing the scope for a channel, that channel is silently omitted from the acknowledgment. Check the `channels` array in the response to confirm which subscriptions were accepted.
</Note>

## Unsubscribing

Send the same message format with `"op": "unsubscribe"` to stop receiving events from specific channels or symbols:

```json theme={null}
{
  "op": "unsubscribe",
  "topics": [
    { "channel": "quotes", "symbols": ["TSLA"] }
  ]
}
```

The server responds with an `unsubscribed` acknowledgment in the same envelope format.

## Close codes

| Code   | Meaning                                                                                       |
| ------ | --------------------------------------------------------------------------------------------- |
| `4401` | No API key provided or key could not be resolved                                              |
| `4403` | API key is valid but the account is not entitled or the key is missing the `ws:connect` scope |
| `4409` | Connection evicted — a new connection was opened for this account and replaced this one       |

## Reconnection

WebSocket connections can drop due to network interruptions, server restarts, or idle timeouts. Implement reconnection with exponential backoff in your client, and re-subscribe to all channels after each successful reconnect.

A basic backoff strategy:

* Start with a 1-second delay after the first disconnect
* Double the delay on each failed reconnect attempt
* Cap the delay at 60 seconds
* Reset the delay counter after a successful reconnect

<Warning>
  Do not reconnect immediately in a tight loop. Rapid reconnection attempts can exhaust your connection budget and delay recovery. Always use backoff.
</Warning>

## Complete examples

The examples below show a full connect → subscribe → receive loop. Replace `YOUR_API_KEY` with your actual key.

<CodeGroup>
  ```python Python theme={null}
  import asyncio
  import json
  import websockets

  API_KEY = "YOUR_API_KEY"
  WS_URL = "wss://app.stratalerts.com/ws/market/v1"


  async def connect_stratalerts():
      headers = {"Authorization": f"Bearer {API_KEY}"}

      async with websockets.connect(WS_URL, additional_headers=headers) as ws:
          # Subscribe to quotes for two symbols and all in-force alerts
          await ws.send(json.dumps({
              "op": "subscribe",
              "topics": [
                  {"channel": "quotes", "symbols": ["AAPL", "SPY"]},
                  {"channel": "alerts.in_force"},
              ]
          }))

          # Receive messages until the connection closes
          async for raw in ws:
              message = json.loads(raw)
              event_type = message.get("type")
              data = message.get("data", {})

              if event_type == "subscribed":
                  print(f"Subscribed to: {data.get('channels')}")
              elif event_type == "quote":
                  print(f"Quote  {data['symbol']}  ${data['price']}  vol {data['volume']}")
              elif event_type == "alert.in_force":
                  print(f"Alert  {data}")


  async def main():
      backoff = 1
      while True:
          try:
              await connect_stratalerts()
              backoff = 1  # reset on clean disconnect
          except websockets.ConnectionClosedError as exc:
              if exc.code == 4409:
                  print("Connection evicted by a newer session — not reconnecting.")
                  break
              print(f"Disconnected (code={exc.code}), retrying in {backoff}s...")
              await asyncio.sleep(backoff)
              backoff = min(backoff * 2, 60)
          except Exception as exc:
              print(f"Error: {exc}, retrying in {backoff}s...")
              await asyncio.sleep(backoff)
              backoff = min(backoff * 2, 60)


  asyncio.run(main())
  ```

  ```javascript JavaScript theme={null}
  const WebSocket = require("ws");

  const API_KEY = "YOUR_API_KEY";
  const WS_URL = "wss://app.stratalerts.com/ws/market/v1";

  function connect() {
    const ws = new WebSocket(WS_URL, {
      headers: { Authorization: `Bearer ${API_KEY}` },
    });

    ws.on("open", () => {
      // Subscribe to quotes for two symbols and all in-force alerts
      ws.send(JSON.stringify({
        op: "subscribe",
        topics: [
          { channel: "quotes", symbols: ["AAPL", "SPY"] },
          { channel: "alerts.in_force" },
        ],
      }));
    });

    ws.on("message", (raw) => {
      const message = JSON.parse(raw);
      const { type, data } = message;

      if (type === "subscribed") {
        console.log("Subscribed to:", data.channels);
      } else if (type === "quote") {
        console.log(`Quote  ${data.symbol}  $${data.price}  vol ${data.volume}`);
      } else if (type === "alert.in_force") {
        console.log("Alert", data);
      }
    });

    ws.on("close", (code) => {
      if (code === 4409) {
        console.log("Connection evicted by a newer session — not reconnecting.");
        return;
      }
      console.log(`Disconnected (code=${code}), reconnecting with backoff...`);
      reconnectWithBackoff();
    });

    ws.on("error", (err) => {
      console.error("WebSocket error:", err.message);
    });

    return ws;
  }

  let backoff = 1000;

  function reconnectWithBackoff() {
    setTimeout(() => {
      connect();
      backoff = Math.min(backoff * 2, 60000);
    }, backoff);
  }

  // Reset backoff on successful open
  const ws = connect();
  ws.on("open", () => { backoff = 1000; });
  ```
</CodeGroup>

## Related

<CardGroup cols={2}>
  <Card title="Channels" icon="list" href="/api/websocket/channels">
    Full reference for all four channels: required scopes, symbol subscriptions, and example event payloads.
  </Card>

  <Card title="Authentication" icon="key" href="/api/authentication">
    How to obtain an API key, understand scopes, and pass credentials in requests.
  </Card>
</CardGroup>
