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

# Real-time Data

> Subscribe to live updates on markets, orders, trades, odds, and scores using the SX Bet WebSocket API.

## Overview

SX Bet's WebSocket API delivers real-time updates on orderbook changes, trade executions, market status, and live scores. All channels are powered by [Centrifugo](https://centrifugal.dev/) and require an API key. Centrifugo provides [official client SDKs](https://centrifugal.dev/docs/transports/client_sdk) for JavaScript, Python, Go, Dart, Swift, Java, and C#.

Rather than polling REST endpoints, subscribe to the channels relevant to your workflow. The recommended pattern for most use cases is: **fetch current state via REST, then subscribe to stay updated** — this avoids gaps between your initial snapshot and the live feed.

<Steps>
  <Step title="Authenticate">
    Pass your API key via the `getToken` callback. Token refresh is handled automatically.
  </Step>

  <Step title="Connect">
    Create a `Centrifuge` client pointed at the WebSocket URL.
  </Step>

  <Step title="Subscribe">
    Create a subscription for each channel you need and attach a publication handler.
  </Step>
</Steps>

***

## Getting started

### Authenticate

To connect, you need a realtime token from the relayer. Fetch it from `/user/realtime-token/api-key` and pass your API key in the `x-api-key` header. In the client SDK, provide a `getToken` callback that returns this token.

The SDK calls `getToken` again whenever the current token expires, so passing a function instead of a static token is all that's needed to keep the connection alive. See [Common failures → Auth](#auth) for how to signal permanent auth failure vs. a transient fetch error.

### Connect

Create one `Centrifuge` client per process and reuse it for all subscriptions. All channel subscriptions are multiplexed over the same connection.

<CodeGroup>
  ```javascript JavaScript theme={null}
  import { Centrifuge } from "centrifuge";

  const RELAYER_URL = "https://api.sx.bet"; // Mainnet — use https://api.toronto.sx.bet for testnet
  const WS_URL = "wss://realtime.sx.bet/connection/websocket"; // Mainnet — use wss://realtime.toronto.sx.bet/connection/websocket for testnet

  async function fetchToken(apiKey) {
    const res = await fetch(`${RELAYER_URL}/user/realtime-token/api-key`, {
      headers: { "x-api-key": apiKey },
    });
    if (!res.ok) throw new Error(`Token endpoint returned ${res.status}`);
    const { token } = await res.json();
    return token;
  }

  const client = new Centrifuge(WS_URL, {
    getToken: () => fetchToken(YOUR_API_KEY),
  });

  client.connect();
  ```

  ```python Python theme={null}
  import asyncio
  import os
  import requests
  from centrifuge import Client

  RELAYER_URL = "https://api.sx.bet"  # Mainnet — use https://api.toronto.sx.bet for testnet
  WS_URL = "wss://realtime.sx.bet/connection/websocket"  # Mainnet — use wss://realtime.toronto.sx.bet/connection/websocket for testnet

  def fetch_token():
      res = requests.get(
          f"{RELAYER_URL}/user/realtime-token/api-key",
          headers={"x-api-key": os.environ["SX_API_KEY"]},
      )
      if not res.ok:
          raise Exception(f"Token endpoint returned {res.status_code}")
      return res.json()["token"]

  async def main():
      client = Client(WS_URL, get_token=fetch_token)
      await client.connect()
      await asyncio.Future()  # keep running

  asyncio.run(main())
  ```
</CodeGroup>

<Tip>All channel subscriptions are multiplexed over the single connection. If you need more than 512 subscriptions, create additional client instances — each connection supports up to 512 channels.</Tip>

### Subscribe

Use `client.newSubscription(channel, options)` to create a subscription, attach event handlers, then call `.subscribe()`:

If you need at-least-once delivery across reconnects, pass `positioned: true` and `recoverable: true` together for channels that support recovery. See [Recovery & reliability](#recovery-%26-reliability) for details.

<CodeGroup>
  ```javascript JavaScript theme={null}
  const sub = client.newSubscription("markets:global");

  sub.on("publication", (ctx) => {
    console.log(ctx.data);
  });

  sub.subscribe();
  ```

  ```python Python theme={null}
  from centrifuge import Client, PublicationContext, SubscriptionEventHandler

  async def on_publication(ctx: PublicationContext) -> None:
      print(ctx.data)

  handler = SubscriptionEventHandler(on_publication=on_publication)
  sub = client.new_subscription("markets:global", handler)
  await sub.subscribe()
  ```
</CodeGroup>

For the full Centrifugo client SDK specification, see [Client SDK specification](https://centrifugal.dev/docs/transports/client_api).

***

## Channels

| Channel                             | What you receive                                            | Payload reference                                                                    |
| ----------------------------------- | ----------------------------------------------------------- | ------------------------------------------------------------------------------------ |
| `active_orders:{maker}`             | Your orders being filled, cancelled, or posted              | [Active Order Updates →](/api-reference/centrifugo-active-order-updates)             |
| `order_book:market_{marketHash}`    | All order changes for a specific market                     | [Order Book Updates →](/api-reference/centrifugo-order-book-updates)                 |
| `order_book:event_{sportXEventId}`  | All order changes for every market in an event              | [Order Book Updates →](/api-reference/centrifugo-order-book-updates)                 |
| `best_odds:global`                  | Best available odds changes across all markets              | [Best Odds →](/api-reference/centrifugo-best-odds)                                   |
| `recent_trades:global`              | Global feed of all exchange trades                          | [Trade Updates →](/api-reference/centrifugo-trade-updates)                           |
| `recent_trades_consolidated:global` | Consolidated trade feed — one entry per fill, not per order | [Consolidated Trade Updates →](/api-reference/centrifugo-consolidated-trade-updates) |
| `markets:global`                    | Market status changes, suspension, settlement               | [Market Updates →](/api-reference/centrifugo-market-updates)                         |
| `main_line:global`                  | Main line shifts on spread and totals markets               | [Line Changes →](/api-reference/centrifugo-line-changes)                             |
| `fixtures:global`                   | Fixture metadata updates                                    | [Live Score & Fixture Updates →](/api-reference/centrifugo-live-score-updates)       |
| `fixtures:live_scores`              | Live match scores                                           | [Live Score Updates →](/api-reference/centrifugo-live-score-updates)                 |
| `ce_refunds:{bettor}`               | Chain enforcer refund events for your address               | [CE Refund Events →](/api-reference/centrifugo-ce-refund-events)                     |
| `parlay_markets:global`             | Incoming parlay RFQ requests                                | [Parlay Market Requests →](/api-reference/centrifugo-parlay-market-requests)         |

For channel name changes from the legacy Ably API, see [WebSocket Channels → Migration Guide](/api-reference/centrifugo-overview#migration-guide).

***

## Recovery & reliability

### Enabling recovery

Pass options to `newSubscription` to control reliability behavior:

| Flag          | Type      | Description                                                                                                                                                                       |
| ------------- | --------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `positioned`  | `boolean` | Enables stream position tracking for the subscription. This lets the client keep its current offset and allows the server to signal when the stream position has become invalid.  |
| `recoverable` | `boolean` | Enables automatic recovery for the subscription. On resubscribe, the client sends its last known stream position and the server tries to replay missed publications from history. |

**Both flags are required together to get at-least-once delivery across reconnects.** Think of it in two steps: `positioned` bookmarks your place in the stream while you're connected; `recoverable` uses that bookmark to fetch the missed pages when you come back.

<CodeGroup>
  ```javascript JavaScript theme={null}
  // Correct — both flags together
  const sub = client.newSubscription("order_book:market_abc123", {
    positioned: true,
    recoverable: true,
  });
  ```

  ```python Python theme={null}
  from centrifuge import SubscriptionOptions

  # Correct — both flags together
  options = SubscriptionOptions(positioned=True, recoverable=True)
  sub = client.new_subscription("order_book:market_abc123", handler, options)
  ```
</CodeGroup>

For channels without history where you always re-seed from REST on reconnect (e.g., `best_odds:global`, `parlay_markets:global`), both flags can be omitted.

### Interpreting subscribed after reconnect

After a reconnect, the `subscribed` event fires with context that tells you whether your local state is still consistent:

The `subscribed` context includes:

* `wasRecovering`: the client attempted to recover from a previous stream position
* `recovered`: the server successfully replayed all missed publications
* `positioned`: the subscription has stream position tracking enabled
* `recoverable`: the subscription supports automatic recovery

<CodeGroup>
  ```javascript JavaScript theme={null}
  sub.on("subscribed", (ctx) => {
    if (ctx.wasRecovering && ctx.recovered) {
      // Reconnected and gap was filled via history replay.
      // No need to re-fetch from REST — all missed messages were replayed.
    } else if (ctx.wasRecovering && !ctx.recovered) {
      // Reconnected but history was pruned before recovery could complete.
      // Too much time passed — re-seed your local state from REST.
    } else {
      // Fresh connect (first connection, or after a clean disconnect).
      // Seed initial state from REST, then rely on the subscription for updates.
    }
  });
  ```

  ```python Python theme={null}
  from centrifuge import SubscribedContext, SubscriptionEventHandler

  async def on_subscribed(ctx: SubscribedContext) -> None:
      if ctx.was_recovering and ctx.recovered:
          # Reconnected and gap was filled via history replay.
          # No need to re-fetch from REST — all missed messages were replayed.
          pass
      elif ctx.was_recovering and not ctx.recovered:
          # Reconnected but history was pruned before recovery could complete.
          # Too much time passed — re-seed your local state from REST.
          pass
      else:
          # Fresh connect (first connection, or after a clean disconnect).
          # Seed initial state from REST, then rely on the subscription for updates.
          pass

  handler = SubscriptionEventHandler(on_subscribed=on_subscribed)
  ```
</CodeGroup>

| `wasRecovering` | `recovered` | State         | What to do                                           |
| --------------- | ----------- | ------------- | ---------------------------------------------------- |
| `true`          | `true`      | Recovered     | History replay filled the gap — no action needed     |
| `true`          | `false`     | Unrecovered   | History was pruned — re-seed from REST               |
| `false`         | —           | Fresh connect | First connection or clean reconnect — seed from REST |

### Delivery guarantees

For namespaces with history enabled, Centrifugo provides **at-least-once delivery** within the recovery window — missed messages are replayed from server-side history on reconnect. The recovery window is 5 minutes, but may be shorter if the namespace's message cap is reached first. After the window expires, `wasRecovering: true, recovered: false` fires and you must re-seed from REST.

`best_odds` and `parlay_markets` do not have history enabled — recovery is not available on those channels.

**Note on epoch:** Recovery can fail even after a short disconnect if the server no longer has the missed publications in history or if the saved stream position is no longer valid. This is rare, but when it happens `recovered` will be `false` and you must re-seed from REST.

### Dedup

At-least-once delivery means a message may occasionally be replayed more than once during recovery. Every publication includes a `messageId` in `ctx.tags` — use it to deduplicate on the client side:

<CodeGroup>
  ```javascript JavaScript theme={null}
  const seen = new Set();
  const MAX_SEEN = 10_000;

  sub.on("publication", (ctx) => {
    const id = ctx.tags?.messageId;
    if (id !== undefined) {
      if (seen.has(id)) return;
      seen.add(id);
      if (seen.size > MAX_SEEN) {
        seen.delete(seen.values().next().value);
      }
    }
    applyUpdate(ctx.data);
  });
  ```

  ```python Python theme={null}
  from collections import OrderedDict
  from centrifuge import PublicationContext, SubscriptionEventHandler

  MAX_SEEN = 10_000
  seen: OrderedDict[str, None] = OrderedDict()

  async def on_publication(ctx: PublicationContext) -> None:
      msg_id = (ctx.tags or {}).get("messageId")
      if msg_id is not None:
          if msg_id in seen:
              return
          seen[msg_id] = None
          if len(seen) > MAX_SEEN:
              seen.popitem(last=False)
      apply_update(ctx.data)

  handler = SubscriptionEventHandler(on_publication=on_publication)
  ```
</CodeGroup>

For long-running processes, bound the size of your dedup set (e.g., keep only the last 1,000 IDs) to avoid unbounded memory growth.

### History

Each namespace with history enabled maintains a server-side log of recent publications. You can fetch this directly with `sub.history()` — useful for seeding initial state or auditing recent activity without a separate REST call.

<CodeGroup>
  ```javascript JavaScript theme={null}
  const resp = await sub.history({ limit: 50 });
  for (const pub of resp.publications) {
    applyUpdate(pub.data);
  }
  ```

  ```python Python theme={null}
  resp = await sub.history(limit=50)
  for pub in resp.publications:
      apply_update(pub.data)
  ```
</CodeGroup>

#### Parameters

| Parameter | Type                                | Description                                                                                                                             |
| --------- | ----------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- |
| `limit`   | `number`                            | Max publications to return. `0` returns only the current stream position (no publications). Omit for all history up to the server cap.  |
| `since`   | `{ offset: number, epoch: string }` | Start from a known stream position. Useful for paginating through history. Omit to start from the beginning (forward) or end (reverse). |
| `reverse` | `boolean`                           | `false` (default) = oldest first. `true` = newest first.                                                                                |

The response contains a `publications` array and the current stream `offset` and `epoch`. Each entry in `publications` has `data`, `offset`, `tags`, and `info` fields — access the payload via `pub.data`, the same as `ctx.data` in a live publication event.

#### Limits

History fetches are bounded by the per-namespace caps in [Namespace history capabilities](/api-reference/centrifugo-overview#namespace-history-capabilities) and the global limit of 1,000 items per request. Calling `sub.history()` on a channel with no history enabled returns error code `108`.

### Snapshot + subscribe pattern

Subscribe with `positioned: true, recoverable: true` and seed from REST inside the `subscribed` handler. The handler fires on every connect and tells you whether recovery filled the gap — so you only hit REST when you actually need to:

<CodeGroup>
  ```javascript JavaScript theme={null}
  let ready = false;
  const buffer = [];

  const sub = client.newSubscription(`order_book:market_${marketHash}`, {
    positioned: true,
    recoverable: true,
  });

  sub.on("publication", (ctx) => {
    if (!ready) {
      buffer.push(ctx.data);
    } else {
      applyUpdate(ctx.data);
    }
  });

  sub.on("subscribed", async (ctx) => {
    if (ctx.wasRecovering && ctx.recovered) {
      // Centrifugo replayed all missed messages — no REST call needed.
      ready = true;
      return;
    }

    // Fresh connect or failed recovery (history gap too large / expired).
    // Reset and re-seed from REST.
    ready = false;
    buffer.length = 0;

    const res = await fetch(`https://api.sx.bet/orders?marketHashes=${marketHash}`);
    applySnapshot(await res.json());

    // Drain buffered publications on top of the snapshot.
    // applyUpdate deduplicates by entity ID so any
    // overlap between the snapshot and the buffer is handled safely.
    for (const data of buffer) applyUpdate(data);
    buffer.length = 0;
    ready = true;
  });

  sub.subscribe();
  client.connect();
  ```

  ```python Python theme={null}
  import asyncio
  import aiohttp
  from centrifuge import (
      Client,
      PublicationContext,
      SubscribedContext,
      SubscriptionEventHandler,
      SubscriptionOptions,
  )

  async def subscribe_order_book(client: Client, market_hash: str) -> None:
      ready = False
      buffer: list = []

      async def on_subscribed(ctx: SubscribedContext) -> None:
          nonlocal ready
          if ctx.was_recovering and ctx.recovered:
              ready = True
              return

          ready = False
          buffer.clear()

          async with aiohttp.ClientSession() as session:
              async with session.get(
                  f"https://api.sx.bet/orders?marketHashes={market_hash}"
              ) as res:
                  apply_snapshot(await res.json())

          for data in buffer:
              apply_update(data)
          buffer.clear()
          ready = True

      async def on_publication(ctx: PublicationContext) -> None:
          if not ready:
              buffer.append(ctx.data)
          else:
              apply_update(ctx.data)

      handler = SubscriptionEventHandler(
          on_subscribed=on_subscribed,
          on_publication=on_publication,
      )
      options = SubscriptionOptions(positioned=True, recoverable=True)
      sub = client.new_subscription(
          f"order_book:market_{market_hash}", handler, options
      )
      await sub.subscribe()
  ```
</CodeGroup>

See [Fetching Odds → Real-time best odds](/developers/fetching-odds#real-time-best-odds) for a worked example applying this pattern to the `best_odds:global` channel.

***

## Connection & Subscription Lifecycle

The client connection and each subscription have separate lifecycles. The key rule is:

* `connecting` and `subscribing` are non-terminal states. They fire on the initial connect or subscribe and also on automatic retry paths.
* `disconnected` and `unsubscribed` are terminal states for automatic retry.

### Client lifecycle

The client connection moves through these states:

* `disconnected -> connecting -> connected`: initial connect
* `connected -> connecting -> connected`: retryable disconnect, then successful reconnect
* `connecting/connected -> disconnected`: terminal disconnect

Use the client events to understand what happened:

* `connecting`: fired on the initial `connect()` and on retryable reconnects. The event includes a `code` and `reason`.
* `connected`: fired when the transport is established and the client is ready.
* `disconnected`: fired only when the client reaches terminal `disconnected` state. After this, the SDK will not reconnect automatically.
* `error`: fired for internal errors that do not necessarily cause a state transition, such as transport errors during initial connect or reconnect, or connection token refresh errors.

```javascript theme={null}
client.on("connecting", (ctx) => {
  console.log("connecting", ctx);
});

client.on("connected", (ctx) => {
  console.log("connected", ctx);
});

client.on("disconnected", (ctx) => {
  console.log("disconnected", ctx);
});

client.on("error", (ctx) => {
  console.error("client error", ctx);
});
```

To reconnect after a terminal disconnect, call `client.connect()` explicitly.

### Subscription lifecycle

Each client-side subscription moves through its own state machine:

* `unsubscribed -> subscribing -> subscribed`: initial subscribe
* `subscribed -> subscribing -> subscribed`: retryable interruption, reconnect, or resubscribe
* `subscribing/subscribed -> unsubscribed`: terminal subscription stop

Use subscription events to understand what happened:

* `subscribing`: fired on the initial `subscribe()` and on retryable resubscribe paths.
* `subscribed`: fired when the subscription becomes active.
* `unsubscribed`: fired only when the subscription reaches terminal `unsubscribed` state. After this, the SDK will not resubscribe automatically.
* `publication`: fired whenever a new message arrives on the subscription while it is active.
* `error`: fired for internal subscription errors that do not necessarily cause a state transition, such as temporary subscribe errors or subscription token related errors.

```javascript theme={null}
sub.on("subscribing", (ctx) => {
  console.log("subscribing", ctx);
});

sub.on("subscribed", (ctx) => {
  console.log("subscribed", ctx);
});

sub.on("unsubscribed", (ctx) => {
  console.log("unsubscribed", ctx);
});

sub.on("publication", (ctx) => {
  console.log("publication", ctx);
});

sub.on("error", (ctx) => {
  console.error("subscription error", ctx);
});
```

To start a terminally unsubscribed subscription again, call `sub.subscribe()` explicitly.

### Retryable vs terminal conditions

In practice, this means:

* temporary transport loss moves the client back to `connecting`
* reconnectable subscription interruptions move the subscription back to `subscribing`
* calling `client.disconnect()` or hitting a terminal disconnect condition moves the client to `disconnected`
* calling `sub.unsubscribe()` or hitting a terminal subscription condition moves the subscription to `unsubscribed`

Recovery outcome is reported on the next `subscribed` event via `wasRecovering` and `recovered`. See [Recovery & reliability](#recovery--reliability) for how to interpret those fields.

***

## Examples

### Consume a global feed

<CodeGroup>
  ```javascript JavaScript theme={null}
  import { Centrifuge } from "centrifuge";

  const client = new Centrifuge("wss://realtime.sx.bet/connection/websocket", {
    getToken: () => fetchToken(YOUR_API_KEY), // see Getting started above
  });

  const sub = client.newSubscription("markets:global");

  sub.on("publication", (ctx) => {
    for (const market of ctx.data) {
      console.log(`${market.marketHash}: status=${market.status}`);
    }
  });

  sub.subscribe();
  client.connect();
  ```

  ```python Python theme={null}
  import asyncio
  from centrifuge import Client, PublicationContext, SubscriptionEventHandler

  async def on_publication(ctx: PublicationContext) -> None:
      for market in ctx.data:
          print(f"{market['marketHash']}: status={market['status']}")

  async def main():
      client = Client(
          "wss://realtime.sx.bet/connection/websocket",
          get_token=fetch_token,
      )
      await client.connect()
      handler = SubscriptionEventHandler(on_publication=on_publication)
      sub = client.new_subscription("markets:global", handler)
      await sub.subscribe()
      await asyncio.Future()  # keep running

  asyncio.run(main())
  ```
</CodeGroup>

### Maintain a recoverable order book

Use `positioned: true` and `recoverable: true` together, then handle the three subscription states to maintain consistent local state across reconnects:

<CodeGroup>
  ```javascript JavaScript theme={null}
  async function subscribeOrderBook(client, marketHash) {
    const sub = client.newSubscription(`order_book:market_${marketHash}`, {
      positioned: true,
      recoverable: true,
    });

    sub.on("subscribed", async (ctx) => {
      if (!ctx.wasRecovering || (ctx.wasRecovering && !ctx.recovered)) {
        // Fresh connect or failed recovery — seed state from REST
        const orders = await fetchOrdersFromRest(marketHash);
        applySnapshot(orders);
      }
      // wasRecovering && recovered: history replay handled it, nothing to do
    });

    sub.on("publication", (ctx) => {
      applyUpdate(ctx.data);
    });

    sub.subscribe();
  }
  ```

  ```python Python theme={null}
  from centrifuge import (
      Client,
      PublicationContext,
      SubscribedContext,
      SubscriptionEventHandler,
      SubscriptionOptions,
  )

  async def subscribe_order_book(client: Client, market_hash: str) -> None:
      async def on_subscribed(ctx: SubscribedContext) -> None:
          if not ctx.was_recovering or (ctx.was_recovering and not ctx.recovered):
              # Fresh connect or failed recovery — seed state from REST
              orders = await fetch_orders_from_rest(market_hash)
              apply_snapshot(orders)
          # was_recovering and recovered: history replay handled it, nothing to do

      async def on_publication(ctx: PublicationContext) -> None:
          apply_update(ctx.data)

      handler = SubscriptionEventHandler(
          on_subscribed=on_subscribed,
          on_publication=on_publication,
      )
      options = SubscriptionOptions(positioned=True, recoverable=True)
      sub = client.new_subscription(f"order_book:market_{market_hash}", handler, options)
      await sub.subscribe()
  ```
</CodeGroup>

### Monitor your active orders

Subscribe to `active_orders:{maker}` to receive fills, cancellations, and new posts for your address in real time:

<CodeGroup>
  ```javascript JavaScript theme={null}
  import { Centrifuge } from "centrifuge";

  const client = new Centrifuge("wss://realtime.sx.bet/connection/websocket", {
    getToken: () => fetchToken(YOUR_API_KEY),
  });

  const sub = client.newSubscription(`active_orders:${YOUR_ADDRESS}`, {
    positioned: true,
    recoverable: true,
  });

  sub.on("subscribed", async (ctx) => {
    if (!ctx.wasRecovering || (ctx.wasRecovering && !ctx.recovered)) {
      // Seed current open orders from REST on fresh connect or failed recovery
      const orders = await fetchActiveOrdersFromRest(YOUR_ADDRESS);
      applySnapshot(orders);
    }
  });

  sub.on("publication", (ctx) => {
    for (const update of ctx.data) {
      console.log(`order ${update.orderHash}: ${update.status}`);
      applyUpdate(update);
    }
  });

  sub.subscribe();
  client.connect();
  ```

  ```python Python theme={null}
  import asyncio
  from centrifuge import (
      Client,
      PublicationContext,
      SubscribedContext,
      SubscriptionEventHandler,
      SubscriptionOptions,
  )

  async def main():
      client = Client(
          "wss://realtime.sx.bet/connection/websocket",
          get_token=fetch_token,
      )

      async def on_subscribed(ctx: SubscribedContext) -> None:
          if not ctx.was_recovering or (ctx.was_recovering and not ctx.recovered):
              orders = await fetch_active_orders_from_rest(YOUR_ADDRESS)
              apply_snapshot(orders)

      async def on_publication(ctx: PublicationContext) -> None:
          for update in ctx.data:
              print(f"order {update['orderHash']}: {update['status']}")
              apply_update(update)

      handler = SubscriptionEventHandler(
          on_subscribed=on_subscribed,
          on_publication=on_publication,
      )
      options = SubscriptionOptions(positioned=True, recoverable=True)
      sub = client.new_subscription(f"active_orders:{YOUR_ADDRESS}", handler, options)
      await client.connect()
      await sub.subscribe()
      await asyncio.Future()  # keep running

  asyncio.run(main())
  ```
</CodeGroup>

***

## Common failures

In most cases, you do not need to write custom retry logic around these errors. The SDK already handles reconnect and resubscribe automatically when the condition is retryable. The codes below are most useful for telemetry, debugging, and contacting support if an issue persists.

### Auth

The `getToken` callback is called on initial connect and whenever the token needs to be refreshed. How you throw from it controls what the SDK does next:

<CodeGroup>
  ```javascript JavaScript theme={null}
  import { Centrifuge, UnauthorizedError } from "centrifuge";

  const client = new Centrifuge(WS_URL, {
    getToken: async () => {
      const res = await fetch(`${RELAYER_URL}/user/realtime-token/api-key`, {
        headers: { "x-api-key": apiKey },
      });
      if (res.status === 401 || res.status === 403) {
        throw new UnauthorizedError(); // permanent — stops all reconnect attempts
      }
      if (!res.ok) throw new Error(`Status ${res.status}`); // transient — retries with backoff
      const { token } = await res.json();
      return token;
    },
  });
  ```

  ```python Python theme={null}
  import os
  import requests
  from centrifuge import Client, UnauthorizedError

  def fetch_token():
      res = requests.get(
          f"{RELAYER_URL}/user/realtime-token/api-key",
          headers={"x-api-key": os.environ["SX_API_KEY"]},
      )
      if res.status_code in (401, 403):
          raise UnauthorizedError()  # permanent — stops all reconnect attempts
      if not res.ok:
          raise Exception(f"Status {res.status_code}")  # transient — retries with backoff
      return res.json()["token"]

  client = Client(WS_URL, get_token=fetch_token)
  ```
</CodeGroup>

If your realtime-token endpoint returns `401` or `403`, throw `UnauthorizedError` so the connection stops retrying and moves to terminal `disconnected`. For transient failures like `429` or `5xx`, throw a normal error so the SDK keeps retrying.

The server may also issue a terminal auth disconnect such as code `3500` (`"invalid token"`). In that case, the client stops reconnecting automatically.

### Subscribe errors

Retryable subscription errors emit the subscription `error` event. Terminal subscription errors move the subscription to `unsubscribed`.

<CodeGroup>
  ```javascript JavaScript theme={null}
  sub.on("error", (ctx) => {
    console.error(ctx.error.code, ctx.error.message);
  });
  ```

  ```python Python theme={null}
  from centrifuge import SubscriptionErrorContext, SubscriptionEventHandler

  async def on_error(ctx: SubscriptionErrorContext) -> None:
      print(ctx.error.code, ctx.error.message)

  handler = SubscriptionEventHandler(on_error=on_error)
  ```
</CodeGroup>

| Code  | Meaning                                               | What happens next                                                                 |
| ----- | ----------------------------------------------------- | --------------------------------------------------------------------------------- |
| `100` | Internal server error                                 | The subscription stays in `subscribing` and the SDK retries.                      |
| `101` | Unauthorized                                          | The subscription moves to terminal `unsubscribed`.                                |
| `102` | Unknown channel                                       | The subscription moves to terminal `unsubscribed`.                                |
| `103` | Permission denied                                     | The subscription moves to terminal `unsubscribed`.                                |
| `106` | Limit exceeded — connection is at the 512 channel cap | The subscription moves to terminal `unsubscribed`.                                |
| `109` | Token expired                                         | The subscription stays in `subscribing`; the SDK refreshes the token and retries. |
| `111` | Too many requests                                     | The subscription stays in `subscribing` and the SDK retries.                      |

For the full list of built-in client error codes, see [Centrifugo client protocol codes](https://centrifugal.dev/docs/server/codes).

### Recovery lost / insufficient state

If Centrifugo detects that recovery cannot continue from the current stream position, it may either resubscribe the affected subscription or reconnect the client, depending on where the problem is detected. This can surface as unsubscribe code `2500` or disconnect code `3010`, both with reason `"insufficient state"`.

This is not terminal by itself. The next `subscribed` event tells you whether the replay succeeded:

* `wasRecovering: true, recovered: true`: replay filled the gap
* `wasRecovering: true, recovered: false`: replay could not fill the gap, so re-seed from REST

If you see `insufficient state` frequently, it usually indicates a stream continuity problem rather than a client bug.

### Terminal disconnects

The client reconnects automatically after most disconnects. It does **not** reconnect for built-in terminal disconnect codes in the `3500-3999` range.

Common terminal examples include:

* `3500` `invalid token`
* `3501` `bad request`
* `3503` `force disconnect`
* `3507` `permission denied`

For the full list of built-in disconnect codes, see [Centrifugo client protocol codes](https://centrifugal.dev/docs/server/codes).

### Slow consumer

The server buffers up to 1 MB per connection. If your `publication` handler is slow, that buffer fills faster than it drains and the server closes the connection. In Centrifugo this can surface as disconnect code `3008` (`"slow"`), which is reconnectable but indicates your consumer cannot keep up.

Keep handlers fast: receive the message and hand it off to a queue or async task immediately. Unexpected disconnects that are not auth-related are often caused by a saturated buffer.

For the full list of built-in unsubscribe and disconnect codes, see [Centrifugo client protocol codes](https://centrifugal.dev/docs/server/codes).

## Related

<CardGroup cols={2}>
  <Card title="Market Making →" icon="chart-line" href="/developers/market-making">
    Using active\_orders to monitor your open orders in real-time.
  </Card>

  <Card title="Filling Orders →" icon="bolt" href="/developers/filling-orders">
    How to submit fills and monitor your trade history.
  </Card>

  <Card title="WebSocket Initialization →" icon="plug" href="/api-reference/centrifugo-initialization">
    Connecting and subscribing with the Centrifuge client.
  </Card>

  <Card title="Market Making Parlays →" icon="layer-group" href="/developers/market-making-parlays">
    Responding to parlay RFQ requests via parlay\_markets:global.
  </Card>
</CardGroup>
