WebSocket Private API Documentation

Real-time data streaming for user positions, orders, trade executions, balance updates, and public market data via SignalR WebSocket hubs.

Key Features

Hub Endpoints

HubEndpointTransportPurpose
GlobalHub /GlobalHub WebSocket Private user events: positions, orders, trades, balance, notifications
PublicMarketData /PublicMarketData WebSocket OrderBook streaming, mini tickers, mark prices

1. Authentication

Authentication is mandatory for receiving private events. Unauthenticated connections receive no user data. Two methods are supported:

Method A: JWT Token

The server sends LR (Login Request) immediately on connect. Respond with your JWT token:

hub.on("LR", () => {
    hub.invoke("WebSocketLogin", jwtToken);
});

hub.on("LS", () => console.log("Authenticated"));  // Success
hub.on("LF", () => console.log("Auth failed"));    // Failure

Method B: API Key + HMAC-SHA256

Authenticate using the same API Key credentials as the REST Trade API. Sign the current timestamp with HMAC-SHA256 using your API Secret.

const crypto = require("crypto");

hub.on("LR", () => {
    const timestamp = Date.now();
    const signature = crypto
        .createHmac("sha256", API_SECRET)
        .update(timestamp.toString())
        .digest("hex");

    hub.invoke("WebSocketLoginWithApiKey", API_KEY, signature, timestamp);
});
ParameterTypeDescription
apiKeystringAPI Key from account dashboard
signaturestringHMAC-SHA256(apiSecret, timestamp.toString()), lowercase hex
timestamplongUnix milliseconds. Must be within 5 seconds of server time.
IP Restrictions: If IP restrictions are configured on the API key, the connection must originate from an allowed IP address.

Connection & Authentication Flow

Client Server | | |------------ Connect (WebSocket) ------------------->| | | |<------------- LR (Login Request) -------------------| | | |--- WebSocketLogin(jwt) --------------->| // Option A: JWT |--- WebSocketLoginWithApiKey(key, sig, ts) ---------->| // Option B: API Key | | |<------------- LS (Login Success) -------------------| // Now receiving private events | | |<---- FuturesAccountUpdate -------------------------| // Position changes |<---- OrderUpdate ---------------------------------| // Order state changes |<---- TradeUpdate ---------------------------------| // Trade executions |<---- BalanceUpdate --------------------------------| // Wallet mutations |<---- Notification ---------------------------------| // System alerts | |

2. Private Events (GlobalHub)

All private events are pushed to authenticated users in real-time. No subscription needed — events are delivered automatically after login.

FuturesAccountUpdate

Pushed when any futures position or account state changes: position open/close, size change, PnL recalculation, margin adjustments, liquidation price update.

{
  "positions": [
    {
      "positionId": 12345,
      "symbol": "BTCUSDT",
      "baseAsset": "BTC",
      "quoteAsset": "USDT",
      "size": 0.5,
      "orderSide": "Buy",
      "marginMode": "Isolated",
      "entryPrice": 65230.50,
      "quantity": 0.5,
      "leverage": 10,
      "initialMargin": 3261.525,
      "marginBalance": 3350.00,
      "maintenanceMargin": 163.08,
      "liquidationPrice": 58700.00,
      "unrealizedPnL": 125.75,
      "realizedPnL": 0,
      "markPrice": 65482.00,
      "fundingFee": -1.23,
      "adl": 2,
      "openedAt": "2025-03-15T10:30:00Z"
    }
  ],
  "symbol": "BTCUSDT",
  "availableBalance": 15000.00,
  "maintenanceMargin": 163.08,
  "marginBalance": 3350.00,
  "balance": 18350.00,
  "accountequity": 18475.75,
  "unrealizedPnL": 125.75,
  "walletMode": "OneWay",
  "marginCallRate": 20,
  "marginRatio": 0.048
}

OrderUpdate

Pushed on every order state change: creation, partial fill, full fill, cancellation. A single event stream covers both active orders and order history.

{
  "orderId": 98765,
  "clientOrderId": "my-order-001",
  "symbol": "BTCUSDT",
  "orderBookType": "Futures",
  "side": "Buy",
  "type": "Limit",
  "orderDirection": "Open",
  "baseAsset": "BTC",
  "quoteAsset": "USDT",
  "price": 65000.00,
  "quantity": 0.5,
  "executedQuantity": 0.3,
  "averagePrice": 65010.25,
  "cost": 19503.075,
  "status": "PartiallyFilled",
  "created": "2025-03-15T10:30:00Z",
  "updated": "2025-03-15T10:30:05Z",
  "stopTriggered": false,
  "stopPrice": 0,
  "stopLossTriggerPrice": 60000.00,
  "takeProfitTriggerPrice": 70000.00,
  "source": "Api"
}

Order Status Values

StatusDescription
NewOrder placed, waiting in order book
PartiallyFilledPartially executed, remaining quantity still active
FilledFully executed
CanceledCanceled by user or system

TradeUpdate

Pushed on each trade execution (fill). Each partial fill generates a separate event. Contains maker/taker information via the isBuyerMaker field.

{
  "id": 456789,
  "symbol": "BTCUSDT",
  "price": 65010.25,
  "quantity": 0.3,
  "quoteQuantity": 19503.075,
  "orderSide": "Buy",
  "isBuyerMaker": false,
  "isBestMatch": true,
  "time": "2025-03-15T10:30:05Z"
}

Maker / Taker Determination

The isBuyerMaker boolean indicates whether the buyer's order was the maker (already resting on the book). To determine your liquidity role:

hub.on("TradeUpdate", (trade) => {
    const isMaker = (trade.orderSide === "Buy" && trade.isBuyerMaker) ||
                    (trade.orderSide === "Sell" && !trade.isBuyerMaker);

    console.log(`Trade ${trade.id}: ${trade.quantity} @ ${trade.price}`);
    console.log(`Liquidity: ${isMaker ? "MAKER" : "TAKER"}`);
});

BalanceUpdate

Pushed when the user's wallet balance changes: after trades, deposits, withdrawals, margin adjustments, funding fee settlements.

{
  "userId": 12345,
  "type": "Futures",
  "currencyId": 1,
  "symbol": "USDT",
  "currencyName": "Tether",
  "free": 15000.00,
  "locked": 3261.525,
  "total": 18261.525,
  "block": 0
}

Additional Private Events

EventPayloadDescription
AccountUpdateFuturesAccountFutures account configuration change (wallet mode, margin call rate)
NotificationGlobalNotificationMessageSystem alerts and announcements
DepositCryptoUpdateDepositCryptoResponseCrypto deposit confirmed
WithdrawCryptoUpdateWithdrawCryptoResponseCrypto withdrawal status change
KYCUpdateKYCStatusUpdateKYC verification status change

3. Market Data (PublicMarketData Hub)

Public market data is served via the /PublicMarketData hub. All market data streams use push-based snapshots: on subscription the server sends a full snapshot immediately, followed by continuous delta updates.

OrderBook Stream

Subscribe to an order book to receive a full snapshot and real-time delta updates every 250ms.

// Subscribe
await marketHub.invoke("Subscribe", "domain.com", "BTCUSDT", "Spot", 500);

// Snapshot (immediate, on subscribe)
marketHub.on("OrderBookSnapshot", (msg) => {
    // msg.s = sequence ID, msg.b = bids, msg.a = asks, msg.tr = recent trades
    initOrderBook(msg);
    lastSeqId = msg.s;
});

// Delta updates (every 250ms)
marketHub.on("OrderBookUpdate", (msg) => {
    if (msg.s !== lastSeqId + 1) {
        // Gap detected - request replay
        marketHub.invoke("RequestReplay", "domain.com", "BTCUSDT", "Spot", lastSeqId);
        return;
    }
    applyDelta(msg);  // qty "0" = remove level, non-zero = replace
    lastSeqId = msg.s;
});

OrderBook Snapshot Wire Format

{
  "c": "snapshot",
  "s": 42,
  "t": 1709500000000,
  "p": "BTCUSDT",
  "o": "spot",
  "d": 500,
  "b": [["65230.50", "1.234"], ["65230.00", "0.567"]],
  "a": [["65231.00", "0.890"], ["65231.50", "2.345"]],
  "tr": [{"i": 1001, "p": "65230.75", "q": "0.100", "m": true, "t": 1709499999500}]
}

OrderBook Update Wire Format

{
  "c": "update",
  "s": 43,
  "t": 1709500000250,
  "p": "BTCUSDT",
  "o": "spot",
  "b": [["65230.50", "1.500"], ["65229.00", "0"]],
  "a": [["65231.00", "0.500"]],
  "tr": [{"i": 1002, "p": "65230.50", "q": "0.050", "m": false, "t": 1709500000100}]
}
FieldTypeDescription
cstringMessage type: "snapshot" or "update"
slongSequence ID (continuous per order book, for gap detection)
tlongServer timestamp (Unix ms)
pstringPair symbol
ostringOrder book type: "spot" or "futures"
dintDepth level (snapshot only)
barrayBids: [[price, qty], ...]
aarrayAsks: [[price, qty], ...]
trarrayRecent trades
Delta Rules: Quantity "0" means remove the price level. Non-zero quantity means replace the entire quantity at that level. Sequence gaps require RequestReplay.

Depth Levels

Available depth levels: 50, 100, 500 (default), 1000

Gap Recovery

The server keeps the last 20 updates (~5 seconds) per order book in a ring buffer. On gap detection, call RequestReplay(domain, symbol, type, lastSeqId). If the gap is within the buffer, individual deltas are replayed. Otherwise a fresh snapshot is sent.

Ticker Stream

Subscribe to receive a full ticker snapshot for all pairs in a tenant, followed by delta updates every 500ms (only changed tickers).

await marketHub.invoke("SubscribeMiniTickers", "domain.com");

// Full snapshot (immediate)
marketHub.on("TickerSnapshot", (msg) => {
    // msg.d = array of all tickers
    msg.d.forEach(t => tickers.set(t.s, t));
});

// Delta updates (every 500ms, changed tickers only)
marketHub.on("TickerUpdate", (msg) => {
    msg.d.forEach(t => tickers.set(t.s, { ...tickers.get(t.s), ...t }));
});

Ticker Fields

FieldTypeDescription
sstringSymbol (e.g. BTCUSDT)
ostringOrder book type: spot or futures
pstringLast price
opstringOpen price (24h)
hstringHigh price (24h)
lstringLow price (24h)
vstringBase volume (24h)
qstringQuote volume (24h)
cintTrade count (24h)
mstringMark price (futures only)
fstringFunding rate (futures only)

Mark Price Stream

Mark price updates are automatically subscribed when you subscribe to a Futures order book. Delivered every 1000ms.

{
  "c": "markprice_update",
  "t": 1709500000000,
  "d": [
    { "s": "BTCUSDT", "m": "65235.50", "i": "65234.80", "f": "0.00010", "t": 1709503600000 }
  ]
}
FieldDescription
sSymbol
mMark price
iIndex price
fFunding rate
tNext funding timestamp

4. Data Schema Reference

Position (PositionView)

Each position in the FuturesAccountUpdate.positions[] array:

FieldTypeDescription
positionIdlongUnique position identifier
symbolstringTrading pair (e.g. BTCUSDT)
baseAssetstringBase asset (e.g. BTC)
quoteAssetstringQuote asset (e.g. USDT)
sizedecimalContract size
orderSidestringBuy (Long) or Sell (Short)
marginModestringIsolated or Cross
entryPricedecimalAverage entry price
quantitydecimalCurrent open quantity
leveragedecimalPosition leverage
initialMargindecimalInitial margin amount
marginBalancedecimalCurrent margin balance
maintenanceMargindecimalMaintenance margin requirement
liquidationPricedecimalEstimated liquidation price
unrealizedPnLdecimalUnrealized profit/loss
realizedPnLdecimalRealized profit/loss
markPricedecimalCurrent mark price
fundingFeedecimalAccumulated funding fees
adlintAuto-deleverage indicator (0-5)
marginRatiodecimalMargin ratio percentage
openedAtdatetimePosition open timestamp

Order (OrderDetailResponse)

FieldTypeDescription
orderIdlongUnique order identifier
clientOrderIdstring?Client-assigned order ID
symbolstringTrading pair
orderBookTypestringSpot or Futures
sidestringBuy or Sell
typestringLimit, Market, StopLimit, StopMarket
orderDirectionstringOpen or Close
pricedecimal?Limit price (null for market orders)
quantitydecimal?Original order quantity
executedQuantitydecimalFilled quantity
averagePricedecimalAverage fill price
costdecimalTotal cost
statusstringNew, PartiallyFilled, Filled, Canceled
createddatetimeOrder creation timestamp
updateddatetime?Last update timestamp
stopPricedecimalStop trigger price
stopLossTriggerPricedecimalStop-loss trigger price
takeProfitTriggerPricedecimalTake-profit trigger price
sourcestringOrder source: Web, Api, Mobile

Trade (TradeItem)

FieldTypeDescription
idlongTrade ID
symbolstringTrading pair
pricedecimalExecution price
quantitydecimalExecuted quantity
quoteQuantitydecimalQuote asset amount (price * quantity)
orderSidestringBuy or Sell
isBuyerMakerbooleanWhether the buyer was the maker (resting order)
isBestMatchbooleanBest match indicator
timedatetimeExecution timestamp

Balance (ResponseWallet)

FieldTypeDescription
userIdlongUser identifier
typestringWallet type: Spot or Futures
currencyIdlongCurrency identifier
symbolstringCurrency symbol (e.g. USDT)
currencyNamestringCurrency display name
freedecimalAvailable balance
lockeddecimalIn-trade / margin locked amount
totaldecimalTotal balance (free + locked)
blockdecimalBlocked amount (withdrawal hold etc.)

5. Integration Guide

Recommended Integration Flow

  1. Connect both hubs (/GlobalHub for private data, /PublicMarketData for market data)
  2. Register event handlers before authentication (to avoid missing events during REST fetch)
  3. Authenticate on GlobalHub (JWT or API Key + HMAC)
  4. Fetch initial state via REST API (positions, orders, balance)
  5. Subscribe to market data streams on PublicMarketData hub (snapshot delivered automatically)
  6. Apply deltas from WebSocket events to local state
Important: Register event handlers before fetching REST data. This ensures no events are missed between the REST response and WebSocket handler registration.

Complete Integration Example

import * as signalR from "@microsoft/signalr";
const crypto = require("crypto");

const API_KEY = "your-api-key";
const API_SECRET = "your-api-secret";
const DOMAIN = "domain.com";

// === State ===
const positions = new Map();
const openOrders = new Map();
const balances = new Map();

// === GlobalHub (Private Data) ===
const globalHub = new signalR.HubConnectionBuilder()
    .withUrl("wss://your-domain.com/GlobalHub", {
        transport: signalR.HttpTransportType.WebSockets,
        skipNegotiation: true
    })
    .withAutomaticReconnect([0, 1000, 5000, 10000])
    .build();

// === PublicMarketData Hub ===
const marketHub = new signalR.HubConnectionBuilder()
    .withUrl("wss://your-domain.com/PublicMarketData", {
        transport: signalR.HttpTransportType.WebSockets,
        skipNegotiation: true
    })
    .withAutomaticReconnect([0, 1000, 5000, 10000])
    .build();

// --- Private Event Handlers ---
globalHub.on("FuturesAccountUpdate", (account) => {
    account.positions.forEach(pos => {
        pos.quantity > 0
            ? positions.set(pos.positionId, pos)
            : positions.delete(pos.positionId);
    });
});

globalHub.on("OrderUpdate", (order) => {
    if (order.status === "New" || order.status === "PartiallyFilled") {
        openOrders.set(order.orderId, order);
    } else {
        openOrders.delete(order.orderId);
    }
});

globalHub.on("TradeUpdate", (trade) => {
    const isMaker = (trade.orderSide === "Buy" && trade.isBuyerMaker) ||
                    (trade.orderSide === "Sell" && !trade.isBuyerMaker);
    console.log(`Trade: ${trade.symbol} ${trade.quantity} @ ${trade.price} [${isMaker ? "MAKER" : "TAKER"}]`);
});

globalHub.on("BalanceUpdate", (wallet) => {
    balances.set(`${wallet.type}:${wallet.symbol}`, wallet);
});

// --- Authentication ---
globalHub.on("LR", () => {
    const timestamp = Date.now();
    const signature = crypto
        .createHmac("sha256", API_SECRET)
        .update(timestamp.toString())
        .digest("hex");
    globalHub.invoke("WebSocketLoginWithApiKey", API_KEY, signature, timestamp);
});

globalHub.on("LS", async () => {
    console.log("Authenticated");

    // Fetch initial state via REST
    const [pos, ord, bal] = await Promise.all([
        fetch("/fapi/v1/positionRisk", { headers: authHeaders }).then(r => r.json()),
        fetch("/fapi/v1/openOrders", { headers: authHeaders }).then(r => r.json()),
        fetch("/fapi/v1/balance", { headers: authHeaders }).then(r => r.json())
    ]);

    pos.forEach(p => positions.set(p.positionId, p));
    ord.forEach(o => openOrders.set(o.orderId, o));
    bal.forEach(b => balances.set(`${b.type}:${b.symbol}`, b));
    console.log("State initialized");
});

// --- Market Data Handlers ---
let lastSeqId = 0;
marketHub.on("OrderBookSnapshot", (msg) => {
    initOrderBook(msg);
    lastSeqId = msg.s;
});

marketHub.on("OrderBookUpdate", (msg) => {
    if (msg.s !== lastSeqId + 1) {
        marketHub.invoke("RequestReplay", DOMAIN, "BTCUSDT", "Spot", lastSeqId);
        return;
    }
    applyDelta(msg);
    lastSeqId = msg.s;
});

// --- Start ---
await Promise.all([globalHub.start(), marketHub.start()]);

// Subscribe to market data (snapshot is pushed automatically)
await marketHub.invoke("Subscribe", DOMAIN, "BTCUSDT", "Spot", 500);
await marketHub.invoke("SubscribeMiniTickers", DOMAIN);