Skip to content
Docs
Get API Access
Browse the Documentation

Streaming

Live tickers, asset quotes, candles and source status over one authenticated WebSocket.

Connect

Text
wss://api.cantoncap.cc/v1/stream

Open one WebSocket with your key in the X-API-Key header of the handshake, then subscribe to channels over it. A browser's WebSocket API cannot set headers, so connect from a server and relay to your clients; this also keeps the key private.

import WebSocket from "ws"; // npm install ws

const socket = new WebSocket("wss://api.cantoncap.cc/v1/stream", {
  headers: { "X-API-Key": process.env.CANTONCAP_API_KEY },
});

socket.on("open", () => {
  socket.send(JSON.stringify({
    type: "subscribe",
    request_id: "cc-usd",
    channel: "asset_quote:ast_515c3eaa72bd4a8b9403297879d78ef6",
  }));
});

socket.on("message", (raw) => {
  const frame = JSON.parse(raw.toString());
  switch (frame.type) {
    case "subscribed":
      console.log("subscribed", frame.subscription_id, frame.delivery);
      break;
    case "event":
      console.log(frame.channel, frame.event_id, frame.data.quotes.USD?.price);
      break;
    case "heartbeat":
      break;
    case "error":
      console.error(frame.code, frame.message, frame.retryable);
      break;
  }
});

socket.on("close", (code, reason) => console.log("closed", code, reason.toString()));

Channels

ChannelDeliversScopeReplay
ticker:{market_id}The market's latest ticker, sampled.stream:ticker15 minutes
asset_quote:{asset_id}The asset's aggregate quotes, sampled. Same model as the REST quotes.stream:asset_quote15 minutes
candles:{market_id}:{interval}Full candle replacements for the current and the previous two periods.stream:candles7 days
source_status:{venue_id}The venue feed's availability, latest only.stream:source_statusNone
trades:{market_id}Approved public trades, in order.stream:trades1 day
orderbook:{market_id}Full book snapshots and resynchronisation notices.stream:orderbookNone

Candle intervals are 1m, 5m, 10m, 15m, 30m, 1h, 4h, 1d, 7d and 1w. Keys sold on cantoncap.cc carry the ticker, asset quote, candle and source status scopes; the trade and order-book channels are not included by default (write to api@cantoncap.cc). Subscribing without the scope answers a forbidden error for that subscription.

Messages

Subscribe and the Acknowledgement

JSON
{ "type": "subscribe", "request_id": "cc-usd", "channel": "asset_quote:ast_515c3eaa72bd4a8b9403297879d78ef6" }
JSON
{
  "type": "subscribed",
  "request_id": "cc-usd",
  "subscription_id": "sub_4f1c9a2e7b",
  "channel": "asset_quote:ast_515c3eaa72bd4a8b9403297879d78ef6",
  "schema_version": "1.0",
  "delivery": "sampled_state",
  "replay": { "mode": "best_effort", "max_age_ms": 900000 },
  "heartbeat_interval_ms": 15000,
  "poll_interval_ms": 1000
}

request_id is yours (1 to 128 letters, digits, _ or -) and is echoed back. The acknowledgement states, once, how the channel is delivered, how far back it can replay, and the heartbeat and polling intervals; read them from it rather than assuming values.

Events

JSON
{
  "type": "event",
  "subscription_id": "sub_4f1c9a2e7b",
  "channel": "asset_quote:ast_515c3eaa72bd4a8b9403297879d78ef6",
  "event_id": "evt_01JA7Q2KX8",
  "data": {
    "asset_id": "ast_515c3eaa72bd4a8b9403297879d78ef6",
    "quotes": {
      "USD": {
        "price": "0.1584",
        "market_cap": null,
        "fdv": "715482911.36",
        "volume_24h": "1840221.55",
        "volume": { "method": "trades", "coverage": "complete", "change_24h_pct": "7.31" },
        "price_changes_pct": { "1h": "0.21", "24h": "1.25", "7d": "4.12", "30d": null },
        "last_updated_at": "2026-10-11T14:00:00.000Z"
      }
    }
  }
}

data has the same model as the matching REST response, with the same exact decimal strings. Keep the last event_id of each channel if you want to resume.

Heartbeats, Unsubscribing and Errors

JSON
{ "type": "heartbeat", "observed_at": "2026-10-11T14:00:15.000Z" }

{ "type": "unsubscribe", "request_id": "stop-1", "subscription_id": "sub_4f1c9a2e7b" }
{ "type": "unsubscribed", "request_id": "stop-1", "subscription_id": "sub_4f1c9a2e7b" }

{
  "type": "error",
  "request_id": "cc-usd",
  "subscription_id": null,
  "code": "forbidden",
  "message": "The API key is not authorized for this channel",
  "retryable": false
}

The server sends a heartbeat at least every 60 seconds; the exact interval is in each acknowledgement. If nothing at all arrives for 90 seconds, treat the connection as dead and reconnect.

Limits

  • Each connection counts as one request against your per-minute window and holds one of your plan's concurrent streams for as long as it is open. Opening more connections than the plan allows is refused with a retryable quota_exceeded error carrying retry_after_ms. See Plans and limits.
  • One connection carries many subscriptions (up to 100 by default); beyond that, subscription_limit. Subscribing twice to the same channel on one connection answers invalid_request and does not use a slot.
  • Client messages are at most 64 KiB.
  • A client that reads too slowly is disconnected with slow_consumer.

Errors and Close Codes

An error that ends the connection (a bad key, an inactive plan, a quota) is sent as an error frame and the socket is then closed with code 1013 when it is retryable or 1008 when it is not, with the error code as the close reason.

CodeMeaning
unauthenticatedThe handshake had no valid X-API-Key.
forbiddenThe plan is inactive, or the key lacks the channel's scope.
invalid_requestA malformed message or channel, or a duplicate subscription.
protocol_errorA frame the server could not read.
not_foundThe channel's resource does not exist, such as an order book with no snapshot in 29 days.
source_unavailableA dependency is briefly unavailable. Retryable.
rate_limitedToo many connections this minute. Retryable.
quota_exceededToo many concurrent streams for the plan. Retryable.
resume_unavailableThe resume token is unknown, expired or lost.
connection_limitThe service is at its connection capacity. Retryable.
subscription_limitThe connection has reached its subscription limit.
payload_too_largeAn event exceeded the wire limit.
slow_consumerThe client did not keep up.
shutdownThe stream service is shutting down. Reconnect.

Resume and Recover

To pick up after a reconnect, subscribe with resume_from_event_id set to the last event ID you processed on that channel. Replay is best effort: it is bounded by age (the acknowledgement's max_age_ms) and capacity, and lost when the server restarts. Order books and source status do not accept a resume token.

JSON
{
  "type": "subscribe",
  "request_id": "cc-usd",
  "channel": "asset_quote:ast_515c3eaa72bd4a8b9403297879d78ef6",
  "resume_from_event_id": "evt_01JA7Q2KX8"
}

When the answer is resume_unavailable, recover from REST: subscribe without a token, buffer incoming events, fetch an overlapping range or snapshot over REST, then merge and de-duplicate (trades by trade_id, candles by market, interval and open_time; a ticker or quote is simply replaced). For order books, discard your local book on a resynchronisation notice and wait for the next full snapshot.

Sampled, Not Every Tick
Tickers and asset quotes are sampled state: with a one-second poll, intermediate values can be coalesced. Candle updates replace a whole candle and can still correct a recent period; corrections older than the last three periods are only visible over REST.

The full message schemas are in the AsyncAPI reference at api.cantoncap.cc/stream/docs.

Esc
navigateopenEscclose