Skip to content

Orderbook Updates

Note: The subscription type is "orderbook" (lowercase). Some exchanges use orderBook (camelCase) for similar functionality.

Receive live changes to market depth, price levels, and liquidity for specific trading pairs.

Endpoint

Public subscriptions (candles, marketPrices, orderbook):

wss://papi.synthetix.io/v1/ws/info

Private subscriptions (subAccountUpdates - requires authentication):

wss://papi.synthetix.io/v1/ws/trade

Subscription Request

Orderbook updates are public data and do not require authentication.

Subscribe to Single Market

{
  "id": "sub-1",
  "method": "subscribe",
  "params": {
    "type": "orderbook",
    "symbol": "BTC-USDT"
  }
}

Subscribe to All Markets

{
  "id": "sub-all",
  "method": "subscribe",
  "params": {
    "type": "orderbook",
    "symbol": "ALL"
  }
}

Request Parameters

ParameterTypeRequiredDescription
idstringYesClient-generated unique request identifier
methodstringYesMust be "subscribe"
params.typestringYesMust be "orderbook"
params.symbolstringYesTrading pair symbol (e.g., "BTC-USDT") or "ALL" for all markets

Orderbook Update Messages

Initial Snapshot

Upon subscription, you'll receive a full orderbook snapshot:

{
  "method": "orderbook_depth_update",
  "data": {
    "symbol": "BTC-USDT",
    "timestamp": "2025-01-01T00:00:00.000Z",
    "bids": [
      {"price": "50000.00", "quantity": "1.5"},
      {"price": "49999.50", "quantity": "2.0"},
      {"price": "49999.00", "quantity": "0.8"}
    ],
    "asks": [
      {"price": "50001.00", "quantity": "1.2"},
      {"price": "50001.50", "quantity": "3.1"},
      {"price": "50002.00", "quantity": "0.9"}
    ]
  }
}

Incremental Updates

After the snapshot, you'll receive incremental updates with the same structure:

{
  "method": "orderbook_depth_update",
  "data": {
    "symbol": "BTC-USDT",
    "timestamp": "2025-01-01T00:00:01.000Z",
    "bids": [
      {"price": "50000.00", "quantity": "0.0"},  // Removed (quantity = 0)
      {"price": "49998.50", "quantity": "1.0"}   // Added/Updated
    ],
    "asks": [
      {"price": "50002.00", "quantity": "1.5"},  // Updated quantity
      {"price": "50003.00", "quantity": "0.5"}   // Added new level
    ]
  }
}

Price Level Format

Each price level is represented as an object with price and quantity fields:

{"price": "50000.50", "quantity": "1.25"}

Where:

  • price (string): Price level
  • quantity (string): Total quantity at this level

Special Case: When quantity is "0.0", the price level should be removed from the orderbook.

Managing Local Orderbook

Initial Setup

class OrderbookManager {
  constructor() {
    this.orderbooks = new Map();
  }
 
  processOrderbookUpdate(data) {
    const { symbol } = data;
 
    // If orderbook doesn't exist, initialize it
    if (!this.orderbooks.has(symbol)) {
      this.initializeOrderbook(symbol, data);
    } else {
      this.updateOrderbook(symbol, data);
    }
  }
 
  initializeOrderbook(symbol, data) {
    const orderbook = {
      symbol,
      bids: new Map(),
      asks: new Map(),
      timestamp: data.timestamp
    };
 
    // Load initial bids
    data.bids.forEach(({price, quantity}) => {
      orderbook.bids.set(price, quantity);
    });
 
    // Load initial asks
    data.asks.forEach(({price, quantity}) => {
      orderbook.asks.set(price, quantity);
    });
 
    this.orderbooks.set(symbol, orderbook);
    console.log(`Orderbook initialized for ${symbol}`);
  }
 
  updateOrderbook(symbol, data) {
    const orderbook = this.orderbooks.get(symbol);
    if (!orderbook) {
      console.error(`No orderbook found for ${symbol}`);
      return;
    }
 
    // Apply bid updates
    data.bids?.forEach(({price, quantity}) => {
      if (quantity === "0.0") {
        orderbook.bids.delete(price);
      } else {
        orderbook.bids.set(price, quantity);
      }
    });
 
    // Apply ask updates
    data.asks?.forEach(({price, quantity}) => {
      if (quantity === "0.0") {
        orderbook.asks.delete(price);
      } else {
        orderbook.asks.set(price, quantity);
      }
    });
 
    orderbook.timestamp = data.timestamp;
 
    this.notifyOrderbookChange(symbol, orderbook);
  }
 
  getOrderbook(symbol) {
    return this.orderbooks.get(symbol);
  }
 
  getBestBidAsk(symbol) {
    const orderbook = this.orderbooks.get(symbol);
    if (!orderbook) return null;
 
    const bestBid = Math.max(...Array.from(orderbook.bids.keys()).map(parseFloat));
    const bestAsk = Math.min(...Array.from(orderbook.asks.keys()).map(parseFloat));
 
    return {
      bestBid: bestBid.toString(),
      bestAsk: bestAsk.toString(),
      spread: (bestAsk - bestBid).toString(),
      spreadPercent: ((bestAsk - bestBid) / bestBid * 100).toFixed(4)
    };
  }
 
  getDepth(symbol, levels = 10) {
    const orderbook = this.orderbooks.get(symbol);
    if (!orderbook) return null;
 
    // Sort and limit bids (highest first)
    const bids = Array.from(orderbook.bids.entries())
      .sort(([a], [b]) => parseFloat(b) - parseFloat(a))
      .slice(0, levels);
 
    // Sort and limit asks (lowest first)
    const asks = Array.from(orderbook.asks.entries())
      .sort(([a], [b]) => parseFloat(a) - parseFloat(b))
      .slice(0, levels);
 
    return { bids, asks };
  }
 
  notifyOrderbookChange(symbol, orderbook) {
    // Emit events for UI updates
    this.emit('orderbookChange', {
      symbol,
      orderbook,
      bestBidAsk: this.getBestBidAsk(symbol),
      timestamp: Date.now()
    });
  }
}

Usage Example

const ws = new WebSocket('wss://papi.synthetix.io/v1/ws/info');
const orderbookManager = new OrderbookManager();
 
ws.onopen = () => {
  // Subscribe to orderbook updates
  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.method === "orderbook_depth_update") {
    orderbookManager.processOrderbookUpdate(message.data);
  }
};
 
// Listen for orderbook changes
orderbookManager.on('orderbookChange', (event) => {
  const { symbol, bestBidAsk } = event;
  console.log(`${symbol} Best: ${bestBidAsk.bestBid}/${bestBidAsk.bestAsk}`);
 
  // Update UI
  updateOrderbookDisplay(symbol, orderbookManager.getDepth(symbol, 20));
});

Performance Optimization

Efficient Data Structures

// Use optimized data structure for large orderbooks
class OptimizedOrderbook {
  constructor() {
    this.bids = new Map(); // Could use sorted tree for very large books
    this.asks = new Map();
    this.bidsSorted = []; // Cache sorted arrays
    this.asksSorted = [];
    this.sortedDirty = true;
  }
 
  updateLevel(side, price, quantity) {
    const book = side === 'bid' ? this.bids : this.asks;
 
    if (quantity === "0.0") {
      book.delete(price);
    } else {
      book.set(price, quantity);
    }
 
    this.sortedDirty = true;
  }
 
  getSortedLevels(side, limit = 10) {
    if (this.sortedDirty) {
      this.updateSortedCache();
    }
 
    const sorted = side === 'bid' ? this.bidsSorted : this.asksSorted;
    return sorted.slice(0, limit);
  }
 
  updateSortedCache() {
    this.bidsSorted = Array.from(this.bids.entries())
      .sort(([a], [b]) => parseFloat(b) - parseFloat(a));
 
    this.asksSorted = Array.from(this.asks.entries())
      .sort(([a], [b]) => parseFloat(a) - parseFloat(b));
 
    this.sortedDirty = false;
  }
}

Rate Limiting Updates

class ThrottledOrderbookManager extends OrderbookManager {
  constructor(throttleMs = 50) {
    super();
    this.throttleMs = throttleMs;
    this.pendingUpdates = new Map();
    this.updateTimers = new Map();
  }
 
  updateOrderbook(symbol, data) {
    // Store latest update
    this.pendingUpdates.set(symbol, data);
 
    // Clear existing timer
    if (this.updateTimers.has(symbol)) {
      clearTimeout(this.updateTimers.get(symbol));
    }
 
    // Set new timer
    const timer = setTimeout(() => {
      const latestUpdate = this.pendingUpdates.get(symbol);
      if (latestUpdate) {
        super.updateOrderbook(symbol, latestUpdate);
        this.pendingUpdates.delete(symbol);
        this.updateTimers.delete(symbol);
      }
    }, this.throttleMs);
 
    this.updateTimers.set(symbol, timer);
  }
}

Authentication

Orderbook updates are public data and do not require authentication. They are available on the public /info endpoint without any authentication.

Error Handling

Connection Recovery

handleConnectionError(symbol) {
  const orderbook = this.orderbooks.get(symbol);
 
  if (!orderbook) {
    console.error(`No orderbook for ${symbol}, requesting snapshot`);
    this.resubscribe(symbol);
    return;
  }
 
  // Clear existing orderbook and resubscribe
  this.orderbooks.delete(symbol);
  this.resubscribe(symbol);
}
 
resubscribe(symbol) {
  // Re-subscribe to get fresh snapshot
  const subscription = {
    id: "resub-" + Date.now(),
    method: "subscribe",
    params: {
      type: "orderbook",
      symbol
    }
  };
  this.ws.send(JSON.stringify(subscription));
}

Common Issues

ErrorDescriptionSolution
Connection lostWebSocket disconnectionReconnect and resubscribe
Invalid symbolSymbol not availableCheck supported markets
Stale dataOrderbook out of syncClear and resubscribe

Use Cases

Market Making

orderbookManager.on('orderbookChange', (event) => {
  const { symbol, bestBidAsk } = event;
  const spread = parseFloat(bestBidAsk.spread);
 
  if (spread > 1.0) { // Wide spread opportunity
    console.log(`Wide spread detected on ${symbol}: ${spread}`);
    // Consider placing maker orders
  }
});

Liquidity Analysis

function analyzeLiquidity(symbol, priceRange = 0.01) {
  const orderbook = orderbookManager.getOrderbook(symbol);
  const bestBidAsk = orderbookManager.getBestBidAsk(symbol);
 
  const midPrice = (parseFloat(bestBidAsk.bestBid) + parseFloat(bestBidAsk.bestAsk)) / 2;
  const rangeMin = midPrice * (1 - priceRange);
  const rangeMax = midPrice * (1 + priceRange);
 
  let bidLiquidity = 0;
  let askLiquidity = 0;
 
  // Calculate liquidity within price range
  orderbook.bids.forEach((quantity, price) => {
    const priceNum = parseFloat(price);
    if (priceNum >= rangeMin) {
      bidLiquidity += parseFloat(quantity) * priceNum;
    }
  });
 
  orderbook.asks.forEach((quantity, price) => {
    const priceNum = parseFloat(price);
    if (priceNum <= rangeMax) {
      askLiquidity += parseFloat(quantity) * priceNum;
    }
  });
 
  return {
    bidLiquidity,
    askLiquidity,
    totalLiquidity: bidLiquidity + askLiquidity,
    liquidityImbalance: (bidLiquidity - askLiquidity) / (bidLiquidity + askLiquidity)
  };
}

Implementation Notes

  • Initial State: On subscription, you receive a full orderbook snapshot
  • Incremental Updates: All subsequent messages contain changes to apply
  • Price Level Management: Remove levels when quantity is "0.0"
  • Efficient Storage: Use appropriate data structures for your use case
  • Rate Limiting: Throttle UI updates for high-frequency markets
  • Error Recovery: Implement robust error handling and recovery

Related Endpoints