OKX WebSocket API Documentation: A Complete Trader's Guide
Learn to connect to OKX WebSocket API with step-by-step code examples. Covers authentication, market data subscriptions, order book feeds, and error handling for serious algo traders.
Learn to connect to OKX WebSocket API with step-by-step code examples. Covers authentication, market data subscriptions, order book feeds, and error handling for serious algo traders.
If you're building a trading bot or just want live market data without hammering REST endpoints every second, OKX WebSocket API is what you need. Unlike platforms such as Coinbase which historically had more limited streaming options, OKX's WebSocket interface gives you access to real-time tickers, full order books, trade feeds, and private account data — all over a single persistent connection. The okx websocket api documentation has evolved significantly, and the v5 API is now the standard. This guide walks you through everything from the first connection to production-ready error handling.
REST APIs work on a request-response model — you ask, you receive. That's fine for placing orders or checking balances. But for market data that changes hundreds of times per second, polling a REST endpoint every 500ms is both slow and wasteful. You'll hit rate limits fast, and your data will always be slightly stale. WebSockets flip the model: you open one connection and the exchange pushes updates to you the moment they happen. On Binance, Bybit, and OKX alike, the difference between WebSocket and REST latency for order book data can be 50–500ms — a gap that matters enormously in volatile markets. For platforms like VoiceOfChain that aggregate real-time signals across multiple assets, WebSocket connections are the only viable architecture. REST simply can't keep up with a live trading environment at scale.
OKX offers separate WebSocket endpoints for public data (no auth required) and private data (requires API key). Always test with public channels first before adding authentication complexity.
OKX runs two main WebSocket environments. The public endpoint at wss://ws.okx.com:8443/ws/v5/public handles market data anyone can read. The private endpoint at wss://ws.okx.com:8443/ws/v5/private handles account data like positions, orders, and balance updates. There's also a demo trading endpoint at wss://wspap.okx.com:8443/ws/v5/public for testing without real funds — use it before going live. The connection flow is simple: open the socket, send a subscribe message with the channels you want, then listen for incoming pushes. The server sends a text ping every 30 seconds; you must respond with a text pong or the connection drops. Here's a minimal working example in Python:
import asyncio
import websockets
import json
OKX_PUBLIC_WS = "wss://ws.okx.com:8443/ws/v5/public"
async def stream_btc_ticker():
async with websockets.connect(OKX_PUBLIC_WS) as ws:
# Subscribe to BTC-USDT real-time ticker
subscribe = {
"op": "subscribe",
"args": [
{"channel": "tickers", "instId": "BTC-USDT"}
]
}
await ws.send(json.dumps(subscribe))
print("Subscribed to BTC-USDT ticker")
async for message in ws:
# OKX sends text-based pings, not WebSocket-level pings
if message == "ping":
await ws.send("pong")
continue
data = json.loads(message)
if data.get("event") == "subscribe":
print(f"Confirmed subscription: {data['arg']}")
continue
if data.get("event") == "error":
print(f"Error {data['code']}: {data['msg']}")
continue
if "data" in data:
ticker = data["data"][0]
print(
f"BTC | Last: {ticker['last']} "
f"| Bid: {ticker['bidPx']} "
f"| Ask: {ticker['askPx']} "
f"| 24h Vol: {ticker['vol24h']}"
)
asyncio.run(stream_btc_ticker())
Install the dependency first with pip install websockets. The response structure is consistent across all channels: an event field for subscription confirmations and errors, a data array containing the actual payload, and an arg field echoing which channel the update came from. Once you see the confirmation message, updates start flowing immediately.
The public API offers a wide range of channels covering every data type a trader could need. The most useful ones are tickers (best bid/ask plus last price), books (full order book with up to 400 levels), books5 (top 5 levels only — lighter and faster), trades (every individual trade as it executes), candle1m through candle1W (OHLCV candles across timeframes), and mark-price (for futures and options fair value). Each channel targets a specific instrument via the instId field. You can subscribe to multiple channels in a single message, which is more efficient than separate subscribe calls. Compared to Bybit's WebSocket structure, OKX's channel naming is more explicit and easier to parse programmatically. Gate.io has a similar subscription model but different field names — always check the specific docs when switching exchanges.
import asyncio
import websockets
import json
async def multi_channel_subscription():
async with websockets.connect("wss://ws.okx.com:8443/ws/v5/public") as ws:
# Subscribe to multiple channels in a single message
subscribe = {
"op": "subscribe",
"args": [
{"channel": "books5", "instId": "BTC-USDT"}, # Top 5 order book levels
{"channel": "trades", "instId": "BTC-USDT"}, # Live trade feed
{"channel": "tickers", "instId": "ETH-USDT"}, # ETH ticker
{"channel": "candle1m", "instId": "BTC-USDT"} # 1-minute candles
]
}
await ws.send(json.dumps(subscribe))
async for message in ws:
if message == "ping":
await ws.send("pong")
continue
data = json.loads(message)
if "data" not in data:
continue
channel = data.get("arg", {}).get("channel", "")
inst_id = data.get("arg", {}).get("instId", "")
if channel == "books5":
book = data["data"][0]
best_bid = book["bids"][0][0] if book["bids"] else None
best_ask = book["asks"][0][0] if book["asks"] else None
print(f"[Book] {inst_id} | Bid: {best_bid} | Ask: {best_ask}")
elif channel == "trades":
for trade in data["data"]:
side = trade["side"].upper() # "BUY" or "SELL"
print(f"[Trade] {inst_id} | {side} {trade['sz']} @ {trade['px']}")
elif channel.startswith("candle"):
# Format: [ts, open, high, low, close, vol, volCcy, volCcyQuote, confirm]
c = data["data"][0]
confirmed = "CLOSED" if c[8] == "1" else "live"
print(f"[{channel}] {inst_id} | O:{c[1]} H:{c[2]} L:{c[3]} C:{c[4]} [{confirmed}]")
asyncio.run(multi_channel_subscription())
| Endpoint | URL | Auth Required |
|---|---|---|
| Public market data | wss://ws.okx.com:8443/ws/v5/public | No |
| Private account data | wss://ws.okx.com:8443/ws/v5/private | Yes |
| Demo trading (paper) | wss://wspap.okx.com:8443/ws/v5/public | No |
| Business channels | wss://ws.okx.com:8443/ws/v5/business | Optional |
Private channels expose your own account data: live order status updates, position changes, and balance snapshots. This is essential for any trading bot that needs to react to fills in real time instead of polling REST. OKX uses HMAC-SHA256 signing for WebSocket authentication. The signature covers a timestamp string, the literal GET, and the path /users/self/verify concatenated together. You must send the login operation within 30 seconds of opening the connection, otherwise OKX closes the socket automatically. One detail that trips people up: OKX API keys have a passphrase set at creation time — this is a third credential separate from the key and secret, and it's required for all authenticated calls. Bitget has a similar three-credential pattern; Binance does not use a passphrase, so don't assume the same structure across exchanges.
import asyncio
import websockets
import json
import hmac
import hashlib
import base64
import time
import os
API_KEY = os.environ["OKX_API_KEY"]
SECRET_KEY = os.environ["OKX_SECRET_KEY"]
PASSPHRASE = os.environ["OKX_PASSPHRASE"]
def sign_okx_ws(timestamp: str, secret: str) -> str:
"""Generate HMAC-SHA256 signature for OKX WebSocket login."""
message = f"{timestamp}GET/users/self/verify"
mac = hmac.new(
bytes(secret, "utf-8"),
bytes(message, "utf-8"),
digestmod=hashlib.sha256
)
return base64.b64encode(mac.digest()).decode()
async def private_channel_stream():
async with websockets.connect("wss://ws.okx.com:8443/ws/v5/private") as ws:
# Step 1: Authenticate
timestamp = str(int(time.time()))
login_msg = {
"op": "login",
"args": [{
"apiKey": API_KEY,
"passphrase": PASSPHRASE,
"timestamp": timestamp,
"sign": sign_okx_ws(timestamp, SECRET_KEY)
}]
}
await ws.send(json.dumps(login_msg))
response = json.loads(await ws.recv())
if response.get("event") != "login" or response.get("code") != "0":
raise RuntimeError(f"OKX auth failed: {response}")
print("Authenticated successfully")
# Step 2: Subscribe to private channels
await ws.send(json.dumps({
"op": "subscribe",
"args": [
{"channel": "orders", "instType": "SPOT"},
{"channel": "account", "ccy": "USDT"}
]
}))
# Step 3: Process live account updates
async for message in ws:
if message == "ping":
await ws.send("pong")
continue
data = json.loads(message)
if "data" not in data:
continue
channel = data["arg"]["channel"]
if channel == "orders":
for order in data["data"]:
print(
f"Order {order['ordId']} | "
f"State: {order['state']} | "
f"Filled: {order['fillSz']} / {order['sz']}"
)
elif channel == "account":
for detail in data["data"][0].get("details", []):
print(f"Balance | {detail['ccy']}: {detail['availBal']} available")
asyncio.run(private_channel_stream())
Never hardcode API credentials in source files. Use environment variables or a secrets manager. If you accidentally commit OKX keys to a public repository, revoke them immediately — compromised keys have been used to drain accounts within minutes of exposure.
WebSocket connections drop. It happens — network blips, server-side restarts, idle timeouts. Any production trading system needs to handle disconnections gracefully and reconnect without human intervention. OKX sends error messages as JSON with an event field set to error and a code field. Common codes include 60001 (invalid arguments), 60009 (login required for private channel), and 60014 (too many subscriptions — max is 240 per connection). The connection itself can also close from external causes that never arrive as an OKX error object — in those cases you catch the websockets.ConnectionClosed exception directly. The pattern below uses exponential backoff, which is standard practice. VoiceOfChain uses a similar reconnection architecture to keep real-time signal feeds running continuously across dozens of instruments without any manual restarts.
import asyncio
import websockets
import json
import logging
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s")
logger = logging.getLogger("okx_ws")
CHANNELS = [
{"channel": "books5", "instId": "BTC-USDT"},
{"channel": "tickers", "instId": "ETH-USDT"}
]
OKX_ERRORS = {
"60001": "Invalid request format",
"60002": "Invalid op value",
"60009": "Login required for this channel",
"60014": "Too many subscriptions (max 240 per connection)",
"60018": "Invalid channel name"
}
async def run_with_reconnect():
delay = 1
max_delay = 60
while True:
try:
logger.info("Connecting to OKX WebSocket...")
async with websockets.connect(
"wss://ws.okx.com:8443/ws/v5/public",
ping_interval=None, # We handle OKX text pings manually
close_timeout=5
) as ws:
delay = 1 # Reset backoff on successful connect
await ws.send(json.dumps({"op": "subscribe", "args": CHANNELS}))
logger.info(f"Subscribed to {len(CHANNELS)} channels")
async for raw in ws:
if raw == "ping":
await ws.send("pong")
continue
msg = json.loads(raw)
if msg.get("event") == "error":
code = msg.get("code", "unknown")
desc = OKX_ERRORS.get(code, msg.get("msg", "Unknown"))
logger.error(f"OKX error {code}: {desc}")
continue
if "data" in msg:
channel = msg["arg"]["channel"]
logger.debug(f"{channel} update: {len(msg['data'])} records")
# Your processing logic goes here
except websockets.ConnectionClosed as e:
logger.warning(f"Connection closed — code={e.code} reason='{e.reason}'")
except Exception as e:
logger.error(f"Unexpected error: {type(e).__name__}: {e}")
finally:
logger.info(f"Reconnecting in {delay}s...")
await asyncio.sleep(delay)
delay = min(delay * 2, max_delay) # Exponential backoff with 60s cap
asyncio.run(run_with_reconnect())
The exponential backoff matters: if OKX is having issues on their end, reconnecting every second will get your IP flagged for aggressive behavior. Starting at 1 second and capping at 60 keeps recovery fast for transient drops while being respectful during prolonged outages. Log every disconnect event with a timestamp — when something goes wrong at 3am, that log is how you reconstruct exactly what happened.
The OKX WebSocket API v5 is one of the better-designed streaming interfaces in crypto — consistent message structure, sensible channel naming, and a demo environment that lets you validate everything safely before touching real funds. The okx websocket api documentation covers the protocol well, but there's still a gap between reading docs and building something that runs reliably in production. The patterns in this guide — text-based ping/pong handling, multi-channel subscriptions, HMAC-SHA256 authentication, and exponential backoff reconnection — are what separate a weekend script from a system you can trust at 3am. Whether you're feeding signals from VoiceOfChain into an automated executor or building your own strategy from scratch on OKX, getting the WebSocket layer right is the foundation everything else depends on.