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.
Learn how to subscribe to OKX V5 WebSocket orderbook data in Python and JavaScript, with real code examples, auth setup, and error handling.
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.
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.
| Environment | URL | Auth Required |
|---|---|---|
| Production (Public) | wss://ws.okx.com:8443/ws/v5/public | No |
| Production (Private) | wss://ws.okx.com:8443/ws/v5/private | Yes |
| Demo Trading | wss://wspap.okx.com:8443/ws/v5/public | No |
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.
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.
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.
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.
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.
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.
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.