Skip to main content

WebSocket API General Info

Basic information and connection guide for ZTDX WebSocket API.

Base URL

NetworkWebSocket URL
Mainnetwss://api.ztdx.io/ws
Testnet (Sepolia)wss://testnet.ztdx.io/ws

Endpoints

PathDescription
/wsInternal market data (orderbook, trades, ticker, kline, positions, orders, balance)
/ws/internalSame as /ws — explicit internal data endpoint
/ws/externalExternal 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 Ping frames with Pong frames automatically.
  • A single WebSocket connection can subscribe to multiple channels simultaneously.

Connection Example

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);
};

Message Format

Client → Server Messages

All client messages must include a type field.

TypeDescriptionAuth Required
authAuthenticate via EIP-712 signature, JWT, or fapi listenKey
auth_tokenAuthenticate via JWT token
subscribeSubscribe to a channelPrivate channels only
unsubscribeUnsubscribe from a channelNo
pingHeartbeat pingNo

Server → Client Messages

All server messages include a type field.

TypeDescription
auth_resultAuthentication result
subscribedSubscription confirmation
unsubscribedUnsubscription confirmation
tradeTrade update
orderbookOrder book snapshot/update
tickerTicker data
klineK-line/candlestick data
kline_snapshotK-line initial snapshot on subscribe
positionPosition update (private)
orderOrder update (private)
balanceBalance update (private)
errorError message
pongHeartbeat 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.

ChannelFormatDescription
Orderbookorderbook:{symbol}Real-time order book depth (top 20 levels)
Tradestrades:{symbol}Real-time trade stream
Tickerticker:{symbol}Market ticker (every 2 seconds)
K-linekline:{symbol}:{interval}Candlestick data

Private Channels

Private channels require authentication.

ChannelFormatDescription
PositionspositionsUser position updates (every 5 seconds)
OrdersordersUser order updates (real-time + every 5 seconds)
BalancebalanceUser balance updates (every 5 seconds)

Symbol Format

Symbols support multiple input formats and are automatically normalized:

InputNormalized
BTCUSDTBTCUSDT
btcusdtBTCUSDT
BTC-USDBTCUSDT
BTC-USDTBTCUSDT
BTC/USDBTCUSDT
BTC_USDBTCUSDT

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: true when 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 CodeDescription
INVALID_MESSAGEFailed to parse the client message
AUTH_REQUIREDAuthentication required for private channel
HL_CONNECTION_FAILEDFailed to connect to external price feed (external endpoint only)
HL_ERRORExternal price feed connection error (external endpoint only)
HL_DISCONNECTEDExternal price feed disconnected (external endpoint only)

Update Intervals

Data TypePush Interval
TradesReal-time (matching engine broadcast)
OrderbookEvery 500ms
TickerEvery 2 seconds
K-lineReal-time (on candle update)
PositionsEvery 5 seconds
OrdersReal-time + every 5 seconds
BalanceEvery 5 seconds

Full Example

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);