WebSocket API Documentation

Real-time data streaming via SignalR WebSocket hubs. This service provides two hubs for different use cases:

HubEndpointPurpose
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

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/PublicMarketData

Connection Examples

JavaScript
Python
C#
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"));
Timestamp Window: The timestamp must be within 5 seconds of the server time. Use the 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)

Client Server │ │ │─────────── Connect (WebSocket) ─────────────────>│ │ │ │<────────────────── LR (Login Request) ───────────│ │ │ │─── WebSocketInit("trade") ──────────────────────>│ │─── WebSocketLogin("jwt-token") ────────────────->│ │ │ │<────────────────── LS (Login Success) ───────────│ // or LF (Login Failed) │ │ │─── JoinChannel("btcusdt") ──────────────────────>│ │─── SubscribeTickers() ──────────────────────────>│ │ │ │<── BalanceUpdate, OrderUpdate, Notification... ──│ // private events │ │ │─── Ping(timestamp) ────────────────────────────->│ │<── Pong(timestamp) ─────────────────────────────│

GlobalHub Flow (API Key)

Client Server │ │ │─────────── Connect (WebSocket) ─────────────────>│ │ │ │<────────────────── LR (Login Request) ───────────│ │ │ │─── WebSocketInit("trade") ──────────────────────>│ │─── WebSocketLoginWithApiKey(key, sig, ts) ──────>│ // HMAC-SHA256(secret, timestamp) │ │ │<────────────────── LS (Login Success) ───────────│ // or LF (Login Failed) │ │ │─── JoinChannel("btcusdt") ──────────────────────>│ │ │ │<── BalanceUpdate, OrderUpdate, Notification... ──│ // private events │ │

PublicMarketData Flow

Client Server │ │ │─────────── Connect (WebSocket) ─────────────────>│ │ │ │<── Connected { connectionId, serverTime, ... } ──│ │ │ │─── Subscribe("domain", "BTCUSDT", "Spot", 500) ->│ │ │ │<── Subscribed { orderBookId, depth } ────────────│ │<── OrderBookSnapshot { s, t, p, b, a, tr } ─────│ │ │ │<── OrderBookUpdate { s, t, p, b, a, tr } ───────│ // every 250ms │<── OrderBookUpdate { s, t, p, b, a, tr } ───────│ │ │ │─── SubscribeMiniTickers("domain") ──────────────>│ │ │ │<── MiniTickerSubscribed { tickerCount, ts } ─────│ │<── TickerSnapshot { c, t, d[] } ─────────────────│ │ │ │<── TickerUpdate { c, t, d[] } ───────────────────│ // every 500ms (delta only) │ │ │─── Ping(id, t1) ────────────────────────────────>│ │<── Pong { id, t1, t2, t3 } ─────────────────────│

GlobalHub

The GlobalHub handles user-specific real-time updates such as notifications, balance changes, order updates, and general application events.

PropertyValue
Endpoint/GlobalHub
TransportWebSocket only
Buffer Size64KB send/receive
AuthenticationJWT token via WebSocketLogin
SEND WebSocketInit(page) PUBLIC

Send initial page/context information after connecting. Tells the server which page the client is viewing.

Parameters

NameTypeRequiredDescription
pagestringYesPage identifier (max 64 chars). Example: "trade", "wallet", "dashboard"

Example

await globalHub.invoke("WebSocketInit", "trade");
SEND WebSocketLogin(token) PUBLIC

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

NameTypeRequiredDescription
tokenstringYesJWT bearer token

Response Events

EventDescription
LSLogin successful - you will now receive private events
LFLogin failed - invalid token or unauthorized

Example

await globalHub.invoke("WebSocketLogin", "eyJhbGciOiJIUzI1Ni...");
SEND WebSocketLoginWithApiKey(apiKey, signature, timestamp) PUBLIC

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

NameTypeRequiredDescription
apiKeystringYesYour API Key (from account dashboard)
signaturestringYesHMAC-SHA256 signature of the timestamp, using your API Secret as the key. Lowercase hex.
timestamplongYesCurrent time in Unix milliseconds. Must be within 5 seconds of server time.

Signing Process

  1. Get current timestamp: Date.now() (Unix milliseconds)
  2. Convert to string: "1709500000000"
  3. Sign with HMAC-SHA256 using your API Secret
  4. Output as lowercase hex string

Response Events

EventDescription
LSLogin successful
LFLogin failed (invalid key, bad signature, timestamp expired, or IP not allowed)

Code Examples

JavaScript
Python
C#
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);
IP Restrictions: If your API key has IP restrictions configured, the connection must originate from an allowed IP address.
SEND JoinChannel(channel) PUBLIC

Subscribe to a channel. Uses single-active-channel semantics: joining a new channel automatically leaves all previous channels for this connection.

Parameters

NameTypeRequiredDescription
channelstringYesChannel name (lowercase letters, digits, :, _, -, max 64 chars). Example: "btcusdt"
Note: Channel names must match the pattern [a-z0-9:_-]{1,64}. Invalid names are silently rejected.

Example

// Switch to BTCUSDT channel (auto-leaves previous)
await globalHub.invoke("JoinChannel", "btcusdt");
SEND SubscribeTickers() PUBLIC

Subscribe to mini ticker updates for both Spot and Futures markets. Requires an active connection (hub user must exist).

Parameters

None

Response Events

EventDescription
RCReconnect needed - connection state not found. Reconnect and try again.

Example

await globalHub.invoke("SubscribeTickers");
SEND SetLastPage(lastpage) PUBLIC

Update the user's current page context. Used for analytics and targeted notifications.

Parameters

NameTypeRequiredDescription
lastpagestringYesCurrent page identifier

Example

await globalHub.invoke("SetLastPage", "wallet/deposit");
SEND SubscribeQRLogin(sessionId) PUBLIC

Subscribe to QR code login session updates. Used for login-via-mobile-app flow.

Parameters

NameTypeRequiredDescription
sessionIdstringYesQR login session identifier

Response Events

EventPayloadDescription
QRSuccess-QR session subscription confirmed
QRLoginUserLoginResponseQR login completed by mobile app

Example

await globalHub.invoke("SubscribeQRLogin", "session-uuid-here");

globalHub.on("QRLogin", (loginData) => {
    console.log("QR login completed:", loginData);
});
SEND Ping(timestamp) PUBLIC

Keep-alive ping. Server responds with Pong echoing the timestamp.

Parameters

NameTypeRequiredDescription
timestamplongYesClient 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.

EventAuthPayloadDescription
LRPUBLIC-Login request - sent on connect, prompt user to authenticate
LSPUBLIC-Login success
LFPUBLIC-Login failed
RCPUBLIC-Reconnect needed - connection state lost
PongPUBLIClongPing response with echoed timestamp
QRSuccessPUBLIC-QR login session subscription confirmed
QRLoginPUBLICUserLoginResponseQR login completed
BalanceUpdatePRIVATEResponseWalletWallet balance changed
OrderUpdatePRIVATEOrderDetailResponseOrder status changed
TradeUpdatePRIVATETradeItemTrade executed
NotificationPRIVATEGlobalNotificationMessageGeneral notification
FuturesAccountUpdatePRIVATEFuturesAccountViewFutures account state changed
AccountUpdatePRIVATEFuturesAccountFutures account update
KYCUpdatePRIVATEKYCStatusUpdateKYC verification status changed
DepositCryptoUpdatePRIVATEDepositCryptoResponseCrypto deposit received
WithdrawCryptoUpdatePRIVATEWithdrawCryptoResponseCrypto withdrawal status changed
AffiliateCodeUpdatePRIVATEAffiliateCodeResponseAffiliate code updated
AffiliateStatUpdatePRIVATEAffiliateUserStatResponseAffiliate stats changed

PublicMarketData Hub

High-performance hub for real-time market data streaming. Optimized for order book updates, mini tickers, and mark prices.

PropertyValue
Endpoint/PublicMarketData
TransportWebSocket only
Buffer Size128KB send/receive
AuthenticationOptional (JWT via Authenticate)
OrderBook Update Interval250ms (aggregated)
Ticker Update Interval500ms (delta only)
Mark Price Update Interval1000ms
On Connect: The server sends a Connected event with your connection ID, server time, and list of available order books.
SEND Subscribe(domain, symbol, orderBookType, depthLevel) PUBLIC

Subscribe to an order book's real-time updates. Receives a snapshot immediately followed by continuous delta updates every 250ms.

Parameters

NameTypeRequiredDefaultDescription
domainstringYes-Tenant domain (e.g. "darkex.com")
symbolstringYes-Trading pair symbol (e.g. "BTCUSDT")
orderBookTypestringYes-"Spot" or "Futures"
depthLevelintNo500Depth level: 50, 100, 500, or 1000

Response Events

EventDescription
SubscribedSubscription confirmed with orderBookId and depth
OrderBookSnapshotFull order book snapshot (immediate)
OrderBookUpdateDelta updates (every 250ms)
MarkPriceUpdateMark price updates for Futures (every 1000ms, auto-subscribed)
ErrorIf 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);
});
SEND Unsubscribe(domain, symbol, orderBookType) PUBLIC

Unsubscribe from an order book's updates. Also removes mark price subscription for futures.

Parameters

NameTypeRequiredDescription
domainstringYesTenant domain
symbolstringYesTrading pair symbol
orderBookTypestringYes"Spot" or "Futures"

Response

Event: Unsubscribed with the orderBookId.

Example

await marketHub.invoke("Unsubscribe", "darkex.com", "BTCUSDT", "Spot");
SEND GetSnapshot(domain, symbol, orderBookType) PUBLIC

Request a fresh order book snapshot. Useful for manual recovery or initial data load.

Parameters

NameTypeRequiredDescription
domainstringYesTenant domain
symbolstringYesTrading pair symbol
orderBookTypestringYes"Spot" or "Futures"

Response

Event: OrderBookSnapshot or Error if not found.

Example

await marketHub.invoke("GetSnapshot", "darkex.com", "BTCUSDT", "Spot");
SEND RequestReplay(domain, symbol, orderBookType, lastBroadcastSeqId) PUBLIC

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

NameTypeRequiredDescription
domainstringYesTenant domain
symbolstringYesTrading pair symbol
orderBookTypestringYes"Spot" or "Futures"
lastBroadcastSeqIdlongYesThe last BroadcastSeqId successfully applied by the client

Response

Either multiple OrderBookUpdate events (if replay possible) or a single OrderBookSnapshot (if too old).

Buffer Window: The replay buffer keeps the last 20 updates (~5 seconds). If your gap is older than this, you'll receive a fresh snapshot instead of individual deltas.

Example

// When a sequence gap is detected
if (update.s !== lastSeqId + 1) {
    await marketHub.invoke("RequestReplay", "darkex.com", "BTCUSDT", "Spot", lastSeqId);
}
SEND SubscribeMiniTickers(domain) PUBLIC

Subscribe to mini ticker updates for a specific tenant. Receives a full snapshot immediately, then delta updates every 500ms (only changed tickers).

Parameters

NameTypeRequiredDescription
domainstringYesTenant domain (e.g. "darkex.com")

Response Events

EventDescription
MiniTickerSubscribedSubscription confirmed with ticker count
TickerSnapshotFull ticker snapshot (immediate)
TickerUpdateDelta updates (every 500ms, only changed tickers)
ErrorIf tenant not found
Tenant Filtering: You only receive tickers for order books belonging to the specified tenant domain.

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);
    });
});
SEND UnsubscribeMiniTickers() PUBLIC

Unsubscribe from mini ticker updates.

Parameters

None

Response

Event: MiniTickerUnsubscribed with timestamp.

Example

await marketHub.invoke("UnsubscribeMiniTickers");
SEND Authenticate(token) PRIVATE

Authenticate with JWT token to receive private updates (balance changes, order updates). Optional — public market data works without authentication.

Parameters

NameTypeRequiredDescription
tokenstringYesJWT bearer token

Response Events

EventPayloadDescription
AuthSuccess{ userId }Authentication successful
AuthFailed{ reason }Authentication failed

Example

await marketHub.invoke("Authenticate", "eyJhbGciOiJIUzI1Ni...");
SEND GetAvailableOrderBooks() PUBLIC

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"
  }
]
SEND GetMySubscriptions() PUBLIC

Get your current active subscriptions for this connection.

Parameters

None

Response

Event: MySubscriptions

{
  "orderBooks": [1, 5, 12],
  "miniTickers": true
}
SEND GetStatus() PUBLIC

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
}
SEND Ping(id, t1) PUBLIC

Latency measurement with detailed timing. Returns server receive and send timestamps for precise RTT calculation.

Parameters

NameTypeRequiredDescription
idlongYesSequence ID to match ping/pong pairs (use incrementing counter)
t1longYesClient 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 }
  ]
}
FieldTypeDescription
cstringMessage type: "snapshot"
slongBroadcastSeqId - use for gap detection
tlongServer timestamp (Unix ms)
pstringPair symbol (e.g. "BTCUSDT")
ostringOrder book type: "spot" or "futures"
dintDepth level
barrayBids: [[price, qty], ...] (descending by price)
aarrayAsks: [[price, qty], ...] (ascending by price)
trarray?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 }
  ]
}
Delta Rules:
  • 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. If s is not previous + 1, call RequestReplay

Trade Fields (tr)

FieldTypeDescription
ilongTrade ID
pstringPrice
qstringQuantity
mboolIs buyer maker (true = sell aggressor)
tlongTrade 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

FieldTypeDescription
sstringSymbol (e.g. "BTCUSDT")
ostringOrder book type: "spot" or "futures"
pstringLast price
opstringOpen price (24h)
hstringHigh price (24h)
lstringLow price (24h)
vstringVolume (base asset)
qstringQuote volume
cintTrade count (24h)
mstringMark price (futures only)
fstringFunding rate (futures only)
cdlongFunding 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

FieldTypeDescription
sstringSymbol
mstringMark price
istringIndex price
fstringFunding rate
tlongNext funding timestamp

Private Events

Private events require authentication. On GlobalHub, authenticate via WebSocketLogin. On PublicMarketData, use Authenticate.

Authentication Required: You must successfully authenticate before receiving any private events. Without authentication, you will only receive public events.

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

  1. On OrderBookSnapshot: store lastSeqId = snapshot.s
  2. On OrderBookUpdate: check if update.s === lastSeqId + 1
  3. If gap detected: call RequestReplay(domain, symbol, type, lastSeqId)
  4. After replay: continue normal processing
Replay Buffer: The server keeps the last 20 updates (~5 seconds) per order book. If your gap is within this window, you'll receive individual deltas. Otherwise, you'll get a fresh snapshot.

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"
}
CodeDescription
TENANT_NOT_FOUNDThe specified domain does not match any tenant
PAIR_NOT_FOUNDThe specified symbol/type combination does not exist for this tenant
ORDER_BOOK_NOT_FOUNDOrder book not found (internal)
SNAPSHOT_NOT_FOUNDSnapshot not available yet for the requested order book