Orderbook Updates
Note: The subscription type is "orderbook" (lowercase). Some exchanges use orderBook (camelCase) for similar functionality.
Receive live orderbook depth updates for specific trading pairs. Subscriptions support two delivery formats: diff (incremental changes, the default) and snapshot (full book on every update).
Endpoint
Public subscriptions (candles, marketPrices, orderbook, trades):
wss://papi.synthetix.io/v1/ws/infoPrivate subscriptions (subAccountUpdates - requires authentication):
wss://papi.synthetix.io/v1/ws/tradeSubscription Request
Orderbook updates are public data and do not require authentication.
Subscribe (Diff Mode — Default)
{
"id": "sub-1",
"method": "subscribe",
"params": {
"type": "orderbook",
"symbol": "BTC-USDT"
}
}Equivalent to format: "diff", depth: 50, updateFrequencyMs: 250.
Subscribe (Snapshot Mode)
{
"id": "sub-1",
"method": "subscribe",
"params": {
"type": "orderbook",
"symbol": "BTC-USDT",
"format": "snapshot"
}
}Subscribe with Custom Parameters
{
"id": "sub-1",
"method": "subscribe",
"params": {
"type": "orderbook",
"symbol": "BTC-USDT",
"format": "diff",
"depth": 100,
"updateFrequencyMs": 500
}
}Request Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
id | string | Yes | Client-generated unique request identifier |
method | string | Yes | Must be "subscribe" |
params.type | string | Yes | Must be "orderbook" |
params.symbol | string | Yes | Trading pair symbol (e.g., "BTC-USDT"). Must be a specific symbol — "ALL" is not supported. |
params.format | string | No | "diff" (default) or "snapshot" |
params.depth | integer | No | Number of price levels per side: 10, 50 (default), or 100 |
params.updateFrequencyMs | integer | No | Update interval in milliseconds (default: 250). See allowed values below. |
Subscribe Response
A successful subscribe response echoes the negotiated parameters:
{
"id": "sub-1",
"requestId": "sub-1",
"status": 200,
"result": {
"type": "orderbook",
"symbol": "BTC-USDT",
"format": "diff",
"depth": 50,
"updateFrequencyMs": 250,
"seq": 987654321
}
}The response echoes both id and requestId for backwards compatibility. The returned seq is the matching-engine sequence at subscription time. The first notification will have meseq >= this value.
Update Modes
Diff Mode (Default)
In diff mode, you receive:
- An initial snapshot (
type: "snapshot") containing the full orderbook at your subscribed depth - Incremental diffs (
type: "diff") containing only the price levels that changed since the last update
Diff mode significantly reduces bandwidth but requires you to maintain local orderbook state and apply updates. You must also validate continuity using the meseq/prevMeseq fields and verify integrity with the checksum.
Snapshot Mode
In snapshot mode, every message is a full orderbook snapshot at your subscribed depth. This is simpler to implement — just replace your local state with each message — but uses more bandwidth.
Message Format
Initial Snapshot (Diff Mode)
When subscribing with format: "diff", the first message is a full snapshot with type: "snapshot". This establishes the baseline for subsequent diffs:
{
"channel": "orderbookUpdate",
"method": "orderbook_depth_update",
"type": "snapshot",
"meseq": 987654321,
"met": 1704067200123456,
"prevMeseq": null,
"checksum": "a1b2c3d4",
"data": {
"symbol": "BTC-USDT",
"timestamp": "2026-01-07T17:08:29Z",
"bids": [
{"price": "100000.00", "quantity": "1.5"},
{"price": "99950.00", "quantity": "2.0"}
],
"asks": [
{"price": "100050.00", "quantity": "1.2"},
{"price": "100100.00", "quantity": "1.8"}
]
},
"timestamp": 1704067200000
}
methodis deprecated. Usechannelto identify notification type. See Message Fields.
Diff Message
Sent for subsequent updates in diff mode. Contains only the price levels that changed:
{
"channel": "orderbookUpdate",
"method": "orderbook_depth_update",
"type": "diff",
"meseq": 987654322,
"met": 1704067201123456,
"prevMeseq": 987654321,
"checksum": "b2c3d4e5",
"data": {
"symbol": "BTC-USDT",
"timestamp": "2026-01-07T17:08:30Z",
"bids": [
{"price": "100000.00", "quantity": "1.2"},
{"price": "99900.00", "quantity": "0"}
],
"asks": [
{"price": "100150.00", "quantity": "0.5"}
]
},
"timestamp": 1704067200100
}
methodis deprecated. Usechannelto identify notification type. See Message Fields.
In this example:
- Bid at
100000.00had its quantity updated from1.5to1.2 - Bid at
99900.00was removed (quantity: "0") - A new ask level appeared at
100150.00 - All other levels remain unchanged
Snapshot Mode Message
When subscribing with format: "snapshot", every message is a full orderbook. In snapshot mode the type field is omitted:
{
"channel": "orderbookUpdate",
"method": "orderbook_depth_update",
"meseq": 987654321,
"met": 1704067200123456,
"checksum": "a1b2c3d4",
"data": {
"symbol": "BTC-USDT",
"timestamp": "2026-01-07T17:08:29Z",
"bids": [
{"price": "100000.00", "quantity": "1.5"},
{"price": "99950.00", "quantity": "2.0"}
],
"asks": [
{"price": "100050.00", "quantity": "1.2"},
{"price": "100100.00", "quantity": "1.8"}
]
},
"timestamp": 1704067200000
}
methodis deprecated. Usechannelto identify notification type. See Message Fields.
Message Fields
| Field | Type | Description |
|---|---|---|
channel | string | "orderbookUpdate" — the canonical channel identifier |
method | string | "orderbook_depth_update" (deprecated) — legacy identifier, included on all notifications for backwards compatibility. Use channel instead. |
type | string | "snapshot" for full book, "diff" for incremental changes. Omitted in snapshot-mode subscriptions. |
meseq | integer | Matching-engine sequence watermark for this update |
met | integer | Matching-engine event time in Unix microseconds |
prevMeseq | integer | null | Expected previous matching-engine sequence. null on snapshots. |
checksum | string | CRC32 checksum of the full depth-truncated orderbook state after applying this message (see Checksum Validation) |
data.symbol | string | Trading pair symbol |
data.timestamp | string | RFC 3339 timestamp of the orderbook state (e.g., "2026-01-07T17:08:29Z") |
data.bids | array | Bid price levels, sorted by price descending (highest first) |
data.asks | array | Ask price levels, sorted by price ascending (lowest first) |
timestamp | integer | Server send time in Unix milliseconds |
Diff Semantics
In a diff message, each price level in bids and asks represents a change:
| Scenario | Representation |
|---|---|
| New level | Level appears in the diff with a non-zero quantity |
| Updated level | Level appears with the new quantity (replaces the previous quantity at that price) |
| Removed level | Level appears with quantity: "0" |
Levels not present in a diff message are unchanged.
Price Level Format
Each price level is an object with price and quantity fields:
{"price": "100000.50", "quantity": "1.25"}Where:
price(string): Price levelquantity(string): Total quantity at this level."0"in a diff means the level was removed.
Depth
The subscription depth parameter controls how many price levels per side are included. Available values:
| Depth | Description |
|---|---|
10 | Top 10 price levels per side |
50 | Top 50 price levels per side (default) |
100 | Top 100 price levels per side |
For deeper snapshots, use the Get Orderbook request/response endpoint which supports depths up to 1000 levels.
Update Frequency
The updateFrequencyMs parameter controls the minimum interval between updates. The server batches changes within each interval and delivers a single message per tick.
Allowed values: 50, 100, 250 (default), 500, 1000.
Constraint: When depth=100, only updateFrequencyMs values of 250, 500, or 1000 are supported.
Managing Local Orderbook
Diff Mode
When using diff mode, you must maintain local orderbook state and apply each diff as it arrives.
class OrderbookManager {
constructor() {
this.orderbooks = new Map(); // symbol → { bids: Map<price, quantity>, asks: Map<price, quantity>, meseq }
}
processMessage(message) {
const { type, meseq, prevMeseq, checksum, data } = message;
const { symbol } = data;
if (type === "snapshot") {
// Replace entire local state
this.orderbooks.set(symbol, {
bids: new Map(data.bids.map(l => [l.price, l.quantity])),
asks: new Map(data.asks.map(l => [l.price, l.quantity])),
meseq
});
return;
}
// type === "diff"
const book = this.orderbooks.get(symbol);
if (!book) return; // No baseline — wait for snapshot
// 1. Verify continuity using prevMeseq
if (prevMeseq !== book.meseq) {
// Gap or out-of-order — local state may be stale
console.error(`Sequence gap for ${symbol}: expected prevMeseq=${book.meseq}, got ${prevMeseq}`);
this.orderbooks.delete(symbol);
this.resubscribe(symbol);
return;
}
// 2. Apply changes
for (const level of data.bids) {
if (level.quantity === "0") {
book.bids.delete(level.price);
} else {
book.bids.set(level.price, level.quantity);
}
}
for (const level of data.asks) {
if (level.quantity === "0") {
book.asks.delete(level.price);
} else {
book.asks.set(level.price, level.quantity);
}
}
book.meseq = meseq;
// 3. Verify checksum
if (!this.verifyChecksum(symbol, checksum)) {
console.error(`Checksum mismatch for ${symbol} — resubscribing`);
this.orderbooks.delete(symbol);
this.resubscribe(symbol);
}
}
getSortedBook(symbol) {
const book = this.orderbooks.get(symbol);
if (!book) return null;
return {
bids: [...book.bids.entries()]
.sort((a, b) => parseFloat(b[0]) - parseFloat(a[0]))
.map(([price, quantity]) => ({ price, quantity })),
asks: [...book.asks.entries()]
.sort((a, b) => parseFloat(a[0]) - parseFloat(b[0]))
.map(([price, quantity]) => ({ price, quantity }))
};
}
}Snapshot Mode
In snapshot mode, each message is the complete orderbook — simply replace local state:
class SnapshotOrderbookManager {
constructor() {
this.orderbooks = new Map();
}
processMessage(message) {
const { data } = message;
this.orderbooks.set(data.symbol, {
bids: data.bids,
asks: data.asks,
timestamp: data.timestamp
});
}
}Usage Example
const ws = new WebSocket('wss://papi.synthetix.io/v1/ws/info');
const orderbookManager = new OrderbookManager();
ws.onopen = () => {
ws.send(JSON.stringify({
id: "sub-1",
method: "subscribe",
params: {
type: "orderbook",
symbol: "BTC-USDT"
}
}));
};
ws.onmessage = (event) => {
const message = JSON.parse(event.data);
if (message.channel === "orderbookUpdate") {
orderbookManager.processMessage(message);
const book = orderbookManager.getSortedBook("BTC-USDT");
if (book && book.bids.length && book.asks.length) {
console.log(`Best bid: ${book.bids[0].price} / Best ask: ${book.asks[0].price}`);
}
}
};Checksum Validation
The checksum field contains a CRC32 (IEEE) checksum of the full depth-truncated orderbook state after applying the message. Use it to verify that your local state matches the server's.
Checksum Algorithm
- Start with the depth-truncated orderbook after applying the update (sorted bids descending, asks ascending)
- Build an input string by concatenating all levels:
- For each bid:
b<price>:<quantity>| - For each ask:
a<price>:<quantity>| - Bids come first, then asks
- For each bid:
- Compute CRC32 (IEEE) of the resulting byte string
- Format as an 8-character lowercase hexadecimal string
b100000.00:1.5|b99950.00:2.0|a100050.00:1.2|a100100.00:1.8|JavaScript Implementation
import crc32 from 'crc-32'; // npm install crc-32
function computeChecksum(bids, asks) {
let input = '';
for (const [price, quantity] of bids) {
input += `b${price}:${quantity}|`;
}
for (const [price, quantity] of asks) {
input += `a${price}:${quantity}|`;
}
const value = crc32.buf(Buffer.from(input)) >>> 0; // unsigned
return value.toString(16).padStart(8, '0');
}Important: The checksum is computed over the depth-truncated book at your subscribed depth. If your local book has accumulated more levels than your subscribed depth (e.g., from levels moving in and out of range), truncate to the top N levels per side before computing.
Recovery
Diff Mode Recovery
If any of the following occur, discard your local orderbook and resubscribe:
- Continuity break: A diff message's
prevMeseqdoes not match your last storedmeseq, indicating a gap or out-of-order delivery - Checksum mismatch: Your locally computed checksum does not match the message
checksum - Missing baseline: You receive a diff but have no local snapshot
Resubscribing gives you a fresh snapshot to re-establish your baseline:
function resubscribe(ws, symbol) {
ws.send(JSON.stringify({
id: "resub-" + Date.now(),
method: "subscribe",
params: {
type: "orderbook",
symbol
}
}));
}Fetching a Snapshot Manually
You can also fetch a one-time snapshot via request/response to sync your state without resubscribing:
Snapshot Mode Recovery
In snapshot mode, reconnection is simple — each message is a full book, so just reconnect and resubscribe.
Performance Optimization
Backpressure: Slow Consumer Disconnection
If your client cannot consume messages fast enough, the server will silently remove your subscription. The WebSocket send buffer has a fixed capacity, and when it fills up, the server drops the subscriber rather than blocking other clients. You will not receive an error message — the orderbook updates will simply stop arriving.
To avoid this:
- Process incoming messages promptly (avoid blocking the WebSocket message handler)
- Choose an appropriate
updateFrequencyMsfor your use case — slower frequencies produce fewer messages - Monitor for gaps in updates and resubscribe if needed
Choosing Parameters
| Use Case | Recommended Settings |
|---|---|
| HFT / market making | depth: 10, updateFrequencyMs: 50, format: "diff" |
| Trading UI | depth: 50, updateFrequencyMs: 250, format: "diff" |
| Analytics / monitoring | depth: 50, updateFrequencyMs: 500, format: "snapshot" |
| Simple integration | depth: 10, format: "snapshot" |
Authentication
Orderbook updates are public data and do not require authentication. They are available on the public /info endpoint.
Common Issues
| Issue | Description | Solution |
|---|---|---|
| Connection lost | WebSocket disconnection | Reconnect and resubscribe |
| Updates stopped | Slow consumer — server removed subscription | Choose a slower updateFrequencyMs, process messages faster, and resubscribe |
| Checksum mismatch | Local state has diverged from server | Discard local state and resubscribe |
| Invalid symbol | Symbol not available | Check supported markets |
"ALL" rejected | Wildcard subscriptions not supported | Subscribe to each symbol individually |
Implementation Notes
- Default is diff mode: Subscriptions default to
format: "diff"— you must maintain local state and apply updates - Configurable depth: Choose 10, 50, or 100 levels per side
- Timer-driven delivery: Updates are batched and sent at the configured
updateFrequencyMsinterval - Checksum on every message: Both snapshot and diff messages include a CRC32 checksum for validation
- No
"ALL"symbol: You must subscribe to each symbol individually - Snapshot fallback: In diff mode, if changes are very large the server may send a snapshot instead of a diff — always check the
typefield
Related Endpoints
- Get Orderbook - Get orderbook snapshot via REST
- Get Orderbook (WebSocket) - Get orderbook snapshot with configurable depth via WebSocket request/response
- Market Price Updates - Track price changes
- Trade Updates - Real-time public trade stream