Streaming
Live tickers, asset quotes, candles and source status over one authenticated WebSocket.
Connect
wss://api.cantoncap.cc/v1/streamOpen 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
| Channel | Delivers | Scope | Replay |
|---|---|---|---|
ticker:{market_id} | The market's latest ticker, sampled. | stream:ticker | 15 minutes |
asset_quote:{asset_id} | The asset's aggregate quotes, sampled. Same model as the REST quotes. | stream:asset_quote | 15 minutes |
candles:{market_id}:{interval} | Full candle replacements for the current and the previous two periods. | stream:candles | 7 days |
source_status:{venue_id} | The venue feed's availability, latest only. | stream:source_status | None |
trades:{market_id} | Approved public trades, in order. | stream:trades | 1 day |
orderbook:{market_id} | Full book snapshots and resynchronisation notices. | stream:orderbook | None |
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
{ "type": "subscribe", "request_id": "cc-usd", "channel": "asset_quote:ast_515c3eaa72bd4a8b9403297879d78ef6" }{
"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
{
"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
{ "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_exceedederror carryingretry_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 answersinvalid_requestand 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.
| Code | Meaning |
|---|---|
unauthenticated | The handshake had no valid X-API-Key. |
forbidden | The plan is inactive, or the key lacks the channel's scope. |
invalid_request | A malformed message or channel, or a duplicate subscription. |
protocol_error | A frame the server could not read. |
not_found | The channel's resource does not exist, such as an order book with no snapshot in 29 days. |
source_unavailable | A dependency is briefly unavailable. Retryable. |
rate_limited | Too many connections this minute. Retryable. |
quota_exceeded | Too many concurrent streams for the plan. Retryable. |
resume_unavailable | The resume token is unknown, expired or lost. |
connection_limit | The service is at its connection capacity. Retryable. |
subscription_limit | The connection has reached its subscription limit. |
payload_too_large | An event exceeded the wire limit. |
slow_consumer | The client did not keep up. |
shutdown | The 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.
{
"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.
The full message schemas are in the AsyncAPI reference at api.cantoncap.cc/stream/docs.