Overview
The Qalypto WebSocket API provides real-time, validated market data from multiple cryptocurrency exchanges. Stream trades, tickers, klines, and orderbook data with sub-second latency.
Supported Exchanges
Data Coverage
Data Delivery Rates
All data is maintained server-side — we handle reconnections, gap detection, and state management. You receive clean, validated data without worrying about exchange quirks.
| Data Type | Delivery | Notes |
|---|---|---|
| trade | Real-time | Every trade pushed immediately |
| ticker | Real-time | Every update pushed immediately |
| kline | Real-time | Live maintained candles with every update |
| orderbook | Sampled | Maintained L2 snapshot every 0.2s or 100 updates |
Authentication
All API requests require authentication using Cloudflare Access Service Tokens. You will receive your credentials when your account is created.
Required Headers
Keep your credentials secure. Do not share them or commit them to version control. If you believe your credentials have been compromised, contact support immediately.
ClickHouse Connection
Connect to ClickHouse for historical data queries using SQL:
| Host | clickhouse.qalypto.com |
| Port | 8443 (HTTPS) |
| Database | market_data |
| Username | <your-username> |
| Password | <your-password> |
TLS/SSL encryption is required for all connections. Unencrypted connections are not supported.
1import requests23# ClickHouse HTTP API Connection4HOST = "https://clickhouse.qalypto.com:8443"5USER = "<your-username>"6PASSWORD = "<your-password>"78def query(sql):9 response = requests.get(10 HOST,11 params={'query': sql},12 auth=(USER, PASSWORD),13 verify=True14 )15 response.raise_for_status()16 return response.text1718# Query recent trades19result = query('''20 SELECT21 timestamp,22 exchange,23 symbol,24 price,25 amount,26 side27 FROM market_data.market_data_trade28 WHERE timestamp >= now() - INTERVAL 1 HOUR29 ORDER BY timestamp DESC30 LIMIT 10031 FORMAT TabSeparated32''')3334for line in result.strip().split('\n'):35 print(line)
Database Tables
Four main tables store all market data:
market_data_trade
| Column | Type |
|---|---|
| timestamp | DateTime64(3) |
| exchange | String |
| symbol | String |
| price | Float64 |
| amount | Float64 |
| side | String |
| trade_id | String |
| is_liquidation | UInt8 |
market_data_ticker
| Column | Type |
|---|---|
| timestamp | DateTime64(3) |
| exchange | String |
| symbol | String |
| last_price | Float64 |
| bid_price, ask_price | Nullable(Float64) |
| bid_size, ask_size | Nullable(Float64) |
| high_24h, low_24h, volume_24h | Nullable(Float64) |
| quote_volume_24h | Nullable(Float64) |
| open_24h | Nullable(Float64) |
| funding_rate | Nullable(Float64) — Perpetual funding rate |
| next_funding_time | Nullable(DateTime64) — Next settlement |
| mark_price, index_price | Nullable(Float64) |
| open_interest | Nullable(Float64) — Contracts |
| open_interest_value | Nullable(Float64) — USD value |
market_data_kline
| Column | Type |
|---|---|
| timestamp | DateTime64(3) |
| exchange | String |
| symbol | String |
| interval | String |
| open, high, low, close | Float64 |
| volume | Float64 |
| quote_volume | Nullable(Float64) |
| num_trades | Nullable(Int32) |
| close_time | Nullable(DateTime64(3)) |
| is_final | UInt8 |
market_data_orderbook
| Column | Type |
|---|---|
| timestamp | DateTime64(3) |
| exchange | String |
| symbol | String |
| bids_json, asks_json | String (JSON) |
| bids_price_0, asks_price_0 | Nullable(Float64) |
| bids_size_0, asks_size_0 | Nullable(Float64) |
| is_snapshot | UInt8 |
| sequence_id | String |
Note on Nullable Fields
Some fields may be NULL depending on the exchange. For example, quote_volume_24h and open_24h are not available from all exchanges.
WebSocket-Only Fields
WebSocket messages include additional fields not present in the ClickHouse tables:
_timestamps.websocket_sent— Epoch ms when the WebSocket server dispatched the message to your connection._timestamps.exchange_event_time— Epoch ms when the event occurred at the exchange (parsed fromdata.timestamp). Use this for end-to-end latency calculation.producer_received— Top-level epoch ms when our producer first received the data from the exchange.
Latency calculations:
# Internal pipeline latency (producer → websocket server) internal_latency = _timestamps.websocket_sent - producer_received # Typical: P50 ~85ms | P95 ~180ms # End-to-end latency (exchange event → websocket server) e2e_latency = _timestamps.websocket_sent - _timestamps.exchange_event_time # Typical: P50 ~180ms | P95 ~400ms (OKX may be higher)
Query Examples
Common SQL queries for market data analysis:
OHLCV Data for Backtesting
1-- Get OHLCV data for backtesting2SELECT3 timestamp,4 exchange,5 symbol,6 interval,7 open,8 high,9 low,10 close,11 volume12FROM market_data.market_data_kline13WHERE symbol IN ('BTCUSDT', 'ETHUSDT', 'SOLUSDT')14 AND interval = '1h'15 AND exchange = 'binance'16 AND is_final = 1 -- only finalized candles (no live updates)17 AND timestamp >= now() - INTERVAL 7 DAY18ORDER BY timestamp ASC
Cross-Exchange Price Comparison
1-- Cross-exchange price comparison2SELECT3 exchange,4 avg(last_price) as avg_price,5 max(last_price) as max_price,6 min(last_price) as min_price,7 count() as updates8FROM market_data.market_data_ticker9WHERE symbol IN ('BTCUSDT', 'ETHUSDT')10 AND timestamp >= now() - INTERVAL 5 MINUTE11GROUP BY exchange, symbol12ORDER BY avg_price DESC
WebSocket Connection
Connect to the WebSocket API using the following endpoint:
Parameters
| Parameter | Type | Description |
|---|---|---|
| client_id | string | Unique identifier for your connection (e.g., your username) |
Connection Examples
1import asyncio2import websockets3import json45async def connect():6 uri = "wss://watchdog.qalypto.com/ws/stream/my-client-001"7 headers = {8 "CF-Access-Client-Id": "<your-client-id>",9 "CF-Access-Client-Secret": "<your-client-secret>"10 }1112 async with websockets.connect(uri, extra_headers=headers,13 ping_interval=None, ping_timeout=None) as ws:14 # Subscribe to channels15 await ws.send(json.dumps({16 "action": "subscribe",17 "channels": ["trade", "ticker", "kline"]18 }))1920 # Receive messages21 async for message in ws:22 msg = json.loads(message)23 # Handle server heartbeat24 if msg.get("type") == "ping":25 await ws.send(json.dumps({"action": "pong"}))26 continue27 data = msg.get("data", {})28 print(f"[{data.get('exchange')}] {data.get('symbol')}: {data}")2930asyncio.run(connect())
Connection Heartbeat (Important!)
Similar to Binance, Bybit, and OKX, our WebSocket server implements a heartbeat mechanism to ensure connection stability:
You will receive: {"type": "ping", "timestamp": 1706450000000}
Send: {"action": "pong"}
After 60 seconds without pong or 3 missed pongs, the server disconnects the client.
if msg.get("type") == "ping":
await ws.send(json.dumps({"action": "pong"}))
Subscription
After connecting, send a subscription message to start receiving data:
1{2 "action": "subscribe",3 "channels": ["trade", "ticker", "kline", "orderbook"]4}
Actions
| Action | Description |
|---|---|
| subscribe | Subscribe to one or more channels |
| unsubscribe | Unsubscribe from channels |
| ping | Client-initiated health check (returns pong) |
| pong | Response to server ping (required for heartbeat) |
| status | Get connection status |
1{2 "action": "unsubscribe",3 "channels": ["orderbook"]4}
Client → Server Messages
1{2 "action": "ping"3}
1{2 "action": "pong"3}
1{2 "action": "status"3}
Server → Client Messages
1{2 "type": "connected",3 "client_id": "my-client-001",4 "message": "Welcome to Qalypto Real-Time Data Stream",5 "available_subscriptions": [6 "trade", "ticker", "orderbook", "kline",7 "trade:btcusdt", "trade:ethusdt", "ticker:*", "*"8 ],9 "connection_guidelines": {10 "heartbeat": {11 "ping_interval_seconds": 20,12 "pong_timeout_seconds": 60,13 "description": "Server sends ping every 20s. Client must respond with pong within 60s."14 },15 "rate_limits": {16 "max_control_messages_per_second": 5,17 "max_subscriptions_per_connection": 5018 },19 "actions": ["subscribe", "unsubscribe", "ping", "pong", "status"]20 }21}
1{2 "type": "subscribed",3 "channels": ["trade", "ticker", "kline", "orderbook"]4}
Server Ping (Heartbeat)
Server sends this every 20 seconds. You MUST respond with pong!
1{2 "type": "ping",3 "timestamp": 1706450000000,4 "server_time": "2026-01-28T12:53:40.123Z"5}
1{2 "type": "pong_ack",3 "timestamp": "2026-01-28T12:53:40.456Z"4}
1{2 "type": "status",3 "client_id": "my-client-001",4 "subscriptions": ["trade", "ticker", "kline", "orderbook"],5 "messages_received": 12847,6 "connected_since": "2026-01-28T12:00:00.000Z"7}
1{2 "error": "Rate limit exceeded. Max 5 control messages per second."3}
Reconnection Handling
The server does not persist subscriptions after disconnect. Implement automatic reconnection with exponential backoff:
1import asyncio2import websockets3import json4from datetime import datetime56class QalyptoClient:7 def __init__(self, username, cf_client_id, cf_client_secret):8 self.uri = f"wss://watchdog.qalypto.com/ws/stream/{username}"9 self.headers = {10 "CF-Access-Client-Id": cf_client_id,11 "CF-Access-Client-Secret": cf_client_secret12 }13 self.channels = ["trade", "ticker"]14 self.reconnect_delay = 115 self.max_reconnect_delay = 601617 async def connect(self):18 while True:19 try:20 async with websockets.connect(21 self.uri,22 extra_headers=self.headers,23 ping_interval=None, # Disable library pings - we use server's JSON heartbeat24 ping_timeout=None25 ) as ws:26 print(f"[{datetime.now()}] Connected")27 self.reconnect_delay = 1 # Reset on success2829 # Subscribe30 await ws.send(json.dumps({31 "action": "subscribe",32 "channels": self.channels33 }))3435 async for message in ws:36 await self.handle_message(json.loads(message), ws)3738 except Exception as e:39 print(f"[{datetime.now()}] Error: {e}")40 print(f"Reconnecting in {self.reconnect_delay}s...")41 await asyncio.sleep(self.reconnect_delay)42 self.reconnect_delay = min(43 self.reconnect_delay * 2,44 self.max_reconnect_delay45 )4647 async def handle_message(self, msg, ws):48 msg_type = msg.get("type")4950 # IMPORTANT: Respond to server pings to keep connection alive51 if msg_type == "ping":52 await ws.send(json.dumps({"action": "pong"}))53 return5455 # Handle market data56 data = msg.get("data", {})57 if msg_type in ["trade", "ticker", "kline", "orderbook"]:58 print(f"[{msg_type}] {data.get('exchange')}: {data.get('price', data.get('last_price', 'N/A'))}")5960# Usage61client = QalyptoClient("<your-username>", "<your-client-id>", "<your-client-secret>")62asyncio.run(client.connect())
Channels
Subscribe to different channels to receive specific types of market data:
Data Type Channels
| trade | All trade data from all exchanges |
| ticker | All ticker data from all exchanges |
| kline | All kline/candlestick data |
| orderbook | All orderbook snapshots |
Symbol-Specific Channels
| trade:btcusdt | Trades for BTCUSDT (also: ethusdt, solusdt, xrpusdt, bnbusdt, dogeusdt, adausdt, dotusdt, linkusdt, avaxusdt) |
| ticker:ethusdt | Ticker for ETHUSDT (also: btcusdt, solusdt, xrpusdt, bnbusdt, dogeusdt, adausdt, dotusdt, linkusdt, avaxusdt) |
| kline:1m:solusdt | 1-minute klines for SOLUSDT (any symbol + interval) |
| orderbook:xrpusdt | Orderbook for XRPUSDT (also: btcusdt, ethusdt, solusdt, bnbusdt, dogeusdt, adausdt, dotusdt, linkusdt, avaxusdt) |
Kline Intervals
Wildcard
Use * to subscribe to all data types.
Message Format
All messages are JSON objects with the following structure:
1{2 "type": "trade", // Data type3 "symbol": "BTCUSDT", // Trading pair4 "timestamp": "2026-...", // Server timestamp (ISO 8601)5 "data": { ... }, // Payload6 "producer_received": 17708... // Epoch ms — when producer received from exchange7}
Timestamps
All timestamps set by Qalypto are UTC. This includes:
producer_receivedEpoch ms (float) — when our producer first received the data from the exchange._timestamps.websocket_sentEpoch ms (int) — when the WebSocket server dispatched the message to your connection._timestamps.exchange_event_timeEpoch ms (int) — when the event occurred at the exchange. Use for end-to-end latency.
Internal latency: websocket_sent - producer_received (P50 ~85ms)
E2E latency: websocket_sent - exchange_event_time (P50 ~180ms)
Trade Data
Trade messages contain individual trade executions:
1{2 "type": "trade",3 "symbol": "SOLUSDT",4 "timestamp": "2026-03-02T14:01:45.300327",5 "data": {6 "_timestamps": {7 "websocket_sent": 1772460105301,8 "exchange_event_time": 17724601051009 },10 "timestamp": "2026-03-02 14:01:45.100000",11 "exchange": "binance",12 "symbol": "SOLUSDT",13 "price": 82.98,14 "amount": 0.75,15 "side": "sell",16 "trade_id": "3198735563",17 "is_liquidation": false18 },19 "producer_received": 1772460105224.047620}
Ticker Data
Ticker messages contain current price and volume information:
1{2 "type": "ticker",3 "symbol": "SOLUSDT",4 "timestamp": "2026-03-02T14:01:45.309347",5 "data": {6 "_timestamps": {7 "websocket_sent": 1772460105313,8 "exchange_event_time": 17724601051389 },10 "timestamp": "2026-03-02 14:01:45.138000",11 "exchange": "bybit",12 "symbol": "SOLUSDT",13 "last_price": 82.98,14 "bid_price": 82.98,15 "ask_price": 82.99,16 "bid_size": 312.7,17 "ask_size": 455.4,18 "high_24h": 86.84,19 "low_24h": 81.63,20 "volume_24h": 15414549.2,21 "quote_volume_24h": 1292880761.203,22 "open_24h": null,23 "funding_rate": -5.129e-05,24 "next_funding_time": "2026-03-02 16:00:00+00:00",25 "mark_price": 82.989,26 "index_price": 83.051,27 "open_interest": 6893141.2,28 "open_interest_value": null29 },30 "producer_received": 1772460105223.475631}
Kline Data
Kline (candlestick) messages contain OHLCV data. Note: interval is also available as a top-level field for convenient filtering without parsing data.
1{2 "type": "kline",3 "symbol": "DOGEUSDT",4 "timestamp": "2026-03-02T14:01:45.299305",5 "data": {6 "_timestamps": {7 "websocket_sent": 1772460105300,8 "exchange_event_time": 17724600000009 },10 "timestamp": "2026-03-02 14:00:00",11 "exchange": "binance",12 "symbol": "DOGEUSDT",13 "interval": "1h",14 "open": 0.0909,15 "high": 0.09093,16 "low": 0.09083,17 "close": 0.09083,18 "volume": 4784283.0,19 "quote_volume": 434786.04922,20 "num_trades": 1314,21 "close_time": "2026-03-02 14:59:59.999000",22 "is_final": false23 },24 "interval": "1h",25 "producer_received": 1772460105128.698726}
Orderbook Data
Orderbook messages contain bid/ask depth:
1{2 "type": "orderbook",3 "symbol": "XRPUSDT",4 "timestamp": "2026-03-02T14:01:45.275758",5 "data": {6 "_timestamps": {7 "websocket_sent": 1772460105301,8 "exchange_event_time": 17724601051569 },10 "timestamp": "2026-03-02 14:01:45.156000",11 "exchange": "bybit",12 "symbol": "XRPUSDT",13 "bids": [14 [1.3388, 1000.0],15 [1.3387, 2500.0],16 [1.3386, 1800.0]17 ],18 "asks": [19 [1.3389, 800.0],20 [1.3390, 1500.0],21 [1.3391, 2200.0]22 ],23 "is_snapshot": true,24 "sequence_id": "233916094713"25 },26 "producer_received": 1772460105157.507827}
Best Practices
Heartbeat (Critical!)
- Always respond to server pings with pong - Connection will be closed after 60s without response
- When you receive {"type": "ping"}, immediately send {"action": "pong"}
- Do NOT rely on library-level ping/pong - our server uses JSON messages
- Server sends ping every 20 seconds
Connection Management
- Use a single WebSocket connection per application
- Implement automatic reconnection with exponential backoff (1s, 2s, 4s, 8s...)
- Handle connection drops gracefully - re-subscribe after reconnect
- Store your subscriptions locally to restore them after reconnect
- Server does not persist subscriptions - always re-subscribe after reconnect
Performance & Backpressure
- Subscribe only to the channels you need - avoid wildcard (*) in production
- Use symbol-specific channels to reduce data volume
- Process messages asynchronously - never block the WebSocket receive loop
- Use a message queue if processing is slow - drop old messages if queue is full
- Monitor your processing latency - if you fall behind, you may get disconnected
- Expected throughput: 500-800 msg/s depending on market activity
Error Handling
- Handle {"error": "..."} messages gracefully
- Rate limit errors: Back off and retry after 1 second
- Connection closed unexpectedly: Reconnect with exponential backoff
- Invalid JSON: Log and skip the message, don't crash
- Unknown message types: Ignore them - we may add new types in the future
Security
- Never expose your credentials in client-side code
- Use environment variables for credentials
- Rotate credentials if compromised
- Use unique client_id per connection for debugging
Rate Limits
Current rate limits for the WebSocket API:
| Connections per account | 5 |
| Subscriptions per connection | 50 |
| Control messages per second | 5 |
| Server ping interval | 20 seconds |
| Pong timeout (disconnect) | 60 seconds |
| Max connection duration | No limit |
Control messages include subscribe, unsubscribe, ping, pong, and status requests. Need higher limits? Contact us at info@qalypto.com
© 2026 Qalypto. All rights reserved.
Questions? Contact us at info@qalypto.com