◈   ⌘ api · Intermediate

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.

Uncle Solieditor · voc · 18.05.2026 ·views 1
◈   Contents
  1. → Why WebSocket Beats REST for Active Trading
  2. → Setting Up Your Python Environment
  3. → Authentication: Signing the Login Request
  4. → Subscribing to Channels and Streaming Market Data
  5. → Error Handling and Production Reconnection Logic
  6. → Private Channels: Streaming Your Own Orders and Positions
  7. → Frequently Asked Questions
  8. → Conclusion

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.

Why WebSocket Beats REST for Active Trading

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.

Setting Up Your Python Environment

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.

Authentication: Signing the Login Request

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.

Subscribing to Channels and Streaming Market Data

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.

Error Handling and Production Reconnection Logic

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.

Private Channels: Streaming Your Own Orders and Positions

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.

OKX V5 WebSocket Channel Reference
ChannelTypeDescriptionAuth Required
tickersPublicBest bid/ask, last price, 24h volume statsNo
books5PublicTop 5 order book levels, full snapshotNo
booksPublicFull order book with incremental delta updatesNo
tradesPublicIndividual trade events as they executeNo
candle1mPublic1-minute OHLCV candles, pushed on each closeNo
ordersPrivateYour order state changes — fills, cancelsYes
positionsPrivateLive position updates with unrealized P&LYes
balance-and-positionPrivateAccount balance and position combined pushYes

Frequently Asked Questions

What is the difference between OKX V5 public and private WebSocket endpoints?
The public endpoint (wss://ws.okx.com:8443/ws/v5/public) streams market data — prices, order books, trades — and requires no authentication. The private endpoint (/ws/v5/private) streams your account data — orders, fills, positions, balance — and requires a signed login frame sent immediately after connecting.
How many channels can I subscribe to on a single OKX WebSocket connection?
OKX allows up to 480 subscriptions per connection. For most bots tracking 10–20 symbols across a few channels, one connection is more than enough. If you need broader coverage, open multiple connections and split subscriptions across them.
Why does my OKX WebSocket connection keep dropping silently?
OKX's server closes connections that are idle for more than 30 seconds. Set ping_interval=20 in your websockets.connect() call — the library automatically sends WebSocket ping frames to keep the connection alive. Also verify your network's NAT or firewall doesn't have a shorter TCP idle timeout than the exchange.
Does OKX V5 WebSocket work the same way as Binance WebSocket streams?
The underlying protocol is identical, but the message format differs. Binance uses combined stream URLs and snake_case field names; OKX uses a subscription op/args model with camelCase fields like instId and bidPx. Authentication also differs — Binance uses a listenKey obtained from REST, while OKX signs directly in the WebSocket login frame itself.
Can I place orders through the OKX V5 WebSocket API?
Yes. OKX supports order placement, amendment, and cancellation over WebSocket on the private endpoint using op: 'order' frames. This is meaningfully faster than REST for high-frequency strategies because it eliminates the HTTP connection overhead on each request.
What happens if my bot misses messages during a reconnect?
WebSocket streams do not guarantee delivery across disconnects — events during the gap are lost. For order book data, fetch a REST snapshot after reconnecting and use the sequence numbers in subsequent book updates to detect gaps. For order fills, always reconcile against GET /api/v5/trade/fills after any reconnect to catch missed events.

Conclusion

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.

◈   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