Skip to content

Websocket Streams

Cantex streams live market data — ticker prices and OHLCV candles — over a public websocket. Like the rest of the Public API, it requires no authentication, no API key and no account, and can be used directly from a browser.

Network Websocket URL
Mainnet wss://api.cantex.io/v1/ws/public
Testnet wss://api.testnet.cantex.io/v1/ws/public

Quick start

A live price ticker in a browser console:

const ws = new WebSocket("wss://api.testnet.cantex.io/v1/ws/public");

ws.onopen = () => {
  ws.send(JSON.stringify({ op: "subscribe", channels: ["market.CC-USDCX.ticker"] }));
};

ws.onmessage = (event) => {
  const msg = JSON.parse(event.data);
  if (msg.op === "ping") return ws.send(JSON.stringify({ op: "pong" }));
  if (msg.type === "snapshot" || msg.type === "update") {
    console.log(`${msg.data.market}: ${msg.data.price}`);
  }
};

You'll receive the current price immediately (the snapshot), then a stream of updates as the price moves.

Discovering channels

Channel names follow two formats:

Stream Channel format Example
Ticker market.<SYMBOL>.ticker market.CC-USDCX.ticker
Candles market.<SYMBOL>.candles.<PERIOD> market.CC-USDCX.candles.60

<PERIOD> is the candle length in seconds: 60, 300, 600 or 3600 (1m, 5m, 10m, 1h).

Don't hard-code symbols — fetch /markets/info to get the live list of markets, the available candle periods and the exact channel names for each.

Market sources

Each market in /markets/info carries a source field telling you where its prices come from:

  • "cantex" — native Cantex markets (e.g. CC-USDCX, CBTC-CC). Prices are derived from Cantex's own on-chain liquidity pools, so the ticker and candles reflect actual trading activity on the exchange. These are the markets you can trade on Cantex.
  • "external" — reference price streams (BTC-USDC, ETH-USDC, CC-USDC). These republish prices from external markets for context and conversion — for example, valuing a CC amount in USD. They are not derived from Cantex pools and are not tradable markets on Cantex.

Both kinds are streamed over the same websocket with the same message formats. If you're charting Cantex trading activity, use the "cantex" markets; if you need a fiat reference rate alongside them, subscribe to the "external" streams too. The examples on this page use CC-USDCX, a native cantex market.

Symbols can contain dots

Some symbols contain a dot, e.g. FRXUSD.B-CC, giving channels like market.FRXUSD.B-CC.ticker. When parsing a channel name, take the fixed parts from the ends (market. prefix; .ticker or .candles.<PERIOD> suffix) and treat everything in between as the symbol — do not simply split on ..

Protocol

Open the websocket, then exchange JSON text messages. The client sends operations (op); the server sends acknowledgements, data messages and keepalive pings.

Subscribe

{ "op": "subscribe", "channels": ["market.CC-USDCX.ticker", "market.CC-USDCX.candles.60"] }

For candle channels you can optionally pass "limit" to control how many historical bars the initial snapshot contains (default 500):

{ "op": "subscribe", "channels": ["market.CC-USDCX.candles.300"], "limit": 100 }

The server acknowledges:

{ "op": "subscribed", "channels": ["market.CC-USDCX.ticker", "market.CC-USDCX.candles.60"], "total_subscriptions": 2 }

For each subscribed channel you then receive one snapshot message with the current state, followed by update messages as new data arrives. You can subscribe to many channels on a single connection, in one request or across several.

Unsubscribe

{ "op": "unsubscribe", "channels": ["market.CC-USDCX.candles.60"] }
{ "op": "unsubscribed", "channels": ["market.CC-USDCX.candles.60"], "total_subscriptions": 1 }

Data message envelope

Every data message has the same shape:

{
  "channel": "market.CC-USDCX.ticker",
  "type": "snapshot",
  "data": { "…": "…" },
  "ts": 1787723100038
}
  • channel — the channel the message belongs to, exactly as you subscribed to it.
  • type"snapshot" (current state, sent once on subscribe) or "update" (incremental).
  • data — the payload; shape depends on the channel type (see below).
  • ts — server send time, Unix milliseconds. Payloads carry their own event timestamps; use those for charting.

Ping / pong and errors

The server sends {"op": "ping"} roughly every 30 seconds as a keepalive; reply with {"op": "pong"} (or ignore it — but make sure your client library isn't treating server messages as errors). You can also send {"op": "ping"} yourself and the server replies {"op": "pong"}.

A malformed request (unknown op, or channels that isn't a list) returns:

{ "op": "error", "message": "Unknown op: subscrib" }

Ticker channel

market.<SYMBOL>.ticker streams the latest price for a market. The snapshot and each update carry the same payload:

{
  "channel": "market.CC-USDCX.ticker",
  "type": "update",
  "data": {
    "market": "CC-USDCX",
    "price": "0.118720",
    "ts": 1787723011334
  },
  "ts": 1787723013514
}
  • price is a decimal string — parse it with a decimal type, per the API-wide convention.
  • data.ts is the time the price was produced, Unix milliseconds.

Updates are pushed as prices move, typically every few seconds per market.

Candles channel

market.<SYMBOL>.candles.<PERIOD> streams OHLCV bars. On subscribe you receive a snapshot of recent history:

{
  "channel": "market.CC-USDCX.candles.300",
  "type": "snapshot",
  "data": {
    "period": 300,
    "bars": [
      {
        "open": 0.11812,
        "high": 0.11855,
        "low": 0.11804,
        "close": 0.11849,
        "volume": 31204.5,
        "start_ts": 1787722500000,
        "end_ts": 1787722800000
      },
      {
        "open": 0.11849,
        "high": 0.11888,
        "low": 0.11842,
        "close": 0.11872,
        "volume": 28677.2,
        "start_ts": 1787722800000,
        "end_ts": 1787723100000
      }
    ]
  },
  "ts": 1787723100038
}

Bars are ordered oldest → newest, up to the limit you passed on subscribe (default 500). After the snapshot, each update carries a single bar:

{
  "channel": "market.CC-USDCX.candles.300",
  "type": "update",
  "data": {
    "open": 0.11849,
    "high": 0.11888,
    "low": 0.11842,
    "close": 0.11872,
    "volume": 28677.2,
    "start_ts": 1787722800000,
    "end_ts": 1787723100000
  },
  "ts": 1787723100038
}
  • start_ts / end_ts bound the bar's period in Unix milliseconds; end_ts − start_ts equals the period. Key updates by start_ts: an update for a start_ts you already have replaces that bar (the current bar updates as trades happen); a new start_ts starts a new bar.
  • volume is denominated in the base token of the market.

Number encoding

Unlike the REST endpoints, candle values may arrive as JSON numbers rather than strings — they are chart data, not token amounts to transact with. Write your parser to accept both numbers and numeric strings for the OHLCV fields, and always treat ticker price as a string.

Reconnection

Subscriptions do not survive a disconnect. When the connection drops:

  1. Reconnect (with a small backoff — a second or two is fine).
  2. Re-send your subscribe requests.

Because every subscription starts with a snapshot, you recover the full current state automatically — there is no separate recovery or replay mechanism to implement, and no gap-filling to worry about for tickers. For candles, the snapshot's historical bars cover anything you missed while disconnected.

Gotchas

  • The subscribed ack echoes your request, not what was accepted. Channels that don't match the expected formats are silently ignored but still appear in the acknowledgement. If you subscribe and receive an ack but no snapshot ever arrives, check the channel name against /markets/info.
  • A valid-looking channel for a nonexistent market is also silent. Subscribing to market.NOPE-USD.ticker succeeds and acks, but no data will ever arrive. Again: build channel names from /markets/info rather than by hand.
  • Ticker snapshots can be absent. For a market that hasn't published a price yet you may get updates without an initial snapshot. Treat the first message on a ticker channel as the current state whether its type is snapshot or update.

Client etiquette

Use one connection with many subscriptions, not one connection per channel — a single connection comfortably carries every market's ticker and candle streams. Unsubscribe from channels you no longer need, and close the connection when your application is done with it.

Examples

const WS_URL = "wss://api.testnet.cantex.io/v1/ws/public";
const CHANNELS = ["market.CC-USDCX.ticker", "market.CC-USDCX.candles.300"];

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

  ws.onopen = () => {
    ws.send(JSON.stringify({ op: "subscribe", channels: CHANNELS, limit: 100 }));
  };

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

    if (msg.op === "ping") {
      ws.send(JSON.stringify({ op: "pong" }));
      return;
    }
    if (msg.op === "subscribed" || msg.op === "unsubscribed") return;

    if (msg.channel?.endsWith(".ticker")) {
      console.log("price", msg.data.market, msg.data.price);
    } else if (msg.type === "snapshot") {
      console.log("candles history", msg.channel, msg.data.bars.length, "bars");
    } else if (msg.type === "update") {
      console.log("candle", msg.channel, msg.data.close);
    }
  };

  // Reconnect and resubscribe on disconnect.
  ws.onclose = () => setTimeout(connect, 2000);
}

connect();

Requires the websockets package (pip install websockets).

import asyncio
import json

import websockets

WS_URL = "wss://api.testnet.cantex.io/v1/ws/public"
CHANNELS = ["market.CC-USDCX.ticker", "market.CC-USDCX.candles.300"]


async def main():
    while True:  # reconnect loop
        try:
            async with websockets.connect(WS_URL) as ws:
                await ws.send(json.dumps({"op": "subscribe", "channels": CHANNELS, "limit": 100}))

                async for raw in ws:
                    msg = json.loads(raw)

                    if msg.get("op") == "ping":
                        await ws.send(json.dumps({"op": "pong"}))
                        continue
                    if "op" in msg:  # subscribed / unsubscribed / error acks
                        continue

                    channel, data = msg["channel"], msg["data"]
                    if channel.endswith(".ticker"):
                        print(f"{data['market']}: {data['price']}")
                    elif msg["type"] == "snapshot":
                        print(f"{channel}: {len(data['bars'])} historical bars")
                    else:
                        print(f"{channel}: close={data['close']} volume={data['volume']}")
        except (websockets.ConnectionClosed, OSError):
            await asyncio.sleep(2)  # then reconnect and resubscribe


asyncio.run(main())

Quick interactive testing with wscat (npm install -g wscat):

wscat -c wss://api.testnet.cantex.io/v1/ws/public

Then paste a subscribe request:

{"op": "subscribe", "channels": ["market.CC-USDCX.ticker"]}

Messages stream to your terminal; press Ctrl+C to disconnect.