WebSocket API General Info
Basic information and connection guide for ZTDX WebSocket API.
Base URL
| Network | WebSocket URL |
|---|---|
| Mainnet | wss://api.ztdx.io/ws |
| Testnet (Sepolia) | wss://testnet.ztdx.io/ws |
Endpoints
| Path | Description |
|---|---|
/ws | Internal market data (orderbook, trades, ticker, kline, positions, orders, balance) |
/ws/internal | Same as /ws — explicit internal data endpoint |
/ws/external | External price feed (Hyperliquid proxy) |
Connection
- WebSocket connections use standard WebSocket protocol (RFC 6455).
- All messages are sent and received as JSON text frames.
- The server responds to WebSocket
Pingframes withPongframes automatically. - A single WebSocket connection can subscribe to multiple channels simultaneously.
Connection Example
- JavaScript
- Python
const ws = new WebSocket('wss://api.ztdx.io/ws');
ws.onopen = () => {
console.log('Connected');
// Subscribe to a public channel
ws.send(JSON.stringify({
type: 'subscribe',
channel: 'ticker:BTCUSDT'
}));
};
ws.onmessage = (event) => {
const data = JSON.parse(event.data);
console.log('Received:', data);
};
import asyncio
import json
import websockets
async def connect():
async with websockets.connect('wss://api.ztdx.io/ws') as ws:
print('Connected')
# Subscribe to a public channel
await ws.send(json.dumps({
'type': 'subscribe',
'channel': 'ticker:BTCUSDT'
}))
async for message in ws:
data = json.loads(message)
print('Received:', data)
asyncio.run(connect())
Message Format
Client → Server Messages
All client messages must include a type field.
| Type | Description | Auth Required |
|---|---|---|
auth | Authenticate via EIP-712 signature, JWT, or fapi listenKey | — |
auth_token | Authenticate via JWT token | — |
subscribe | Subscribe to a channel | Private channels only |
unsubscribe | Unsubscribe from a channel | No |
ping | Heartbeat ping | No |
Server → Client Messages
All server messages include a type field.
| Type | Description |
|---|---|
auth_result | Authentication result |
subscribed | Subscription confirmation |
unsubscribed | Unsubscription confirmation |
trade | Trade update |
orderbook | Order book snapshot/update |
ticker | Ticker data |
kline | K-line/candlestick data |
kline_snapshot | K-line initial snapshot on subscribe |
position | Position update (private) |
order | Order update (private) |
balance | Balance update (private) |
error | Error message |
pong | Heartbeat pong |
Authentication
Private channels (positions, orders, balance) require authentication before subscribing. Three authentication methods are supported:
Method 1: EIP-712 Signature
{
"type": "auth",
"address": "0xYourWalletAddress",
"signature": "0x...",
"timestamp": 1711000000
}
address— Ethereum wallet address.signature— EIP-712 typed data signature.timestamp— UNIX timestamp in seconds. Must be within 5 minutes of server time.
Method 2: JWT Token
{
"type": "auth",
"token": "eyJhbGciOiJIUzI1..."
}
Or use the dedicated auth_token type:
{
"type": "auth_token",
"token": "eyJhbGciOiJIUzI1..."
}
Method 3: fapi listenKey
For HMAC-authenticated bots (Binance fapi clients), obtain a listenKey via POST /fapi/v1/listenKey and pass it on the WebSocket:
{
"type": "auth",
"listenKey": "a1b2c3d4e5f6..."
}
The server resolves the listenKey to its owning user_address (Redis lookup) and refreshes its TTL on each authenticated message — a long-running WebSocket connection acts as implicit keepalive, so calling PUT /fapi/v1/listenKey is only needed when the WebSocket is offline.
Method 4: Token with Subscribe
You can also pass a token field directly in a subscribe message. The server will auto-authenticate before processing the subscription:
{
"type": "subscribe",
"channel": "positions",
"token": "eyJhbGciOiJIUzI1..."
}
Auth Response
{
"type": "auth_result",
"success": true,
"message": null
}
On failure:
{
"type": "auth_result",
"success": false,
"message": "Invalid or expired token"
}
Channels
Public Channels
Public channels do not require authentication.
| Channel | Format | Description |
|---|---|---|
| Orderbook | orderbook:{symbol} | Real-time order book depth (top 20 levels) |
| Trades | trades:{symbol} | Real-time trade stream |
| Ticker | ticker:{symbol} | Market ticker (every 2 seconds) |
| K-line | kline:{symbol}:{interval} | Candlestick data |
Private Channels
Private channels require authentication.
| Channel | Format | Description |
|---|---|---|
| Positions | positions | User position updates (every 5 seconds) |
| Orders | orders | User order updates (real-time + every 5 seconds) |
| Balance | balance | User balance updates (every 5 seconds) |
Symbol Format
Symbols support multiple input formats and are automatically normalized:
| Input | Normalized |
|---|---|
BTCUSDT | BTCUSDT |
btcusdt | BTCUSDT |
BTC-USD | BTCUSDT |
BTC-USDT | BTCUSDT |
BTC/USD | BTCUSDT |
BTC_USD | BTCUSDT |
K-line Intervals
Supported intervals for kline:{symbol}:{interval}:
1m, 3m, 5m, 15m, 30m, 1h, 2h, 4h, 6h, 8h, 12h, 1d, 3d, 1w, 1M
Subscribe / Unsubscribe
Subscribe
{
"type": "subscribe",
"channel": "orderbook:BTCUSDT"
}
Response:
{
"type": "subscribed",
"channel": "orderbook:BTCUSDT"
}
Upon subscribing to orderbook, ticker, positions, orders, or balance channels, the server immediately sends a snapshot of the current data.
Unsubscribe
{
"type": "unsubscribe",
"channel": "orderbook:BTCUSDT"
}
Response:
{
"type": "unsubscribed",
"channel": "orderbook:BTCUSDT"
}
Heartbeat / Keep-Alive
Send a ping message to keep the connection alive:
{
"type": "ping"
}
Response:
{
"type": "pong"
}
The server also responds to standard WebSocket Ping frames with Pong frames.
Data Payloads
Trade
{
"type": "trade",
"id": "1711000000000-a1b2c3d4",
"symbol": "BTCUSDT",
"price": "65432.10000",
"amount": "0.5",
"side": "buy",
"timestamp": 1711000000000
}
Orderbook
{
"type": "orderbook",
"symbol": "BTCUSDT",
"bids": [
{ "price": "65430.00", "size": "1.2" },
{ "price": "65429.50", "size": "0.8" }
],
"asks": [
{ "price": "65431.00", "size": "0.5" },
{ "price": "65431.50", "size": "1.0" }
],
"timestamp": 1711000000000
}
- Orderbook updates are pushed every 500ms.
- Top 20 levels on each side.
Ticker
{
"type": "ticker",
"symbol": "BTCUSDT",
"last_price": "65432.10",
"mark_price": "65433.00",
"index_price": "65431.50",
"price_change_24h": "1200.50",
"price_change_percent_24h": "1.87",
"high_24h": "66000.00",
"low_24h": "64000.00",
"volume_24h": "12345.6789",
"volume_24h_usd": "807654321.00",
"open_interest_long": "5000.0000",
"open_interest_short": "4200.0000",
"open_interest_long_percent": "54",
"open_interest_short_percent": "46",
"available_liquidity_long": "1500000.00",
"available_liquidity_short": "1200000.00",
"funding_rate_long_1h": "+0.0050%",
"funding_rate_short_1h": "-0.0050%"
}
- Ticker updates are pushed every 2 seconds.
K-line
{
"type": "kline",
"channel": "kline:BTCUSDT:5m",
"data": {
"time": 1711000000000,
"open": "65400.00",
"high": "65500.00",
"low": "65350.00",
"close": "65432.10",
"volume": "123.456",
"quote_volume": "8074000.00",
"trade_count": 542,
"is_final": false
}
}
is_final:truewhen the candlestick period has closed and the candle is complete.
Position (Private)
{
"type": "position",
"id": "pos-uuid-1234",
"symbol": "BTCUSDT",
"side": "long",
"size": "0.5",
"entry_price": "65000.00",
"mark_price": "65432.10",
"liquidation_price": "62000.00",
"unrealized_pnl": "216.05",
"leverage": 10,
"margin": "3250.00",
"updated_at": 1711000000000,
"event": "update"
}
Order (Private)
{
"type": "order",
"id": "order-uuid-5678",
"symbol": "BTCUSDT",
"side": "buy",
"order_type": "limit",
"price": "64000.00",
"amount": "0.5",
"filled_amount": "0.0",
"status": "open",
"updated_at": 1711000000000,
"event": "created"
}
Order updates are delivered in real-time (pushed immediately when an order is created, filled, or cancelled) and also polled every 5 seconds.
Balance (Private)
{
"type": "balance",
"token": "USDT",
"symbol": "USDT",
"available": "10000.00",
"frozen": "3250.00",
"total": "13250.00"
}
Error Messages
{
"type": "error",
"code": "AUTH_REQUIRED",
"message": "Authentication required for private channels"
}
| Error Code | Description |
|---|---|
INVALID_MESSAGE | Failed to parse the client message |
AUTH_REQUIRED | Authentication required for private channel |
HL_CONNECTION_FAILED | Failed to connect to external price feed (external endpoint only) |
HL_ERROR | External price feed connection error (external endpoint only) |
HL_DISCONNECTED | External price feed disconnected (external endpoint only) |
Update Intervals
| Data Type | Push Interval |
|---|---|
| Trades | Real-time (matching engine broadcast) |
| Orderbook | Every 500ms |
| Ticker | Every 2 seconds |
| K-line | Real-time (on candle update) |
| Positions | Every 5 seconds |
| Orders | Real-time + every 5 seconds |
| Balance | Every 5 seconds |
Full Example
- JavaScript
- Python
const ws = new WebSocket('wss://api.ztdx.io/ws');
ws.onopen = () => {
// 1. Authenticate
ws.send(JSON.stringify({
type: 'auth_token',
token: 'your-jwt-token'
}));
};
ws.onmessage = (event) => {
const msg = JSON.parse(event.data);
switch (msg.type) {
case 'auth_result':
if (msg.success) {
// 2. Subscribe to channels after auth
ws.send(JSON.stringify({ type: 'subscribe', channel: 'ticker:BTCUSDT' }));
ws.send(JSON.stringify({ type: 'subscribe', channel: 'orderbook:BTCUSDT' }));
ws.send(JSON.stringify({ type: 'subscribe', channel: 'trades:BTCUSDT' }));
ws.send(JSON.stringify({ type: 'subscribe', channel: 'kline:BTCUSDT:5m' }));
ws.send(JSON.stringify({ type: 'subscribe', channel: 'positions' }));
ws.send(JSON.stringify({ type: 'subscribe', channel: 'orders' }));
ws.send(JSON.stringify({ type: 'subscribe', channel: 'balance' }));
}
break;
case 'ticker':
console.log(`${msg.symbol}: ${msg.last_price} (${msg.price_change_percent_24h}%)`);
break;
case 'trade':
console.log(`Trade: ${msg.side} ${msg.amount} @ ${msg.price}`);
break;
case 'orderbook':
console.log(`Orderbook: ${msg.bids.length} bids, ${msg.asks.length} asks`);
break;
case 'position':
console.log(`Position: ${msg.symbol} ${msg.side} PnL: ${msg.unrealized_pnl}`);
break;
case 'order':
console.log(`Order: ${msg.symbol} ${msg.side} ${msg.status}`);
break;
case 'error':
console.error(`Error [${msg.code}]: ${msg.message}`);
break;
}
};
// 3. Keep alive
setInterval(() => {
if (ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({ type: 'ping' }));
}
}, 30000);
import asyncio
import json
import websockets
async def main():
async with websockets.connect('wss://api.ztdx.io/ws') as ws:
# 1. Authenticate
await ws.send(json.dumps({
'type': 'auth_token',
'token': 'your-jwt-token'
}))
# 2. Keep-alive task
async def heartbeat():
while True:
await asyncio.sleep(30)
await ws.send(json.dumps({'type': 'ping'}))
asyncio.create_task(heartbeat())
# 3. Listen for messages
async for message in ws:
msg = json.loads(message)
msg_type = msg.get('type')
if msg_type == 'auth_result':
if msg.get('success'):
# Subscribe to channels after auth
channels = [
'ticker:BTCUSDT',
'orderbook:BTCUSDT',
'trades:BTCUSDT',
'kline:BTCUSDT:5m',
'positions',
'orders',
'balance',
]
for ch in channels:
await ws.send(json.dumps({
'type': 'subscribe', 'channel': ch
}))
else:
print(f"Auth failed: {msg.get('message')}")
elif msg_type == 'ticker':
print(f"{msg['symbol']}: {msg['last_price']} "
f"({msg['price_change_percent_24h']}%)")
elif msg_type == 'trade':
print(f"Trade: {msg['side']} {msg['amount']} @ {msg['price']}")
elif msg_type == 'orderbook':
print(f"Orderbook: {len(msg['bids'])} bids, "
f"{len(msg['asks'])} asks")
elif msg_type == 'position':
print(f"Position: {msg['symbol']} {msg['side']} "
f"PnL: {msg['unrealized_pnl']}")
elif msg_type == 'order':
print(f"Order: {msg['symbol']} {msg['side']} {msg['status']}")
elif msg_type == 'error':
print(f"Error [{msg['code']}]: {msg['message']}")
asyncio.run(main())