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.

4
Exchanges
4
Data Types
~200ms E2E
WS Latency

Supported Exchanges

BinanceBitgetBybitOKX

Data Coverage

Symbols:BTCUSDT, ETHUSDT, SOLUSDT, XRPUSDT, BNBUSDT, DOGEUSDT, ADAUSDT, DOTUSDT, LINKUSDT, AVAXUSDT
Kline Intervals:1m, 5m, 15m, 30m, 1h, 4h, 12h, 1d

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 TypeDeliveryNotes
tradeReal-timeEvery trade pushed immediately
tickerReal-timeEvery update pushed immediately
klineReal-timeLive maintained candles with every update
orderbookSampledMaintained 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

CF-Access-Client-Id: <your-client-id>
CF-Access-Client-Secret: <your-client-secret>

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:

Hostclickhouse.qalypto.com
Port8443 (HTTPS)
Databasemarket_data
Username<your-username>
Password<your-password>

TLS/SSL encryption is required for all connections. Unencrypted connections are not supported.

1import requests
2
3# ClickHouse HTTP API Connection
4HOST = "https://clickhouse.qalypto.com:8443"
5USER = "<your-username>"
6PASSWORD = "<your-password>"
7
8def query(sql):
9 response = requests.get(
10 HOST,
11 params={'query': sql},
12 auth=(USER, PASSWORD),
13 verify=True
14 )
15 response.raise_for_status()
16 return response.text
17
18# Query recent trades
19result = query('''
20 SELECT
21 timestamp,
22 exchange,
23 symbol,
24 price,
25 amount,
26 side
27 FROM market_data.market_data_trade
28 WHERE timestamp >= now() - INTERVAL 1 HOUR
29 ORDER BY timestamp DESC
30 LIMIT 100
31 FORMAT TabSeparated
32''')
33
34for line in result.strip().split('\n'):
35 print(line)

Database Tables

Four main tables store all market data:

market_data_trade

ColumnType
timestampDateTime64(3)
exchangeString
symbolString
priceFloat64
amountFloat64
sideString
trade_idString
is_liquidationUInt8

market_data_ticker

ColumnType
timestampDateTime64(3)
exchangeString
symbolString
last_priceFloat64
bid_price, ask_priceNullable(Float64)
bid_size, ask_sizeNullable(Float64)
high_24h, low_24h, volume_24hNullable(Float64)
quote_volume_24hNullable(Float64)
open_24hNullable(Float64)
funding_rateNullable(Float64) — Perpetual funding rate
next_funding_timeNullable(DateTime64) — Next settlement
mark_price, index_priceNullable(Float64)
open_interestNullable(Float64) — Contracts
open_interest_valueNullable(Float64) — USD value

market_data_kline

ColumnType
timestampDateTime64(3)
exchangeString
symbolString
intervalString
open, high, low, closeFloat64
volumeFloat64
quote_volumeNullable(Float64)
num_tradesNullable(Int32)
close_timeNullable(DateTime64(3))
is_finalUInt8

market_data_orderbook

ColumnType
timestampDateTime64(3)
exchangeString
symbolString
bids_json, asks_jsonString (JSON)
bids_price_0, asks_price_0Nullable(Float64)
bids_size_0, asks_size_0Nullable(Float64)
is_snapshotUInt8
sequence_idString

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 from data.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

Get historical klines
1-- Get OHLCV data for backtesting
2SELECT
3 timestamp,
4 exchange,
5 symbol,
6 interval,
7 open,
8 high,
9 low,
10 close,
11 volume
12FROM market_data.market_data_kline
13WHERE 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 DAY
18ORDER BY timestamp ASC

Cross-Exchange Price Comparison

Compare prices across exchanges
1-- Cross-exchange price comparison
2SELECT
3 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 updates
8FROM market_data.market_data_ticker
9WHERE symbol IN ('BTCUSDT', 'ETHUSDT')
10 AND timestamp >= now() - INTERVAL 5 MINUTE
11GROUP BY exchange, symbol
12ORDER BY avg_price DESC

WebSocket Connection

Connect to the WebSocket API using the following endpoint:

wss://watchdog.qalypto.com/ws/stream/{client_id}

Parameters

ParameterTypeDescription
client_idstringUnique identifier for your connection (e.g., your username)

Connection Examples

1import asyncio
2import websockets
3import json
4
5async 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 }
11
12 async with websockets.connect(uri, extra_headers=headers,
13 ping_interval=None, ping_timeout=None) as ws:
14 # Subscribe to channels
15 await ws.send(json.dumps({
16 "action": "subscribe",
17 "channels": ["trade", "ticker", "kline"]
18 }))
19
20 # Receive messages
21 async for message in ws:
22 msg = json.loads(message)
23 # Handle server heartbeat
24 if msg.get("type") == "ping":
25 await ws.send(json.dumps({"action": "pong"}))
26 continue
27 data = msg.get("data", {})
28 print(f"[{data.get('exchange')}] {data.get('symbol')}: {data}")
29
30asyncio.run(connect())

Connection Heartbeat (Important!)

Similar to Binance, Bybit, and OKX, our WebSocket server implements a heartbeat mechanism to ensure connection stability:

1.
Server sends ping every 20 seconds

You will receive: {"type": "ping", "timestamp": 1706450000000}

2.
Client must respond with pong within 60 seconds

Send: {"action": "pong"}

3.
Connection will be closed if no pong received

After 60 seconds without pong or 3 missed pongs, the server disconnects the client.

# Python example - handle server ping
if msg.get("type") == "ping":
    await ws.send(json.dumps({"action": "pong"}))

Subscription

After connecting, send a subscription message to start receiving data:

Subscribe Message
1{
2 "action": "subscribe",
3 "channels": ["trade", "ticker", "kline", "orderbook"]
4}

Actions

ActionDescription
subscribeSubscribe to one or more channels
unsubscribeUnsubscribe from channels
pingClient-initiated health check (returns pong)
pongResponse to server ping (required for heartbeat)
statusGet connection status
Unsubscribe Message
1{
2 "action": "unsubscribe",
3 "channels": ["orderbook"]
4}

Client → Server Messages

Ping (client-initiated)
1{
2 "action": "ping"
3}
Pong (response to server ping)
1{
2 "action": "pong"
3}
Status Request
1{
2 "action": "status"
3}

Server → Client Messages

Connected (on connection)
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": 50
18 },
19 "actions": ["subscribe", "unsubscribe", "ping", "pong", "status"]
20 }
21}
Subscribed (after subscribe)
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!

Server Ping
1{
2 "type": "ping",
3 "timestamp": 1706450000000,
4 "server_time": "2026-01-28T12:53:40.123Z"
5}
Pong Acknowledgement
1{
2 "type": "pong_ack",
3 "timestamp": "2026-01-28T12:53:40.456Z"
4}
Status Response
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}
Error Response
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:

Python - Reconnection with Backoff
1import asyncio
2import websockets
3import json
4from datetime import datetime
5
6class 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_secret
12 }
13 self.channels = ["trade", "ticker"]
14 self.reconnect_delay = 1
15 self.max_reconnect_delay = 60
16
17 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 heartbeat
24 ping_timeout=None
25 ) as ws:
26 print(f"[{datetime.now()}] Connected")
27 self.reconnect_delay = 1 # Reset on success
28
29 # Subscribe
30 await ws.send(json.dumps({
31 "action": "subscribe",
32 "channels": self.channels
33 }))
34
35 async for message in ws:
36 await self.handle_message(json.loads(message), ws)
37
38 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_delay
45 )
46
47 async def handle_message(self, msg, ws):
48 msg_type = msg.get("type")
49
50 # IMPORTANT: Respond to server pings to keep connection alive
51 if msg_type == "ping":
52 await ws.send(json.dumps({"action": "pong"}))
53 return
54
55 # Handle market data
56 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'))}")
59
60# Usage
61client = 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

tradeAll trade data from all exchanges
tickerAll ticker data from all exchanges
klineAll kline/candlestick data
orderbookAll orderbook snapshots

Symbol-Specific Channels

trade:btcusdtTrades for BTCUSDT (also: ethusdt, solusdt, xrpusdt, bnbusdt, dogeusdt, adausdt, dotusdt, linkusdt, avaxusdt)
ticker:ethusdtTicker for ETHUSDT (also: btcusdt, solusdt, xrpusdt, bnbusdt, dogeusdt, adausdt, dotusdt, linkusdt, avaxusdt)
kline:1m:solusdt1-minute klines for SOLUSDT (any symbol + interval)
orderbook:xrpusdtOrderbook for XRPUSDT (also: btcusdt, ethusdt, solusdt, bnbusdt, dogeusdt, adausdt, dotusdt, linkusdt, avaxusdt)

Kline Intervals

1m5m15m30m1h4h12h1d

Wildcard

Use * to subscribe to all data types.

Message Format

All messages are JSON objects with the following structure:

Message Structure
1{
2 "type": "trade", // Data type
3 "symbol": "BTCUSDT", // Trading pair
4 "timestamp": "2026-...", // Server timestamp (ISO 8601)
5 "data": { ... }, // Payload
6 "producer_received": 17708... // Epoch ms — when producer received from exchange
7}

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:

Trade Message
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": 1772460105100
9 },
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": false
18 },
19 "producer_received": 1772460105224.0476
20}

Ticker Data

Ticker messages contain current price and volume information:

Ticker Message
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": 1772460105138
9 },
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": null
29 },
30 "producer_received": 1772460105223.4756
31}

Kline Data

Kline (candlestick) messages contain OHLCV data. Note: interval is also available as a top-level field for convenient filtering without parsing data.

Kline Message
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": 1772460000000
9 },
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": false
23 },
24 "interval": "1h",
25 "producer_received": 1772460105128.6987
26}

Orderbook Data

Orderbook messages contain bid/ask depth:

Orderbook Message
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": 1772460105156
9 },
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.5078
27}

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 account5
Subscriptions per connection50
Control messages per second5
Server ping interval20 seconds
Pong timeout (disconnect)60 seconds
Max connection durationNo 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