◈   ⌘ api · Intermediate

OKX V5 WebSocket Orderbook Subscription Guide

Learn how to subscribe to OKX V5 WebSocket orderbook data in Python and JavaScript, with real code examples, auth setup, and error handling.

Uncle Solieditor · voc · 06.05.2026 ·views 47
◈   Contents
  1. → Understanding the OKX V5 WebSocket Architecture
  2. → Python WebSocket Connection and Subscription
  3. → JavaScript Implementation with WebSocket API
  4. → Authenticated WebSocket for Private Channel Access
  5. → Practical Orderbook Strategies and Signal Integration
  6. → Production Reliability: Reconnection and Error Handling
  7. → Frequently Asked Questions
  8. → Conclusion

Real-time orderbook data is the heartbeat of algorithmic trading. If your strategy depends on spread dynamics, liquidity depth, or order flow imbalance, polling REST endpoints will always leave you one step behind. OKX's V5 WebSocket API gives you a persistent, low-latency stream directly into the exchange's order matching engine — and once you understand the subscription model, it becomes one of the cleanest market data interfaces in crypto.

OKX overhauled its API architecture with V5, consolidating spot, futures, and options under a unified endpoint structure. Unlike the fragmented V3 era, you now connect to a single WebSocket gateway and subscribe to channels using a consistent message format. This guide walks through the full setup: connection, authentication, orderbook channel subscription, snapshot vs. incremental updates, and production-grade error handling.

Understanding the OKX V5 WebSocket Architecture

OKX V5 exposes several WebSocket endpoints depending on your use case. Public market data — including orderbooks — doesn't require authentication. Private channels (orders, positions, account) do. For orderbook subscriptions you'll always connect to the public endpoint.

OKX V5 WebSocket Endpoints
EnvironmentURLAuth Required
Production (Public)wss://ws.okx.com:8443/ws/v5/publicNo
Production (Private)wss://ws.okx.com:8443/ws/v5/privateYes
Demo Tradingwss://wspap.okx.com:8443/ws/v5/publicNo

The V5 protocol uses a simple JSON message format. Every subscription is an 'op' (operation) with an 'args' array listing channels and instruments. Responses include a confirmation event and then a stream of data events. OKX supports two orderbook channels: 'books' (400 levels, full depth, incremental updates) and 'books5' (top 5 levels, full snapshot on every tick). For most strategies, 'books' is what you want — it gives you depth while minimizing bandwidth.

OKX sends a full snapshot first when you subscribe to 'books', then incremental updates. You must maintain a local orderbook and apply the deltas correctly. Skipping the snapshot handling is the #1 reason traders get corrupted orderbooks.

Python WebSocket Connection and Subscription

The cleanest Python approach uses the websockets library with asyncio. Here's a complete working example that connects to OKX, subscribes to the BTC-USDT-SWAP perpetual orderbook, and handles both the snapshot and incremental updates.

import asyncio
import json
import websockets
from collections import OrderedDict

OKX_WS_PUBLIC = "wss://ws.okx.com:8443/ws/v5/public"

class OKXOrderbook:
    def __init__(self):
        self.bids = {}  # price -> size
        self.asks = {}  # price -> size
        self.seq_id = None

    def apply_snapshot(self, data):
        """Full orderbook reset from snapshot."""
        self.bids = {float(p): float(s) for p, s, *_ in data["bids"]}
        self.asks = {float(p): float(s) for p, s, *_ in data["asks"]}
        self.seq_id = data.get("seqId")

    def apply_update(self, data):
        """Apply incremental delta."""
        # Validate sequence continuity
        if self.seq_id and data.get("prevSeqId") != self.seq_id:
            raise ValueError(f"Sequence gap detected: expected {self.seq_id}, got {data.get('prevSeqId')}")
        
        for price, size, *_ in data["bids"]:
            p = float(price)
            if float(size) == 0:
                self.bids.pop(p, None)  # remove level
            else:
                self.bids[p] = float(size)
        
        for price, size, *_ in data["asks"]:
            p = float(price)
            if float(size) == 0:
                self.asks.pop(p, None)
            else:
                self.asks[p] = float(size)
        
        self.seq_id = data.get("seqId")

    def best_bid(self):
        return max(self.bids.keys()) if self.bids else None

    def best_ask(self):
        return min(self.asks.keys()) if self.asks else None

    def spread(self):
        bb, ba = self.best_bid(), self.best_ask()
        return round(ba - bb, 2) if bb and ba else None


async def subscribe_orderbook(inst_id="BTC-USDT-SWAP"):
    ob = OKXOrderbook()
    subscribe_msg = {
        "op": "subscribe",
        "args": [{"channel": "books", "instId": inst_id}]
    }

    async with websockets.connect(OKX_WS_PUBLIC, ping_interval=20, ping_timeout=30) as ws:
        await ws.send(json.dumps(subscribe_msg))
        print(f"Subscribed to {inst_id} orderbook")

        async for raw in ws:
            msg = json.loads(raw)

            # Skip subscription confirmation
            if msg.get("event") == "subscribe":
                print(f"Confirmed: {msg}")
                continue

            if msg.get("event") == "error":
                print(f"OKX error: {msg}")
                break

            data_list = msg.get("data", [])
            action = msg.get("action")  # "snapshot" or "update"

            for data in data_list:
                try:
                    if action == "snapshot":
                        ob.apply_snapshot(data)
                    elif action == "update":
                        ob.apply_update(data)
                    
                    print(f"Best bid: {ob.best_bid()} | Best ask: {ob.best_ask()} | Spread: {ob.spread()}")
                except ValueError as e:
                    print(f"Sequence error — resubscribing: {e}")
                    await ws.send(json.dumps({"op": "unsubscribe", "args": [{"channel": "books", "instId": inst_id}]}))
                    await ws.send(json.dumps(subscribe_msg))


if __name__ == "__main__":
    asyncio.run(subscribe_orderbook("BTC-USDT-SWAP"))

A few things worth noting in this implementation: size == 0 in a delta means remove that price level entirely — this is the standard orderbook diff protocol used by OKX, Binance, and Bybit alike. The sequence ID check catches missed messages early; if you get a gap, the safest move is to unsubscribe and resubscribe to get a fresh snapshot rather than silently corrupting your local book.

JavaScript Implementation with WebSocket API

For Node.js environments or browser-based trading dashboards, here's the equivalent implementation using the native ws library. The logic for maintaining the orderbook is identical — snapshot first, then incremental deltas.

const WebSocket = require('ws');

const OKX_WS_PUBLIC = 'wss://ws.okx.com:8443/ws/v5/public';

class OKXOrderbook {
  constructor() {
    this.bids = new Map(); // price -> size
    this.asks = new Map();
    this.seqId = null;
  }

  applySnapshot(data) {
    this.bids = new Map(data.bids.map(([p, s]) => [parseFloat(p), parseFloat(s)]));
    this.asks = new Map(data.asks.map(([p, s]) => [parseFloat(p), parseFloat(s)]));
    this.seqId = data.seqId;
  }

  applyUpdate(data) {
    if (this.seqId !== null && data.prevSeqId !== this.seqId) {
      throw new Error(`Sequence gap: expected ${this.seqId}, got ${data.prevSeqId}`);
    }
    for (const [price, size] of data.bids) {
      const p = parseFloat(price);
      parseFloat(size) === 0 ? this.bids.delete(p) : this.bids.set(p, parseFloat(size));
    }
    for (const [price, size] of data.asks) {
      const p = parseFloat(price);
      parseFloat(size) === 0 ? this.asks.delete(p) : this.asks.set(p, parseFloat(size));
    }
    this.seqId = data.seqId;
  }

  bestBid() { return this.bids.size ? Math.max(...this.bids.keys()) : null; }
  bestAsk() { return this.asks.size ? Math.min(...this.asks.keys()) : null; }
  spread() {
    const bb = this.bestBid(), ba = this.bestAsk();
    return bb && ba ? (ba - bb).toFixed(2) : null;
  }
}

function subscribeOrderbook(instId = 'BTC-USDT-SWAP') {
  const ob = new OKXOrderbook();
  const ws = new WebSocket(OKX_WS_PUBLIC);
  const subMsg = JSON.stringify({
    op: 'subscribe',
    args: [{ channel: 'books', instId }]
  });

  ws.on('open', () => {
    ws.send(subMsg);
    console.log(`Subscribed to ${instId} orderbook`);
    // Keepalive ping every 25 seconds
    setInterval(() => ws.readyState === WebSocket.OPEN && ws.send('ping'), 25000);
  });

  ws.on('message', (raw) => {
    if (raw.toString() === 'pong') return;
    const msg = JSON.parse(raw);

    if (msg.event === 'error') {
      console.error('OKX error:', msg);
      return;
    }
    if (msg.event === 'subscribe') {
      console.log('Confirmed:', msg);
      return;
    }

    const { action, data = [] } = msg;
    for (const tick of data) {
      try {
        action === 'snapshot' ? ob.applySnapshot(tick) : ob.applyUpdate(tick);
        console.log(`Bid: ${ob.bestBid()} | Ask: ${ob.bestAsk()} | Spread: ${ob.spread()}`);
      } catch (err) {
        console.warn('Resyncing orderbook:', err.message);
        ws.send(JSON.stringify({ op: 'unsubscribe', args: [{ channel: 'books', instId }] }));
        ws.send(subMsg);
      }
    }
  });

  ws.on('close', (code) => {
    console.warn(`WebSocket closed (${code}), reconnecting in 3s...`);
    setTimeout(() => subscribeOrderbook(instId), 3000);
  });

  ws.on('error', (err) => console.error('WebSocket error:', err.message));
}

subscribeOrderbook('BTC-USDT-SWAP');
OKX requires a ping/pong keepalive. Send the string 'ping' every 25-30 seconds — the server responds with 'pong'. If you miss it, OKX will close the connection after 30 seconds of inactivity. Always handle the 'close' event and reconnect automatically.

Authenticated WebSocket for Private Channel Access

Public orderbook data needs no credentials. But if your strategy also needs to track your own orders, positions, or account balance alongside market depth, you'll need to authenticate the private WebSocket connection. OKX V5 uses HMAC-SHA256 signing.

import hmac
import hashlib
import base64
import time
import json

def generate_okx_signature(secret_key: str, timestamp: str, method: str = "GET", path: str = "/users/self/verify", body: str = "") -> str:
    """Generate HMAC-SHA256 signature for OKX WebSocket auth."""
    message = timestamp + method + path + body
    signature = hmac.new(
        secret_key.encode("utf-8"),
        message.encode("utf-8"),
        digestmod=hashlib.sha256
    ).digest()
    return base64.b64encode(signature).decode()

def build_auth_message(api_key: str, secret_key: str, passphrase: str) -> dict:
    timestamp = str(int(time.time()))
    sign = generate_okx_signature(secret_key, timestamp)
    return {
        "op": "login",
        "args": [{
            "apiKey": api_key,
            "passphrase": passphrase,
            "timestamp": timestamp,
            "sign": sign
        }]
    }

# Usage: send this as the first message after connecting to the private endpoint
# auth_msg = build_auth_message(API_KEY, SECRET_KEY, PASSPHRASE)
# await ws.send(json.dumps(auth_msg))
# Then subscribe to private channels after receiving the login confirmation event

After sending the login message, wait for the event response confirming successful authentication before subscribing to any private channels. A common pattern is to subscribe to both public orderbook data and private order updates on separate connections — one public WebSocket for market depth, one private WebSocket for execution state. This keeps concerns separated and avoids auth errors from disrupting your market data stream.

Practical Orderbook Strategies and Signal Integration

Raw orderbook data becomes useful when you derive signals from it. Here are the metrics that matter most in practice:

Platforms like VoiceOfChain aggregate real-time signals across major venues including OKX, Bybit, and Binance — useful for cross-referencing whether an orderbook signal on one exchange is idiosyncratic or part of a broader market move. When OBI spikes on OKX perps while VoiceOfChain shows a buy signal across multiple exchanges, the confluence is significantly more reliable than either signal alone.

For arbitrage setups, the OKX orderbook WebSocket pairs naturally with similar streams from Binance (wss://stream.binance.com:9443/ws/) and Bybit (wss://stream.bybit.com/v5/public). Running three concurrent async connections and comparing best bids and asks in real time is straightforward with the asyncio pattern shown above. Gate.io and Bitget also offer V5-compatible WebSocket orderbook feeds if you want broader venue coverage.

Production Reliability: Reconnection and Error Handling

A WebSocket connection that works in testing but breaks in production is worse than useless — your strategy is trading on stale data. These are the failure modes you must handle:

Never assume your local orderbook is correct after a reconnection. Always wait for and apply the fresh snapshot before generating signals. One bad trade from a stale orderbook will cost more than the extra complexity of proper resync logic.

Frequently Asked Questions

What is the difference between OKX 'books' and 'books5' channels?
'books' delivers up to 400 price levels with incremental delta updates — you get a snapshot first, then only changed levels. 'books5' sends only the top 5 bid and ask levels, but as a full snapshot on every update (no delta logic needed). Use 'books5' for simple spread monitoring; use 'books' when your strategy needs depth data or order imbalance calculations.
Do I need an API key to subscribe to OKX orderbook data?
No. Orderbook data is public and requires no authentication. You only need API keys when subscribing to private channels like orders, positions, or account balance. Connect to the public endpoint (wss://ws.okx.com:8443/ws/v5/public) without any login message.
How do I handle a sequence gap in the OKX orderbook stream?
When you receive an update where prevSeqId does not match your stored seqId, your local orderbook has a gap and is unreliable. Immediately unsubscribe and resubscribe to the same channel. OKX will send a fresh full snapshot, which you apply to reset the book. Never attempt to reconstruct the missing data.
How many simultaneous orderbook subscriptions can one OKX WebSocket connection handle?
OKX allows up to 240 subscribe/unsubscribe operations per hour per connection. You can subscribe to multiple instrument orderbooks in a single message by passing multiple objects in the 'args' array. For very large-scale data collection across dozens of pairs, use multiple connections and distribute subscriptions across them.
Is the OKX V5 WebSocket orderbook format the same for spot and perpetuals?
Yes, the channel name ('books' or 'books5') and message structure are identical across spot, perpetuals, and futures. The only difference is the instId format: 'BTC-USDT' for spot, 'BTC-USDT-SWAP' for perpetuals, 'BTC-USD-250328' for dated futures. The snapshot/delta logic and sequence ID handling are the same for all instrument types.
How does OKX WebSocket orderbook latency compare to Binance?
Both exchanges offer sub-100ms latency for orderbook updates under normal conditions. The practical difference depends on your server location relative to their data centers — OKX's primary infrastructure is in Hong Kong, Binance in Tokyo. For latency-sensitive strategies, co-location or a nearby VPS matters far more than the protocol differences between the two.

Conclusion

The OKX V5 WebSocket orderbook API is well-designed once you internalize two things: maintain your local book correctly (snapshot then deltas, validate sequences), and treat the connection as ephemeral (reconnect logic is not optional). The Python and JavaScript implementations above are production-ready starting points — not toy examples. The sequence validation, resync logic, and keepalive handling are what separate a strategy that runs reliably for months from one that quietly breaks at 3am.

From here, the natural next step is layering derived signals on top of the raw book: imbalance ratios, depth-weighted mid-price, spread z-scores. Cross-reference those with broader market signals from tools like VoiceOfChain to build confluence-based entries rather than reacting to single-exchange microstructure noise. The data is there — what you do with it is the strategy.

◈   more on this topic
◉ basics Mastering the ccxt library documentation for crypto traders ⌂ exchanges Mastering the Binance CCXT Library for Crypto Traders ⌬ bots Best Crypto Trading Bots 2025: Profitable AI-Powered Strategies