WebSocket API Documentation
Real-time data streaming via SignalR WebSocket hubs. This service provides two hubs for different use cases:
| Hub | Endpoint | Purpose |
|---|---|---|
GlobalHub |
/GlobalHub |
User notifications, balance updates, order updates, general real-time events |
PublicMarketDataHub |
/PublicMarketData |
Order book streaming, mini tickers, mark prices, public market data |
Protocol
- Transport: WebSocket (SignalR)
- All timestamps are in Unix milliseconds (UTC)
- Wire format uses compact field names for minimal bandwidth
- Prices and quantities are sent as strings to preserve decimal precision
Connection
Connect to hubs using the SignalR client library for your platform. The connection uses WebSocket transport exclusively.
Hub URLs
# GlobalHub - for user-specific real-time updates
wss://your-domain.com/GlobalHub
# PublicMarketData - for market data streaming
wss://your-domain.com/PublicMarketDataConnection Examples
import * as signalR from "@microsoft/signalr";
// GlobalHub connection
const globalHub = new signalR.HubConnectionBuilder()
.withUrl("https://your-domain.com/GlobalHub", {
transport: signalR.HttpTransportType.WebSockets,
skipNegotiation: true
})
.withAutomaticReconnect()
.build();
// PublicMarketData connection
const marketHub = new signalR.HubConnectionBuilder()
.withUrl("https://your-domain.com/PublicMarketData", {
transport: signalR.HttpTransportType.WebSockets,
skipNegotiation: true
})
.withAutomaticReconnect()
.build();
await globalHub.start();
await marketHub.start();from signalrcore.hub_connection_builder import HubConnectionBuilder
# GlobalHub connection
global_hub = HubConnectionBuilder() \
.with_url("https://your-domain.com/GlobalHub") \
.with_automatic_reconnect({
"type": "raw",
"keep_alive_interval": 15,
"reconnect_interval": 5
}) \
.build()
# PublicMarketData connection
market_hub = HubConnectionBuilder() \
.with_url("https://your-domain.com/PublicMarketData") \
.with_automatic_reconnect({
"type": "raw",
"keep_alive_interval": 15,
"reconnect_interval": 5
}) \
.build()
global_hub.start()
market_hub.start()using Microsoft.AspNetCore.SignalR.Client;
// GlobalHub connection
var globalHub = new HubConnectionBuilder()
.WithUrl("https://your-domain.com/GlobalHub", options =>
{
options.Transports = HttpTransportType.WebSockets;
options.SkipNegotiation = true;
})
.WithAutomaticReconnect()
.Build();
// PublicMarketData connection
var marketHub = new HubConnectionBuilder()
.WithUrl("https://your-domain.com/PublicMarketData", options =>
{
options.Transports = HttpTransportType.WebSockets;
options.SkipNegotiation = true;
})
.WithAutomaticReconnect()
.Build();
await globalHub.StartAsync();
await marketHub.StartAsync();Authentication
Both hubs support authentication via JWT token. Authentication is required for receiving private events (balance updates, order updates, etc.).
GlobalHub Authentication
After connecting, the server sends an LR (Login Request) event. Send your JWT token via WebSocketLogin:
// Listen for login request
globalHub.on("LR", () => {
// Send JWT token
globalHub.invoke("WebSocketLogin", "your-jwt-token");
});
// Listen for login success
globalHub.on("LS", () => {
console.log("Authenticated successfully");
});
// Listen for login failure
globalHub.on("LF", () => {
console.log("Authentication failed");
});GlobalHub Authentication (API Key)
Alternatively, authenticate with your API Key + HMAC signature (same credentials used for the REST Trade API):
// Sign the timestamp with your API secret
const crypto = require("crypto");
const apiKey = "your-api-key";
const apiSecret = "your-api-secret";
const timestamp = Date.now();
const signature = crypto
.createHmac("sha256", apiSecret)
.update(timestamp.toString())
.digest("hex");
// Send API key login
globalHub.invoke("WebSocketLoginWithApiKey", apiKey, signature, timestamp);
// Same response events: LS (success) / LF (failure)
globalHub.on("LS", () => console.log("API key auth successful"));
globalHub.on("LF", () => console.log("API key auth failed"));Ping method to measure clock drift.
PublicMarketData Authentication
Authentication is optional on PublicMarketData hub. Only required for receiving private updates (balance, orders):
// Authenticate (optional - only for private events)
marketHub.invoke("Authenticate", "your-jwt-token");
// Listen for auth result
marketHub.on("AuthSuccess", (data) => {
console.log("Authenticated as user:", data.userId);
});
marketHub.on("AuthFailed", (data) => {
console.log("Auth failed:", data.reason);
});Connection Flow
GlobalHub Flow (JWT Token)
GlobalHub Flow (API Key)
PublicMarketData Flow
GlobalHub
The GlobalHub handles user-specific real-time updates such as notifications, balance changes, order updates, and general application events.
| Property | Value |
|---|---|
| Endpoint | /GlobalHub |
| Transport | WebSocket only |
| Buffer Size | 64KB send/receive |
| Authentication | JWT token via WebSocketLogin |
Send initial page/context information after connecting. Tells the server which page the client is viewing.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
page | string | Yes | Page identifier (max 64 chars). Example: "trade", "wallet", "dashboard" |
Example
await globalHub.invoke("WebSocketInit", "trade");Authenticate with JWT token. Call this after receiving the LR event. On success, the server sends LS; on failure, LF and the connection is aborted.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
token | string | Yes | JWT bearer token |
Response Events
| Event | Description |
|---|---|
LS | Login successful - you will now receive private events |
LF | Login failed - invalid token or unauthorized |
Example
await globalHub.invoke("WebSocketLogin", "eyJhbGciOiJIUzI1Ni...");Authenticate using API Key + HMAC-SHA256 signature. Uses the same API key credentials as the REST Trade API. On success: LS, on failure: LF.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
apiKey | string | Yes | Your API Key (from account dashboard) |
signature | string | Yes | HMAC-SHA256 signature of the timestamp, using your API Secret as the key. Lowercase hex. |
timestamp | long | Yes | Current time in Unix milliseconds. Must be within 5 seconds of server time. |
Signing Process
- Get current timestamp:
Date.now()(Unix milliseconds) - Convert to string:
"1709500000000" - Sign with HMAC-SHA256 using your API Secret
- Output as lowercase hex string
Response Events
| Event | Description |
|---|---|
LS | Login successful |
LF | Login failed (invalid key, bad signature, timestamp expired, or IP not allowed) |
Code Examples
const crypto = require("crypto");
const API_KEY = "your-api-key";
const API_SECRET = "your-api-secret";
function loginWithApiKey(hub) {
const timestamp = Date.now();
const signature = crypto
.createHmac("sha256", API_SECRET)
.update(timestamp.toString())
.digest("hex");
hub.invoke("WebSocketLoginWithApiKey", API_KEY, signature, timestamp);
}
// Listen for result
hub.on("LS", () => console.log("Authenticated via API key"));
hub.on("LF", () => console.log("API key authentication failed"));import hmac
import hashlib
import time
API_KEY = "your-api-key"
API_SECRET = "your-api-secret"
def login_with_api_key(hub):
timestamp = int(time.time() * 1000)
signature = hmac.new(
API_SECRET.encode("utf-8"),
str(timestamp).encode("utf-8"),
hashlib.sha256
).hexdigest()
hub.send("WebSocketLoginWithApiKey", [API_KEY, signature, timestamp])using System.Security.Cryptography;
using System.Text;
var apiKey = "your-api-key";
var apiSecret = "your-api-secret";
var timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(apiSecret));
var hash = hmac.ComputeHash(Encoding.UTF8.GetBytes(timestamp.ToString()));
var signature = Convert.ToHexStringLower(hash);
await hub.InvokeAsync("WebSocketLoginWithApiKey", apiKey, signature, timestamp);Subscribe to a channel. Uses single-active-channel semantics: joining a new channel automatically leaves all previous channels for this connection.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
channel | string | Yes | Channel name (lowercase letters, digits, :, _, -, max 64 chars). Example: "btcusdt" |
[a-z0-9:_-]{1,64}. Invalid names are silently rejected.
Example
// Switch to BTCUSDT channel (auto-leaves previous)
await globalHub.invoke("JoinChannel", "btcusdt");Subscribe to mini ticker updates for both Spot and Futures markets. Requires an active connection (hub user must exist).
Parameters
None
Response Events
| Event | Description |
|---|---|
RC | Reconnect needed - connection state not found. Reconnect and try again. |
Example
await globalHub.invoke("SubscribeTickers");Update the user's current page context. Used for analytics and targeted notifications.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
lastpage | string | Yes | Current page identifier |
Example
await globalHub.invoke("SetLastPage", "wallet/deposit");Subscribe to QR code login session updates. Used for login-via-mobile-app flow.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
sessionId | string | Yes | QR login session identifier |
Response Events
| Event | Payload | Description |
|---|---|---|
QRSuccess | - | QR session subscription confirmed |
QRLogin | UserLoginResponse | QR login completed by mobile app |
Example
await globalHub.invoke("SubscribeQRLogin", "session-uuid-here");
globalHub.on("QRLogin", (loginData) => {
console.log("QR login completed:", loginData);
});Keep-alive ping. Server responds with Pong echoing the timestamp.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
timestamp | long | Yes | Client timestamp (Unix milliseconds) |
Response
Event: Pong with the same timestamp value.
Example
await globalHub.invoke("Ping", Date.now());
globalHub.on("Pong", (timestamp) => {
const rtt = Date.now() - timestamp;
console.log(`RTT: ${rtt}ms`);
});GlobalHub - Server Events
Events sent from server to client on the GlobalHub. Private events require authentication via WebSocketLogin.
| Event | Auth | Payload | Description |
|---|---|---|---|
LR | PUBLIC | - | Login request - sent on connect, prompt user to authenticate |
LS | PUBLIC | - | Login success |
LF | PUBLIC | - | Login failed |
RC | PUBLIC | - | Reconnect needed - connection state lost |
Pong | PUBLIC | long | Ping response with echoed timestamp |
QRSuccess | PUBLIC | - | QR login session subscription confirmed |
QRLogin | PUBLIC | UserLoginResponse | QR login completed |
BalanceUpdate | PRIVATE | ResponseWallet | Wallet balance changed |
OrderUpdate | PRIVATE | OrderDetailResponse | Order status changed |
TradeUpdate | PRIVATE | TradeItem | Trade executed |
Notification | PRIVATE | GlobalNotificationMessage | General notification |
FuturesAccountUpdate | PRIVATE | FuturesAccountView | Futures account state changed |
AccountUpdate | PRIVATE | FuturesAccount | Futures account update |
KYCUpdate | PRIVATE | KYCStatusUpdate | KYC verification status changed |
DepositCryptoUpdate | PRIVATE | DepositCryptoResponse | Crypto deposit received |
WithdrawCryptoUpdate | PRIVATE | WithdrawCryptoResponse | Crypto withdrawal status changed |
AffiliateCodeUpdate | PRIVATE | AffiliateCodeResponse | Affiliate code updated |
AffiliateStatUpdate | PRIVATE | AffiliateUserStatResponse | Affiliate stats changed |
PublicMarketData Hub
High-performance hub for real-time market data streaming. Optimized for order book updates, mini tickers, and mark prices.
| Property | Value |
|---|---|
| Endpoint | /PublicMarketData |
| Transport | WebSocket only |
| Buffer Size | 128KB send/receive |
| Authentication | Optional (JWT via Authenticate) |
| OrderBook Update Interval | 250ms (aggregated) |
| Ticker Update Interval | 500ms (delta only) |
| Mark Price Update Interval | 1000ms |
Connected event with your connection ID, server time, and list of available order books.
Subscribe to an order book's real-time updates. Receives a snapshot immediately followed by continuous delta updates every 250ms.
Parameters
| Name | Type | Required | Default | Description |
|---|---|---|---|---|
domain | string | Yes | - | Tenant domain (e.g. "darkex.com") |
symbol | string | Yes | - | Trading pair symbol (e.g. "BTCUSDT") |
orderBookType | string | Yes | - | "Spot" or "Futures" |
depthLevel | int | No | 500 | Depth level: 50, 100, 500, or 1000 |
Response Events
| Event | Description |
|---|---|
Subscribed | Subscription confirmed with orderBookId and depth |
OrderBookSnapshot | Full order book snapshot (immediate) |
OrderBookUpdate | Delta updates (every 250ms) |
MarkPriceUpdate | Mark price updates for Futures (every 1000ms, auto-subscribed) |
Error | If tenant or pair not found |
Example
// Subscribe to BTCUSDT spot order book with depth 500
await marketHub.invoke("Subscribe", "darkex.com", "BTCUSDT", "Spot", 500);
// Listen for snapshot
marketHub.on("OrderBookSnapshot", (snapshot) => {
console.log("Snapshot:", snapshot.p, "Seq:", snapshot.s);
console.log("Bids:", snapshot.b.length, "Asks:", snapshot.a.length);
lastSeqId = snapshot.s;
});
// Listen for delta updates
marketHub.on("OrderBookUpdate", (update) => {
if (update.s !== lastSeqId + 1) {
console.log("Gap detected! Requesting replay...");
marketHub.invoke("RequestReplay", "darkex.com", "BTCUSDT", "Spot", lastSeqId);
return;
}
lastSeqId = update.s;
applyDelta(update);
});Unsubscribe from an order book's updates. Also removes mark price subscription for futures.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
domain | string | Yes | Tenant domain |
symbol | string | Yes | Trading pair symbol |
orderBookType | string | Yes | "Spot" or "Futures" |
Response
Event: Unsubscribed with the orderBookId.
Example
await marketHub.invoke("Unsubscribe", "darkex.com", "BTCUSDT", "Spot");Request a fresh order book snapshot. Useful for manual recovery or initial data load.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
domain | string | Yes | Tenant domain |
symbol | string | Yes | Trading pair symbol |
orderBookType | string | Yes | "Spot" or "Futures" |
Response
Event: OrderBookSnapshot or Error if not found.
Example
await marketHub.invoke("GetSnapshot", "darkex.com", "BTCUSDT", "Spot");Request replay of missed updates when a sequence gap is detected. The server stores the last 20 updates (~5 seconds) in a ring buffer. If the requested sequence is within the buffer, individual deltas are replayed. Otherwise, a fresh snapshot is sent.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
domain | string | Yes | Tenant domain |
symbol | string | Yes | Trading pair symbol |
orderBookType | string | Yes | "Spot" or "Futures" |
lastBroadcastSeqId | long | Yes | The last BroadcastSeqId successfully applied by the client |
Response
Either multiple OrderBookUpdate events (if replay possible) or a single OrderBookSnapshot (if too old).
Example
// When a sequence gap is detected
if (update.s !== lastSeqId + 1) {
await marketHub.invoke("RequestReplay", "darkex.com", "BTCUSDT", "Spot", lastSeqId);
}Subscribe to mini ticker updates for a specific tenant. Receives a full snapshot immediately, then delta updates every 500ms (only changed tickers).
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
domain | string | Yes | Tenant domain (e.g. "darkex.com") |
Response Events
| Event | Description |
|---|---|
MiniTickerSubscribed | Subscription confirmed with ticker count |
TickerSnapshot | Full ticker snapshot (immediate) |
TickerUpdate | Delta updates (every 500ms, only changed tickers) |
Error | If tenant not found |
Example
await marketHub.invoke("SubscribeMiniTickers", "darkex.com");
marketHub.on("TickerSnapshot", (snapshot) => {
console.log("Received", snapshot.d.length, "tickers");
snapshot.d.forEach(t => {
console.log(t.s, t.p); // symbol, lastPrice
});
});
marketHub.on("TickerUpdate", (update) => {
update.d.forEach(t => {
updateTickerUI(t.s, t);
});
});Unsubscribe from mini ticker updates.
Parameters
None
Response
Event: MiniTickerUnsubscribed with timestamp.
Example
await marketHub.invoke("UnsubscribeMiniTickers");Authenticate with JWT token to receive private updates (balance changes, order updates). Optional — public market data works without authentication.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
token | string | Yes | JWT bearer token |
Response Events
| Event | Payload | Description |
|---|---|---|
AuthSuccess | { userId } | Authentication successful |
AuthFailed | { reason } | Authentication failed |
Example
await marketHub.invoke("Authenticate", "eyJhbGciOiJIUzI1Ni...");Get a list of all available order books that can be subscribed to.
Parameters
None
Response
Event: AvailableOrderBooks with array of order book objects:
[
{
"orderBookId": 1,
"domain": "",
"symbol": "BTCUSDT",
"orderBookType": "Spot"
},
{
"orderBookId": 2,
"domain": "",
"symbol": "ETHUSDT",
"orderBookType": "Spot"
}
]Get your current active subscriptions for this connection.
Parameters
None
Response
Event: MySubscriptions
{
"orderBooks": [1, 5, 12],
"miniTickers": true
}Get connection status and system health information.
Parameters
None
Response
Event: Status
{
"anyConnected": true,
"allConnected": true,
"totalClients": 3,
"connectedClients": 3,
"totalSnapshots": 24,
"totalOrderBooks": 24,
"totalTickers": 48,
"isAuthenticated": false,
"miniTickerSubscribers": 150
}Latency measurement with detailed timing. Returns server receive and send timestamps for precise RTT calculation.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
id | long | Yes | Sequence ID to match ping/pong pairs (use incrementing counter) |
t1 | long | Yes | Client send timestamp (Unix milliseconds) |
Response
Event: Pong
{
"id": 1,
"t1": 1709500000000,
"t2": 1709500000005,
"t3": 1709500000006
}Latency Calculation
let pingId = 0;
async function measureLatency() {
const id = ++pingId;
const t1 = Date.now();
await marketHub.invoke("Ping", id, t1);
}
marketHub.on("Pong", (pong) => {
const t4 = Date.now();
const rtt = t4 - pong.t1; // Total round-trip
const serverProcessing = pong.t3 - pong.t2; // Server processing time
const networkRtt = rtt - serverProcessing; // Network round-trip
const oneWayLatency = networkRtt / 2; // Approximate one-way
console.log(`RTT: ${rtt}ms, Network: ${networkRtt}ms, OneWay: ~${oneWayLatency}ms`);
});OrderBook Events
Events related to order book subscriptions on the PublicMarketData hub.
Connected
Sent immediately when client connects.
{
"connectionId": "abc123...",
"serverTimeUtc": "2025-01-15T10:30:00Z",
"availableOrderBooks": [
{ "orderBookId": 1, "symbol": "BTCUSDT", "orderBookType": "Spot" },
{ "orderBookId": 2, "symbol": "ETHUSDT", "orderBookType": "Spot" }
]
}OrderBookSnapshot
Full order book state. Sent on subscription or replay fallback. Compact 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 }
]
}| Field | Type | Description |
|---|---|---|
c | string | Message type: "snapshot" |
s | long | BroadcastSeqId - use for gap detection |
t | long | Server timestamp (Unix ms) |
p | string | Pair symbol (e.g. "BTCUSDT") |
o | string | Order book type: "spot" or "futures" |
d | int | Depth level |
b | array | Bids: [[price, qty], ...] (descending by price) |
a | array | Asks: [[price, qty], ...] (ascending by price) |
tr | array? | Last 10 recent trades (optional) |
OrderBookUpdate
Delta update sent every 250ms with changed price levels. Apply to local order book state:
{
"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 }
]
}- A quantity of
"0"means remove this price level - A non-zero quantity means replace the entire quantity at that price level
- Sequence IDs (
s) are continuous per order book. Ifsis not previous + 1, callRequestReplay
Trade Fields (tr)
| Field | Type | Description |
|---|---|---|
i | long | Trade ID |
p | string | Price |
q | string | Quantity |
m | bool | Is buyer maker (true = sell aggressor) |
t | long | Trade timestamp (Unix ms) |
Ticker Events
TickerSnapshot
Full ticker snapshot sent immediately on subscription:
{
"c": "ticker_snapshot",
"t": 1709500000000,
"d": [
{
"s": "BTCUSDT",
"o": "spot",
"p": "65230.50",
"op": "64800.00",
"h": "65500.00",
"l": "64500.00",
"v": "1234.567",
"q": "80500000.00",
"c": 15234,
"m": "0",
"f": "0",
"cd": 0
}
]
}TickerUpdate
Delta update sent every 500ms containing only changed tickers:
{
"c": "ticker_update",
"t": 1709500000500,
"d": [
{ "s": "BTCUSDT", "o": "spot", "p": "65231.00", "v": "1234.890" }
]
}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 | Volume (base asset) |
q | string | Quote volume |
c | int | Trade count (24h) |
m | string | Mark price (futures only) |
f | string | Funding rate (futures only) |
cd | long | Funding countdown timestamp (futures only) |
MarkPrice Events
Mark price updates for futures order books. Automatically subscribed when you subscribe to a futures order book.
MarkPriceUpdate
Sent every 1000ms for subscribed futures order books:
{
"c": "markprice_update",
"t": 1709500000000,
"d": [
{
"s": "BTCUSDT",
"m": "65235.50",
"i": "65234.80",
"f": "0.00010",
"t": 1709503600000
}
]
}MarkPrice Fields
| Field | Type | Description |
|---|---|---|
s | string | Symbol |
m | string | Mark price |
i | string | Index price |
f | string | Funding rate |
t | long | Next funding timestamp |
Private Events
Private events require authentication. On GlobalHub, authenticate via WebSocketLogin. On PublicMarketData, use Authenticate.
BalanceUpdate
Sent when user's wallet balance changes (deposits, trades, withdrawals).
globalHub.on("BalanceUpdate", (wallet) => {
console.log("Balance updated:", wallet);
// wallet contains: currency, available, locked, total, etc.
});OrderUpdate
Sent when a user's order status changes (new, filled, partially filled, cancelled).
globalHub.on("OrderUpdate", (order) => {
console.log("Order updated:", order.orderId, order.status);
});TradeUpdate
Sent when a user's trade is executed.
globalHub.on("TradeUpdate", (trade) => {
console.log("Trade executed:", trade);
});FuturesAccountUpdate
Sent when futures account state changes (margin, positions).
Notification
General notifications (system alerts, announcements).
DepositCryptoUpdate / WithdrawCryptoUpdate
Crypto deposit/withdrawal status updates.
KYCUpdate
KYC verification status changes.
Wire Format - OrderBook
Order book messages use compact single-letter field names for minimal bandwidth.
Snapshot Message
{
"c": "snapshot", // message type
"s": 42, // BroadcastSeqId
"t": 1709500000000, // server timestamp (Unix ms)
"p": "BTCUSDT", // pair symbol
"o": "spot", // orderbook type
"d": 500, // depth level
"b": [ // bids [[price, qty], ...]
["65230.50", "1.234"],
["65230.00", "0.567"]
],
"a": [ // asks [[price, qty], ...]
["65231.00", "0.890"],
["65231.50", "2.345"]
],
"tr": [ // recent trades (last 10)
{ "i": 1001, "p": "65230.75", "q": "0.100", "m": true, "t": 1709499999500 }
]
}Update Message
{
"c": "update", // message type
"s": 43, // BroadcastSeqId (continuous)
"t": 1709500000250, // server timestamp
"p": "BTCUSDT", // pair symbol
"o": "spot", // orderbook type
"b": [["65230.50", "1.500"]], // changed bids
"a": [["65231.00", "0"]], // qty "0" = remove level
"tr": [ // new trades since last update
{ "i": 1002, "p": "65230.50", "q": "0.050", "m": false, "t": 1709500000100 }
]
}Wire Format - Ticker
Ticker Snapshot
{
"c": "ticker_snapshot",
"t": 1709500000000,
"d": [
{ "s": "BTCUSDT", "o": "spot", "p": "65230.50", "op": "64800.00", "h": "65500.00", "l": "64500.00", "v": "1234.567", "q": "80500000.00", "c": 15234 },
{ "s": "ETHUSDT", "o": "spot", "p": "3450.25", "op": "3400.00", "h": "3480.00", "l": "3390.00", "v": "45678.901", "q": "157000000.00", "c": 28456 }
]
}Ticker Update (Delta)
{
"c": "ticker_update",
"t": 1709500000500,
"d": [
{ "s": "BTCUSDT", "o": "spot", "p": "65231.00", "v": "1234.890" }
]
}Wire Format - MarkPrice
{
"c": "markprice_update",
"t": 1709500000000,
"d": [
{ "s": "BTCUSDT", "m": "65235.50", "i": "65234.80", "f": "0.00010", "t": 1709503600000 }
]
}Sequence & Gap Detection
Each order book has an independent BroadcastSeqId sequence (1, 2, 3, ...). Use this for gap detection:
How It Works
- On
OrderBookSnapshot: storelastSeqId = snapshot.s - On
OrderBookUpdate: check ifupdate.s === lastSeqId + 1 - If gap detected: call
RequestReplay(domain, symbol, type, lastSeqId) - After replay: continue normal processing
Implementation
class OrderBookClient {
constructor(hub, domain, symbol, type) {
this.hub = hub;
this.domain = domain;
this.symbol = symbol;
this.type = type;
this.lastSeqId = 0;
this.bids = new Map(); // price -> qty
this.asks = new Map(); // price -> qty
hub.on("OrderBookSnapshot", (msg) => this.onSnapshot(msg));
hub.on("OrderBookUpdate", (msg) => this.onUpdate(msg));
}
onSnapshot(msg) {
if (msg.p !== this.symbol) return;
this.bids.clear();
this.asks.clear();
msg.b.forEach(([p, q]) => this.bids.set(p, q));
msg.a.forEach(([p, q]) => this.asks.set(p, q));
this.lastSeqId = msg.s;
}
onUpdate(msg) {
if (msg.p !== this.symbol) return;
// Gap detection
if (msg.s !== this.lastSeqId + 1) {
console.warn(`Gap: expected ${this.lastSeqId + 1}, got ${msg.s}`);
this.hub.invoke("RequestReplay",
this.domain, this.symbol, this.type, this.lastSeqId);
return;
}
// Apply delta
msg.b.forEach(([p, q]) => {
q === "0" ? this.bids.delete(p) : this.bids.set(p, q);
});
msg.a.forEach(([p, q]) => {
q === "0" ? this.asks.delete(p) : this.asks.set(p, q);
});
this.lastSeqId = msg.s;
}
async subscribe(depth = 500) {
await this.hub.invoke("Subscribe",
this.domain, this.symbol, this.type, depth);
}
}Reconnection Guide
Use SignalR's built-in automatic reconnection. After reconnection, you must re-subscribe to all channels and data streams.
Reconnection Strategy
const hub = new signalR.HubConnectionBuilder()
.withUrl("https://your-domain.com/PublicMarketData", {
transport: signalR.HttpTransportType.WebSockets,
skipNegotiation: true
})
.withAutomaticReconnect([0, 1000, 5000, 10000, 30000])
.build();
hub.onreconnecting((error) => {
console.log("Reconnecting...", error);
showReconnectingUI();
});
hub.onreconnected((connectionId) => {
console.log("Reconnected:", connectionId);
hideReconnectingUI();
// Re-subscribe to all streams
resubscribeAll();
});
hub.onclose((error) => {
console.log("Connection closed:", error);
// Attempt manual reconnect after delay
setTimeout(() => hub.start(), 5000);
});
async function resubscribeAll() {
// Re-authenticate if needed
if (authToken) {
await hub.invoke("Authenticate", authToken);
}
// Re-subscribe to order books
for (const sub of activeSubscriptions) {
await hub.invoke("Subscribe",
sub.domain, sub.symbol, sub.type, sub.depth);
}
// Re-subscribe to tickers
if (tickerSubscribed) {
await hub.invoke("SubscribeMiniTickers", currentDomain);
}
}Complete Code Examples
JavaScript - Full Market Data Client
import * as signalR from "@microsoft/signalr";
const DOMAIN = "darkex.com";
const WS_URL = "https://your-domain.com/PublicMarketData";
class MarketDataClient {
constructor() {
this.hub = new signalR.HubConnectionBuilder()
.withUrl(WS_URL, {
transport: signalR.HttpTransportType.WebSockets,
skipNegotiation: true
})
.withAutomaticReconnect([0, 1000, 5000, 10000])
.build();
this.setupEventHandlers();
}
setupEventHandlers() {
// Connection events
this.hub.on("Connected", (data) => {
console.log("Connected:", data.connectionId);
console.log("Available order books:", data.availableOrderBooks.length);
});
// Order book events
this.hub.on("Subscribed", (data) => {
console.log("Subscribed to OB:", data.orderBookId, "Depth:", data.depthLevel);
});
this.hub.on("OrderBookSnapshot", (msg) => {
console.log(`Snapshot [${msg.p}]: ${msg.b.length} bids, ${msg.a.length} asks, seq=${msg.s}`);
});
this.hub.on("OrderBookUpdate", (msg) => {
console.log(`Update [${msg.p}]: seq=${msg.s}`);
});
// Ticker events
this.hub.on("MiniTickerSubscribed", (data) => {
console.log("Ticker subscribed:", data.tickerCount, "tickers");
});
this.hub.on("TickerSnapshot", (msg) => {
console.log("Ticker snapshot:", msg.d.length, "tickers");
});
this.hub.on("TickerUpdate", (msg) => {
msg.d.forEach(t => console.log(`${t.s}: ${t.p}`));
});
// Mark price events
this.hub.on("MarkPriceUpdate", (msg) => {
msg.d.forEach(mp => console.log(`MarkPrice [${mp.s}]: ${mp.m}`));
});
// Error handling
this.hub.on("Error", (err) => {
console.error("Server error:", err.code, err.message);
});
// Pong
this.hub.on("Pong", (pong) => {
const rtt = Date.now() - pong.t1;
console.log(`Pong: RTT=${rtt}ms`);
});
// Reconnection
this.hub.onreconnected(() => this.resubscribe());
}
async connect() {
await this.hub.start();
console.log("Hub started");
}
async subscribeTicker() {
await this.hub.invoke("SubscribeMiniTickers", DOMAIN);
}
async subscribeOrderBook(symbol, type = "Spot", depth = 500) {
await this.hub.invoke("Subscribe", DOMAIN, symbol, type, depth);
}
async resubscribe() {
await this.subscribeTicker();
await this.subscribeOrderBook("BTCUSDT");
}
async ping() {
await this.hub.invoke("Ping", 1, Date.now());
}
}
// Usage
const client = new MarketDataClient();
await client.connect();
await client.subscribeTicker();
await client.subscribeOrderBook("BTCUSDT", "Spot", 500);Python - Market Data Client
from signalrcore.hub_connection_builder import HubConnectionBuilder
import time
DOMAIN = "darkex.com"
WS_URL = "https://your-domain.com/PublicMarketData"
def on_connected(data):
print(f"Connected: {data[0]['connectionId']}")
print(f"Available order books: {len(data[0]['availableOrderBooks'])}")
def on_snapshot(data):
msg = data[0]
print(f"Snapshot [{msg['p']}]: {len(msg['b'])} bids, {len(msg['a'])} asks, seq={msg['s']}")
def on_update(data):
msg = data[0]
print(f"Update [{msg['p']}]: seq={msg['s']}")
def on_ticker_snapshot(data):
msg = data[0]
print(f"Ticker snapshot: {len(msg['d'])} tickers")
def on_ticker_update(data):
for t in data[0]["d"]:
print(f"{t['s']}: {t['p']}")
def on_error(data):
print(f"Error: {data[0]['code']} - {data[0]['message']}")
# Build connection
hub = HubConnectionBuilder() \
.with_url(WS_URL) \
.with_automatic_reconnect({
"type": "raw",
"keep_alive_interval": 15,
"reconnect_interval": 5
}) \
.build()
# Register handlers
hub.on("Connected", on_connected)
hub.on("OrderBookSnapshot", on_snapshot)
hub.on("OrderBookUpdate", on_update)
hub.on("TickerSnapshot", on_ticker_snapshot)
hub.on("TickerUpdate", on_ticker_update)
hub.on("Error", on_error)
# Connect and subscribe
hub.start()
time.sleep(1)
hub.send("SubscribeMiniTickers", [DOMAIN])
hub.send("Subscribe", [DOMAIN, "BTCUSDT", "Spot", 500])
# Keep alive
try:
while True:
hub.send("Ping", [1, int(time.time() * 1000)])
time.sleep(30)
except KeyboardInterrupt:
hub.stop()C# - Market Data Client
using Microsoft.AspNetCore.SignalR.Client;
const string Domain = "darkex.com";
const string WsUrl = "https://your-domain.com/PublicMarketData";
var hub = new HubConnectionBuilder()
.WithUrl(WsUrl, options =>
{
options.Transports = HttpTransportType.WebSockets;
options.SkipNegotiation = true;
})
.WithAutomaticReconnect()
.Build();
// Order book snapshot
hub.On<object>("OrderBookSnapshot", (msg) =>
{
Console.WriteLine($"Snapshot received");
});
// Order book update
hub.On<object>("OrderBookUpdate", (msg) =>
{
Console.WriteLine($"Update received");
});
// Ticker events
hub.On<object>("TickerSnapshot", (msg) =>
{
Console.WriteLine("Ticker snapshot received");
});
hub.On<object>("TickerUpdate", (msg) =>
{
Console.WriteLine("Ticker update received");
});
// Error
hub.On<object>("Error", (err) =>
{
Console.WriteLine($"Error: {err}");
});
// Connect
await hub.StartAsync();
Console.WriteLine("Connected!");
// Subscribe
await hub.InvokeAsync("SubscribeMiniTickers", Domain);
await hub.InvokeAsync("Subscribe", Domain, "BTCUSDT", "Spot", 500);
// Keep alive
while (true)
{
await hub.InvokeAsync("Ping", 1L, DateTimeOffset.UtcNow.ToUnixTimeMilliseconds());
await Task.Delay(30000);
}Error Codes
The Error event is sent with a code and message when a request fails:
{
"code": "TENANT_NOT_FOUND",
"message": "Tenant not found for domain: invalid.com"
}| Code | Description |
|---|---|
TENANT_NOT_FOUND | The specified domain does not match any tenant |
PAIR_NOT_FOUND | The specified symbol/type combination does not exist for this tenant |
ORDER_BOOK_NOT_FOUND | Order book not found (internal) |
SNAPSHOT_NOT_FOUND | Snapshot not available yet for the requested order book |