# CryHub Market Data API > Real-time and historical market data over REST and WebSocket. Covers crypto > spot, perpetual futures, and tokenized equities, ETFs, commodities, indices > and FX. One token authenticates both surfaces. Every price is converted > server-side to the requested quote currency, so no client-side FX is needed. This file is a condensed reference for AI agents and automated clients. Human documentation: https://cryhub.io/en/docs ## Endpoints REST https://api.cryhub.io/rest/v1 header: Authorization: Bearer WebSocket wss://api.cryhub.io/ws?token= Health https://api.cryhub.io/health plain-text "ok", HTTP 200 Tokens are exactly 32 alphanumeric characters. Health lives at the domain root, NOT under /rest/v1 (that path 404s). ## Sources and quote currencies s1 spot feed REST quotes: usdt, tmn, aed, eur WebSocket quotes: usdt, tmn s2 perp feed REST quotes: usdc, tmn WebSocket quotes: usdc, tmn - AED and EUR are REST-only. Subscribing to an aed/eur channel succeeds and then never emits a frame. Use REST for those quotes. s2 supports neither anywhere. - Fiat quotes are converted server-side, rates refreshed every 4 seconds. ## Symbols REST `symbols`/`symbol` take BASE ASSETS only; a WebSocket {pair} is base+quote. REST symbols=btc,eth,xrp -> response symbol: "btcusdt", "ethusdt" WebSocket s1.btcusdt.ticker -> pair: btcusdt, btctmn, btcusdc, ... On s2, eight builder-deployed perp DEXes extend the main perp universe. Their markets are addressed with a - prefix, because the same coin name can exist on several DEXes: dexes: xyz, flx, vntl, hyna, km, abcd, cash, para REST symbols=xyz-tsla,cash-meta -> "xyz-tslausdc", "cash-metausdc" WebSocket s2.xyz-tslausdc.ticker A hyphen in `symbol` is the reliable way to tell DEX rows from main-perp rows. Mixed lists are allowed in one request. Only currently-listed assets return data; the listing refreshes hourly. ## Response envelope (REST) { "success": true, "message": "ok", "ts": , "next_cursor": , "data": ... } - `success` is present on success only; it is OMITTED on errors. Use the HTTP status code as the primary error signal, `message` for the human reason. - `next_cursor` appears on kline and trade only, when `data` is non-empty. It is the OLDEST item timestamp in the page — pass it as `end_time` for the previous page. /market/detail never returns it. - IMPORTANT: numeric fields equal to zero may be OMITTED ENTIRELY from any REST payload or WebSocket tick. Always treat a missing numeric field as 0. ## GET /market/kline params: source, symbols, quote, slice, [size=500, 1..2000], [start_time], [end_time] slice: 1min 5min 15min 30min 60min 4hour 1day 1week 1mon Query modes: A size only -> NEWEST-FIRST (initial chart load) B size + end_time -> oldest-first (backward pagination) C size + start_time -> oldest-first (forward window) D start_time + end_time -> oldest-first (server caps at 2000) CRITICAL: Mode A returns newest-first. Reverse the array before charting. Mode B pages arrive oldest-first and can be prepended directly. Stop paginating when data.length < size. Candle: { symbol, slice, ts, open, close, high, low, vol, amount, count } vol = quote-asset volume (converted to the requested quote) amount = base-asset volume (coin units, never converted) TIMESTAMP CONVENTION DIFFERS PER SOURCE: s1: ts is the candle OPEN time, period-aligned. s2: ts is the candle CLOSE boundary minus 1 ms. Use ts consistently as the bar key. Never assume ts % interval === 0 on s2. slice=1year is not supported by REST on either source (HTTP 400). Yearly candles exist only as the WebSocket channel s1.{pair}.kline.1y. With several symbols all candles arrive in one flat array — group by `symbol`. `next_cursor` is the global minimum ts, so reuse one end_time for all symbols. ## GET /market/detail Same params and query modes as kline; one entry per candle per symbol. Adds `change` (close - open) and `change_percent`. No next_cursor — treat as a snapshot. Typical call: slice=1day&size=1 for current price + 24h stats. Item: { symbol, price, change, high, low, volume_quote, change_percent, volume, count } ## GET /market/ticker params: source, symbols, quote. 24-hour rolling window, no pagination. Item: { symbol, open, close, high, low, vol, amount, count, bid, bid_size, ask, ask_size } ## GET /market/trade params: source, symbol (SINGLE base symbol), quote, [size=500], [start_time], [end_time] Batch: { symbol, id, ts, data: [{ price, amount, direction, ts }] } direction: "buy" (taker bought) | "sell" (taker sold) Time filtering is applied in memory over roughly the 2000 most recent batches, so cursor pagination is bounded to that window. For continuous history use the WebSocket trade channel. ## GET /market/depth params: source, symbol (single), quote, [step=step0] steps: step0 raw | step1 1dp | step2 2dp | step3 integer | step4 nearest 10 | step5 nearest 100 Response: { symbol, ts, version, bids: [{price, qty}], asks: [{price, qty}] } bids descending by price, asks ascending. `version` detects stale snapshots. NOTE: REST depth returns {price, qty} OBJECTS; the WebSocket depth channel returns [price, qty] ARRAYS. Map accordingly. ## GET /market/top-mover params: source, currency, type currency s1: usdt, tmn s2: usdc, tmn type gainer | loser | volume This endpoint takes `currency`, NOT `quote`. Recomputed every ~3 seconds, up to 25 symbols, pre-sorted. gainer/loser sort by change_percent, volume by quote_volume. Item: { symbol, price, change, quote_volume, change_percent, volume } On s2 the ranking pools main-perp and builder-DEX coins into one list; filter on a hyphen in `symbol` to separate them. ## WebSocket protocol Connect: wss://api.cryhub.io/ws?token=<32 chars> Welcome: { "msg": "Connected to Cryhub Stream", "ts": } Client -> server: { "action": "sub", "channels": ["s1.btcusdt.ticker"] } { "action": "unsub", "channels": ["s1.btcusdt.ticker"] } { "action": "pong", "data": { "pong": "" } } Server -> client frames carry: ch, tick, ts (data) or msg, ping, ts (system). Subscribe ACK echoes the channels comma-joined with msg:"sub". HEARTBEAT: the server pings every 30 seconds and closes the connection after 3 missed pongs. Echo the exact timestamp back, and the pong value MUST BE A STRING, not a number. Channel format: {source}.{pair}.{stream}[.{arg}] {source}.{currency}.global.top_mover.{type} Channels and delivery: s1.{pair}.ticker always-on s2.{pair}.ticker always-on, 0.01% price-delta gate (~1-3s cadence) s1.{pair}.detail always-on s2.{pair}.detail always-on s1.{pair}.bbo always-on s2.{pair}.bbo lazy (shares its upstream feed with depth) s1.{pair}.trade always-on s2.{pair}.trade lazy s1.{pair}.kline.{tf} lazy tf: 1m 5m 15m 30m 60m 4h 1d 1w 1mo 1y s2.{pair}.kline.{tf} lazy tf: same, EXCEPT 1y (rejected at subscribe) s1.{pair}.depth.{agg} lazy agg: agg0..agg5 (maps 1:1 to REST step0..step5) s2.{pair}.depth.{agg} lazy all agg levels share one upstream feed s1.{currency}.global.top_mover.{type} always-on currency: usdt, tmn s2.{currency}.global.top_mover.{type} always-on currency: usdc, tmn "lazy" = the upstream connection opens on the first subscriber and is released after the last unsubscribes, so the first frame can take ~200 ms. ## WebSocket tick shapes kline { symbol, ts, open, high, low, close, vol, amount, count } Each frame is the CURRENT IN-PROGRESS candle. Update in place when tick.ts equals the current bar's ts, append when greater. ts is in MILLISECONDS — divide by 1000 for chart libraries that expect seconds. trade { trades: [{ symbol, price, amount, direction, ts }] } Prepend to your list and trim it to bound memory. ticker { symbol, open, high, low, close, bid, ask, last_price, vol, amount, count, bid_size, ask_size, last_size } detail { symbol, price, change, high, low, volume_quote, change_percent, volume } Field names match REST /market/detail; 24-hour window. bbo { symbol, bid, ask, bid_size, ask_size } Lightweight spread indicator — use instead of ticker when you only need the spread. depth { symbol, bids: [[price, qty]], asks: [[price, qty]] } Every frame is a FULL SNAPSHOT, not a delta. Replace the local book. top_mover tick is an ARRAY, and it is the ONLY channel family using short keys: [{ sym, p, c, qv, prt, v }] sym=symbol, p=price, c=change, qv=quote volume, prt=change percent, v=base volume. Every other channel uses the long-form names above — do not add short-key fallbacks elsewhere. ## Errors Error responses return HTTP 4xx/5xx and omit `success`: { "message": "start_time must be earlier than end_time", "ts": } 400 cannot combine size with both start_time and end_time 400 invalid size: N, accepted range 1 to 2000 400 start_time must be earlier than end_time 400 source X is not supported (valid: s1, s2) 400 quote not supported for this source (e.g. aed on s2) 400 invalid slice for this provider: "1year" 400 invalid step "step6", accepted: step0..step5 400 type "X" is not supported; valid values: gainer, loser, volume 400 symbols is required 401 unauthorized (missing or malformed token) 500 symbol not found: (unknown or delisted) WebSocket errors arrive as a bare msg frame with no `ch`: invalid action: | channels are empty | invalid source: | invalid symbol: | failed to sub: ... | ping timeout. connection closed. ## Gotcha checklist 1. Reverse Mode A kline results before charting. 2. Treat every missing numeric field as 0. 3. Send the pong value as a string. 4. s2 kline ts is the close boundary minus 1 ms, not the open time. 5. top-mover takes `currency`, every other endpoint takes `quote`. 6. REST depth is {price,qty}; WebSocket depth is [price,qty]. 7. Depth frames are full snapshots — replace, never merge. 8. Only top_mover ticks are arrays and use short field names. 9. AED/EUR work on REST but never emit over WebSocket. 10. Health is at the domain root, not under /rest/v1.