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
- Private user data: Positions, orders, trades, and balance updates pushed in real-time
- Public market data: OrderBook snapshots & deltas, mini tickers, mark prices
- Dual authentication: JWT token or API Key + HMAC-SHA256 signature
- Per-user isolation: Private events delivered exclusively to authenticated user channel
- Push-based snapshots: Market data subscribers receive full snapshot on subscribe, then continuous deltas
- Sequence-based gap recovery: OrderBook updates include sequence IDs for gap detection and replay
Hub Endpoints
| Hub | Endpoint | Transport | Purpose |
|---|---|---|---|
| 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")); // FailureMethod 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);
});| Parameter | Type | Description |
|---|---|---|
apiKey | string | API Key from account dashboard |
signature | string | HMAC-SHA256(apiSecret, timestamp.toString()), lowercase hex |
timestamp | long | Unix milliseconds. Must be within 5 seconds of server time. |
Connection & Authentication Flow
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
| Status | Description |
|---|---|
New | Order placed, waiting in order book |
PartiallyFilled | Partially executed, remaining quantity still active |
Filled | Fully executed |
Canceled | Canceled 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
| Event | Payload | Description |
|---|---|---|
AccountUpdate | FuturesAccount | Futures account configuration change (wallet mode, margin call rate) |
Notification | GlobalNotificationMessage | System alerts and announcements |
DepositCryptoUpdate | DepositCryptoResponse | Crypto deposit confirmed |
WithdrawCryptoUpdate | WithdrawCryptoResponse | Crypto withdrawal status change |
KYCUpdate | KYCStatusUpdate | KYC 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}]
}| Field | Type | Description |
|---|---|---|
c | string | Message type: "snapshot" or "update" |
s | long | Sequence ID (continuous per order book, for gap detection) |
t | long | Server timestamp (Unix ms) |
p | string | Pair symbol |
o | string | Order book type: "spot" or "futures" |
d | int | Depth level (snapshot only) |
b | array | Bids: [[price, qty], ...] |
a | array | Asks: [[price, qty], ...] |
tr | array | Recent trades |
"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
| Field | Type | Description |
|---|---|---|
s | string | Symbol (e.g. BTCUSDT) |
o | string | Order book type: spot or futures |
p | string | Last price |
op | string | Open price (24h) |
h | string | High price (24h) |
l | string | Low price (24h) |
v | string | Base volume (24h) |
q | string | Quote volume (24h) |
c | int | Trade count (24h) |
m | string | Mark price (futures only) |
f | string | Funding 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 }
]
}| Field | Description |
|---|---|
s | Symbol |
m | Mark price |
i | Index price |
f | Funding rate |
t | Next funding timestamp |
4. Data Schema Reference
Position (PositionView)
Each position in the FuturesAccountUpdate.positions[] array:
| Field | Type | Description |
|---|---|---|
positionId | long | Unique position identifier |
symbol | string | Trading pair (e.g. BTCUSDT) |
baseAsset | string | Base asset (e.g. BTC) |
quoteAsset | string | Quote asset (e.g. USDT) |
size | decimal | Contract size |
orderSide | string | Buy (Long) or Sell (Short) |
marginMode | string | Isolated or Cross |
entryPrice | decimal | Average entry price |
quantity | decimal | Current open quantity |
leverage | decimal | Position leverage |
initialMargin | decimal | Initial margin amount |
marginBalance | decimal | Current margin balance |
maintenanceMargin | decimal | Maintenance margin requirement |
liquidationPrice | decimal | Estimated liquidation price |
unrealizedPnL | decimal | Unrealized profit/loss |
realizedPnL | decimal | Realized profit/loss |
markPrice | decimal | Current mark price |
fundingFee | decimal | Accumulated funding fees |
adl | int | Auto-deleverage indicator (0-5) |
marginRatio | decimal | Margin ratio percentage |
openedAt | datetime | Position open timestamp |
Order (OrderDetailResponse)
| Field | Type | Description |
|---|---|---|
orderId | long | Unique order identifier |
clientOrderId | string? | Client-assigned order ID |
symbol | string | Trading pair |
orderBookType | string | Spot or Futures |
side | string | Buy or Sell |
type | string | Limit, Market, StopLimit, StopMarket |
orderDirection | string | Open or Close |
price | decimal? | Limit price (null for market orders) |
quantity | decimal? | Original order quantity |
executedQuantity | decimal | Filled quantity |
averagePrice | decimal | Average fill price |
cost | decimal | Total cost |
status | string | New, PartiallyFilled, Filled, Canceled |
created | datetime | Order creation timestamp |
updated | datetime? | Last update timestamp |
stopPrice | decimal | Stop trigger price |
stopLossTriggerPrice | decimal | Stop-loss trigger price |
takeProfitTriggerPrice | decimal | Take-profit trigger price |
source | string | Order source: Web, Api, Mobile |
Trade (TradeItem)
| Field | Type | Description |
|---|---|---|
id | long | Trade ID |
symbol | string | Trading pair |
price | decimal | Execution price |
quantity | decimal | Executed quantity |
quoteQuantity | decimal | Quote asset amount (price * quantity) |
orderSide | string | Buy or Sell |
isBuyerMaker | boolean | Whether the buyer was the maker (resting order) |
isBestMatch | boolean | Best match indicator |
time | datetime | Execution timestamp |
Balance (ResponseWallet)
| Field | Type | Description |
|---|---|---|
userId | long | User identifier |
type | string | Wallet type: Spot or Futures |
currencyId | long | Currency identifier |
symbol | string | Currency symbol (e.g. USDT) |
currencyName | string | Currency display name |
free | decimal | Available balance |
locked | decimal | In-trade / margin locked amount |
total | decimal | Total balance (free + locked) |
block | decimal | Blocked amount (withdrawal hold etc.) |
5. Integration Guide
Recommended Integration Flow
- Connect both hubs (
/GlobalHubfor private data,/PublicMarketDatafor market data) - Register event handlers before authentication (to avoid missing events during REST fetch)
- Authenticate on GlobalHub (JWT or API Key + HMAC)
- Fetch initial state via REST API (positions, orders, balance)
- Subscribe to market data streams on PublicMarketData hub (snapshot delivered automatically)
- Apply deltas from WebSocket events to local state
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);