◈   ⌘ api · Intermediate

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.

Uncle Solieditor · voc · 06.04.2026 ·views 534
◈   Contents
  1. → WebSocket vs REST: Why It Matters for Active Trading
  2. → Connecting to OKX WebSocket API: First Steps
  3. → OKX WebSocket Channels: Subscribing to Market Data
  4. → Authentication: Accessing Private Channels
  5. → Building a Resilient Connection with Error Handling
  6. → Frequently Asked Questions
  7. → Conclusion

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.

WebSocket vs REST: Why It Matters for Active Trading

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.

Connecting to OKX WebSocket API: First Steps

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.

OKX WebSocket Channels: Subscribing to Market Data

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())
OKX WebSocket v5 Key Endpoints
EndpointURLAuth Required
Public market datawss://ws.okx.com:8443/ws/v5/publicNo
Private account datawss://ws.okx.com:8443/ws/v5/privateYes
Demo trading (paper)wss://wspap.okx.com:8443/ws/v5/publicNo
Business channelswss://ws.okx.com:8443/ws/v5/businessOptional

Authentication: Accessing Private Channels

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.

Building a Resilient Connection with Error Handling

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.

Frequently Asked Questions

What is the maximum number of subscriptions per OKX WebSocket connection?
OKX allows up to 240 channel subscriptions per connection. You can open multiple connections if you need more coverage — up to 3 connections per endpoint URL is the recommended ceiling. Exceeding subscription limits triggers error code 60014.
Does the OKX WebSocket v5 API support futures and options instruments?
Yes. The v5 API covers SPOT, MARGIN, SWAP (perpetual futures), FUTURES (delivery contracts), and OPTIONS. You specify the instrument type via instType in channel args, or use a specific instId like BTC-USDT-SWAP for the BTC perpetual.
How often does OKX push order book updates over WebSocket?
The books channel sends a full snapshot first, then incremental updates as bids and asks change. During active markets, updates can arrive multiple times per second. The lighter books5 channel (top 5 levels) updates at roughly 100ms intervals and is preferable for most trading bots.
Can I use OKX WebSocket API without creating an account or API key?
Yes — all public channels including tickers, order books, trades, and candles require zero authentication. API credentials are only needed for private channels that expose your own account data, open orders, or positions.
How do I handle the OKX WebSocket ping/pong keepalive correctly?
OKX sends a plain text message containing the string 'ping' every 30 seconds — not a WebSocket protocol-level ping frame. You must detect this string and reply with the plain text 'pong' within 30 seconds. Libraries that auto-handle WebSocket pings will not catch this; you need to handle it explicitly in your message loop.
What should I do if my OKX WebSocket connection keeps dropping after 30 seconds?
This is almost always a missing pong response. Check that your message handler explicitly checks for the string 'ping' and sends back 'pong' as a text message. If you're using a library that buffers messages, also verify that buffering isn't delaying your pong past the 30-second timeout window.

Conclusion

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.

◈   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