WebSocket
Channels, subscribe frames, socket authentication, sequence numbers and connection hygiene.
One socket carries market data and private account updates:
wss://api-mainnet.1024ex.com/api/v1/ws
wss://api-testnet-stable.1024ex.com/api/v1/wsPublic channels need no authentication. Private channels require authenticating the socket itself.
Frames
Requests are JSON objects with method and params — not op:
{ "id": "1", "method": "subscribe", "params": { "channel": "perp.ticker", "market": "BTC-USDC" } }Methods: subscribe, unsubscribe, auth, ping. id is echoed back so you can correlate replies.
An acknowledgement, then data:
{ "id": "1", "type": "subscribed", "channel": "perp.ticker:BTC-USDC", "success": true, "timestamp": 1790094770315 }{
"channel": "perp.ticker:BTC-USDC",
"market": "BTC-USDC",
"event": "snapshot",
"seq": 1,
"data": {
"market": "BTC-USDC", "bid": "86115.74", "ask": "86154.39",
"lastPrice": "82918.53", "markPrice": "86134.55", "indexPrice": "86134.55",
"openInterest": "0.012139", "priceChangePercent24h": "0.24",
"timestamp": 1790094770443
},
"timestamp": 1790094770443
}The subscription key is channel:market. seq increments per subscription — a gap means you missed a frame and should resynchronize from REST. The server also pushes unsolicited {"type":"heartbeat"} frames.
Channels
| Channel | Needs market | Needs auth |
|---|---|---|
perp.ticker | ✓ | |
perp.orderbook | ✓ | |
perp.trades | ✓ | |
perp.kline | ✓ (+ interval) | |
perp.prices | ||
perp.funding | ✓ | |
perp.insurance | ||
pm.markets / pm.orderbook / pm.trades | varies | |
user.orders | ✓ | |
user.trades | ✓ | |
user.positions | ✓ | |
user.balances | ✓ | |
user.margin | ✓ | |
user.advanced_orders | ✓ | |
pm.positions / pm.orders | ✓ |
perp.orderbook accepts depth; perp.kline requires interval. Private channels accept an optional subAccountId — omit it to receive events across every sub-account on the wallet.
There is no options channel. Options state comes from REST: poll GET /api/v1/options/chain for marks and GET /api/v1/options/positions for your book.
Authenticating the socket
Send an auth frame before subscribing to any user.* channel. The signature covers a fixed payload — timestamp, method, path, no body:
payload = <timestamp> + "GET" + "/api/v1/ws"{
"id": "auth-1",
"method": "auth",
"params": {
"apiKey": "1024_…",
"timestamp": 1790094770000,
"signature": "<hex HMAC_SHA256(secret, payload)>"
}
}Failures come back as AUTH_INVALID (11002) or AUTH_INVALID_SIGNATURE (11003), with the same meanings as on REST.
Connection hygiene
- Send a frame —
pingwill do — at least every 90 seconds. A sweeper runs on a 30 s tick against a 90 s idle threshold, so an idle socket is dropped somewhere between ~91 s and 120 s. Do not tune your keepalive to exactly 90 s. - Tier-0 ceilings: 3 connections, 50 subscriptions, 50 messages/second. Exceeding any of them is
RATE_LIMIT_WS(19010). - Reconnect with backoff and re-subscribe — subscriptions do not survive a reconnect. After any gap, resynchronize state from REST before trusting incremental frames again.
Minimal client
import asyncio, hashlib, hmac, json, time, websockets
async def main():
async with websockets.connect("wss://api-mainnet.1024ex.com/api/v1/ws") as ws:
ts = int(time.time() * 1000)
sig = hmac.new(SECRET.encode(), f"{ts}GET/api/v1/ws".encode(), hashlib.sha256).hexdigest()
await ws.send(json.dumps({"id": "auth", "method": "auth",
"params": {"apiKey": KEY, "timestamp": ts, "signature": sig}}))
await ws.send(json.dumps({"id": "1", "method": "subscribe",
"params": {"channel": "perp.ticker", "market": "BTC-USDC"}}))
await ws.send(json.dumps({"id": "2", "method": "subscribe",
"params": {"channel": "user.positions"}}))
async def keepalive():
while True:
await asyncio.sleep(30)
await ws.send(json.dumps({"id": "ping", "method": "ping"}))
asyncio.create_task(keepalive())
async for raw in ws:
msg = json.loads(raw)
if msg.get("type") != "heartbeat":
print(msg.get("channel"), msg.get("event"), msg.get("data"))
asyncio.run(main())Updated 9 days ago
