OKX V5 API WebSocket Python: Real-Time Trading Guide
A practical guide to using Python with OKX's V5 WebSocket API. Learn authentication, channel subscriptions, and real-time data parsing with working code examples.
A practical guide to using Python with OKX's V5 WebSocket API. Learn authentication, channel subscriptions, and real-time data parsing with working code examples.
REST APIs are fine for batch queries and order placement, but the moment you need live price feeds, order book snapshots, or trade execution confirmations, polling becomes a liability. Every extra HTTP round-trip adds latency and burns rate limits. The OKX V5 WebSocket API solves this by pushing data to your Python client the instant it changes — no polling, no wasted requests, no stale candles. Once you have this wired up, you are not just reacting to the market: you are watching it move in real time.
Every major exchange — OKX, Binance, Bybit, and Bitget — now offers both REST and WebSocket endpoints, but the trade-offs are not symmetric. REST is a request-response model: you ask, they answer. WebSocket is a persistent bidirectional channel: you subscribe once, data flows until you disconnect. For a scalping bot checking BTC/USDT prices every 100ms, REST would hammer rate limits and introduce 10–50ms of overhead per request. The same setup over WebSocket costs one handshake and then streams ticks with sub-millisecond server-side latency.
OKX's V5 API consolidates public market data — tickers, order books, candlesticks — and private account streams — orders, positions, account balance — under a unified WebSocket architecture. The public endpoint needs no authentication. The private endpoint requires HMAC-SHA256 signing with your API key, secret, and passphrase, which is exactly what most beginners get wrong on their first attempt.
OKX exposes two WebSocket environments: live trading (wss://ws.okx.com:8443/ws/v5/public and /private) and demo/paper trading (wss://wspap.okx.com:8443/ws/v5/public). Always test on the demo endpoint before connecting to live markets.
You need three things: the websockets library for the async connection, hmac and hashlib from the standard library for request signing, and asyncio for managing concurrent streams. If you plan to watch multiple symbols simultaneously — BTC/USDT and ETH/USDT order books at the same time, for example — asyncio is not optional, it is essential. The examples below use websockets v12+ and Python 3.9+.
pip install websockets
# Optional but recommended
pip install loguru
If you run pip show websockets and see a version below 10, upgrade it. The async context manager interface changed significantly between major versions and older code patterns will silently misbehave rather than throwing clean errors.
Public channels need no authentication. Private channels — your orders, positions, fills — require a login frame sent immediately after the WebSocket handshake completes. OKX's V5 auth is a HMAC-SHA256 signature of the string timestamp + 'GET' + '/users/self/verify', base64-encoded. One critical gotcha: the timestamp is Unix epoch in seconds as a string, not milliseconds. Coming from Binance or Bybit, both of which use milliseconds, this trips up almost everyone the first time.
import hmac
import hashlib
import base64
import time
import json
API_KEY = "your_api_key"
SECRET_KEY = "your_secret_key"
PASSPHRASE = "your_passphrase"
def generate_signature(timestamp: str, secret: str) -> str:
message = f"{timestamp}GET/users/self/verify"
mac = hmac.new(
secret.encode("utf-8"),
message.encode("utf-8"),
digestmod=hashlib.sha256
)
return base64.b64encode(mac.digest()).decode("utf-8")
def build_login_message(api_key: str, secret: str, passphrase: str) -> str:
timestamp = str(int(time.time()))
sign = generate_signature(timestamp, secret)
payload = {
"op": "login",
"args": [{
"apiKey": api_key,
"passphrase": passphrase,
"timestamp": timestamp,
"sign": sign
}]
}
return json.dumps(payload)
# Quick sanity check — prints the login frame
print(build_login_message(API_KEY, SECRET_KEY, PASSPHRASE))
A successful login returns {"event": "login", "code": "0", "msg": ""}. Any non-zero code means authentication failed. The most common causes: using the API key instead of the secret key for signing, a system clock that is out of sync (OKX rejects frames where the timestamp deviates more than 30 seconds from server time), or a passphrase that contains special characters that were URL-encoded when the key was created.
After the connection opens — and after login confirmation if using private channels — you send a subscription frame specifying which channels and instruments you want. OKX organizes data by channel name and instId (instrument ID like BTC-USDT). The most useful public channels for traders are: tickers for best bid/ask and 24h stats, books5 for a 5-level order book snapshot, trades for individual trade events, and candle1m through candle1D for OHLCV candles.
import asyncio
import json
import websockets
PUBLIC_WS_URL = "wss://ws.okx.com:8443/ws/v5/public"
async def stream_ticker(symbol: str = "BTC-USDT"):
subscribe_msg = json.dumps({
"op": "subscribe",
"args": [
{"channel": "tickers", "instId": symbol},
{"channel": "books5", "instId": symbol}
]
})
async with websockets.connect(PUBLIC_WS_URL) as ws:
await ws.send(subscribe_msg)
print(f"Subscribed to {symbol} ticker and order book")
async for raw_msg in ws:
msg = json.loads(raw_msg)
# OKX sends subscription confirmations as events
if "event" in msg:
status = msg.get("code", "0")
print(f"[EVENT] {msg['event']} code={status}")
continue
if "data" not in msg:
continue
channel = msg.get("arg", {}).get("channel", "")
if channel == "tickers":
tick = msg["data"][0]
print(f"[TICKER] last={tick['last']} bid={tick['bidPx']} ask={tick['askPx']}")
elif channel == "books5":
book = msg["data"][0]
best_bid = book["bids"][0][0] if book["bids"] else "N/A"
best_ask = book["asks"][0][0] if book["asks"] else "N/A"
print(f"[BOOK5] bid={best_bid} ask={best_ask}")
asyncio.run(stream_ticker("BTC-USDT"))
Run this and you'll see a firehose of ticks. BTC-USDT on OKX typically generates several ticker updates per second during active sessions. To stream multiple pairs — BTC-USDT, ETH-USDT, SOL-USDT — add them all to the args list in the same subscription message rather than opening separate connections. OKX allows up to 480 subscriptions per connection, so for most bots a single connection covers everything.
WebSocket code without reconnection handling is a time bomb. Network blips, exchange maintenance windows, and load balancer timeouts will drop your connection — the only question is when, not if. The pattern below wraps the stream in a retry loop with exponential backoff, logs connection events, and re-subscribes cleanly after each reconnect. This same approach works on Binance futures and Bybit's v5 API with minimal changes.
import asyncio
import json
import websockets
from websockets.exceptions import ConnectionClosedError, ConnectionClosedOK
PUBLIC_WS_URL = "wss://ws.okx.com:8443/ws/v5/public"
SYMBOLS = ["BTC-USDT", "ETH-USDT", "SOL-USDT"]
def build_subscribe_msg(symbols: list) -> str:
args = [{"channel": "tickers", "instId": s} for s in symbols]
return json.dumps({"op": "subscribe", "args": args})
async def handle_message(msg: dict) -> None:
if "event" in msg:
if msg["event"] == "error":
print(f"[OKX ERROR] code={msg.get('code')} msg={msg.get('msg')}")
return
data = msg.get("data", [])
channel = msg.get("arg", {}).get("channel", "")
if channel == "tickers" and data:
tick = data[0]
ts = int(tick.get("ts", 0)) / 1000
print(f"[{tick['instId']}] last={tick['last']} vol24h={tick['vol24h']} ts={ts:.3f}")
async def run_stream():
retry_delay = 1
max_delay = 60
while True:
try:
async with websockets.connect(
PUBLIC_WS_URL,
ping_interval=20, # keeps conn alive; OKX drops idle after 30s
ping_timeout=10
) as ws:
print("[WS] Connected")
await ws.send(build_subscribe_msg(SYMBOLS))
retry_delay = 1 # reset backoff on clean connect
async for raw in ws:
try:
await handle_message(json.loads(raw))
except json.JSONDecodeError as e:
print(f"[WARN] Bad JSON: {e}")
except (ConnectionClosedError, ConnectionClosedOK) as e:
print(f"[WS] Closed: {e}. Retry in {retry_delay}s")
except Exception as e:
print(f"[WS] Error: {e}. Retry in {retry_delay}s")
await asyncio.sleep(retry_delay)
retry_delay = min(retry_delay * 2, max_delay)
asyncio.run(run_stream())
The ping_interval=20 parameter is not optional in production. OKX's server silently drops connections that go quiet for more than 30 seconds. Without it you'll see mysterious gaps in your data that look like strategy bugs. The websockets library sends ping frames automatically and handles the pong response — you don't need to implement this yourself.
If you want pre-processed signals layered on top of raw WebSocket data — whale movements, order flow imbalance, breakout alerts — VoiceOfChain aggregates these streams across multiple exchanges and surfaces them as actionable trading signals in real time. Pair your WebSocket bot with VoiceOfChain to get both raw market data and high-level context without building the analytics layer yourself.
For a live trading bot, you also need to track your own order fills, position changes, and balance updates without polling REST. OKX's private WebSocket channels handle this cleanly. Instead of polling GET /api/v5/trade/orders-pending every few seconds, the orders channel pushes every state change — open, partial-fill, filled, cancelled — the moment it happens on the matching engine.
The flow for private channels: connect to wss://ws.okx.com:8443/ws/v5/private, send the login frame, wait for the confirmation event with code 0, then subscribe. The key private channels are orders for fill notifications, positions for live P&L tracking, and balance-and-position for a combined account snapshot that updates atomically on each trade.
| Channel | Type | Description | Auth Required |
|---|---|---|---|
| tickers | Public | Best bid/ask, last price, 24h volume stats | No |
| books5 | Public | Top 5 order book levels, full snapshot | No |
| books | Public | Full order book with incremental delta updates | No |
| trades | Public | Individual trade events as they execute | No |
| candle1m | Public | 1-minute OHLCV candles, pushed on each close | No |
| orders | Private | Your order state changes — fills, cancels | Yes |
| positions | Private | Live position updates with unrealized P&L | Yes |
| balance-and-position | Private | Account balance and position combined push | Yes |
The OKX V5 WebSocket API gives Python traders direct access to exchange-quality data streams with minimal overhead. The three core pieces — authentication signing, channel subscription, and reconnection handling — are not complicated once you have working code in front of you. Start with public tickers to get comfortable with the message format, then layer in private order streams when you're ready to automate execution. Pair the raw feed with a signal layer like VoiceOfChain to bridge the gap between data and decision, and you have the foundation of a serious algorithmic trading setup.