OKX V5 API Authentication in Python: Complete Guide
Learn how to authenticate with OKX V5 API using Python, including HMAC signing, headers setup, and live trading requests with full code examples.
Learn how to authenticate with OKX V5 API using Python, including HMAC signing, headers setup, and live trading requests with full code examples.
OKX V5 API is one of the most capable interfaces in the crypto space — and if you're building an algo trading system, a signal executor, or just want to pull live market data programmatically, getting authentication right is the foundation everything else stands on. The signing process trips up a lot of traders coming from simpler APIs like Binance's or Bybit's, but once you understand the pattern, it clicks fast.
Before writing a single line of Python, you need three things from OKX: an API key, a secret key, and a passphrase. Unlike Binance, which only uses key + secret, OKX V5 requires all three for any authenticated request. Log into your OKX account, go to Account → API, and create a new key. Assign only the permissions your bot actually needs — read-only for data, trade permission only if you're executing orders. Never enable withdrawal permissions on a trading key.
Store your API key, secret, and passphrase in environment variables or a secrets manager — never hardcode them in your script. A leaked key on GitHub means a drained account.
OKX also lets you whitelist IP addresses per API key. If you're running a bot on a VPS, add that server's IP. This single step eliminates most key-theft risk even if credentials somehow leak.
OKX V5 uses HMAC-SHA256 request signing. Every authenticated request must include four headers: OK-ACCESS-KEY (your API key), OK-ACCESS-SIGN (the signature), OK-ACCESS-TIMESTAMP (ISO 8601 UTC timestamp), and OK-ACCESS-PASSPHRASE. The signature is computed over a concatenated string: timestamp + HTTP method + request path + request body (empty string for GET requests). The secret key is used as the HMAC key, and the result is base64-encoded.
The timestamp must be within 30 seconds of OKX's server time — if your system clock drifts, you'll get 'Invalid timestamp' errors constantly. This is a common gotcha that Bybit and Gate.io handle more leniently, but OKX enforces it strictly.
Here's a clean, reusable auth module you can drop into any project. It handles signature generation, header construction, and works for both REST GET and POST requests.
import hmac
import hashlib
import base64
import os
from datetime import datetime, timezone
API_KEY = os.getenv('OKX_API_KEY')
SECRET_KEY = os.getenv('OKX_SECRET_KEY')
PASSPHRASE = os.getenv('OKX_PASSPHRASE')
BASE_URL = 'https://www.okx.com'
def get_timestamp() -> str:
"""Return ISO 8601 UTC timestamp OKX expects."""
return datetime.now(timezone.utc).strftime('%Y-%m-%dT%H:%M:%S.%f')[:-3] + 'Z'
def sign(timestamp: str, method: str, path: str, body: str = '') -> str:
message = timestamp + method.upper() + path + body
mac = hmac.new(
SECRET_KEY.encode('utf-8'),
message.encode('utf-8'),
hashlib.sha256
)
return base64.b64encode(mac.digest()).decode()
def build_headers(method: str, path: str, body: str = '') -> dict:
ts = get_timestamp()
return {
'OK-ACCESS-KEY': API_KEY,
'OK-ACCESS-SIGN': sign(ts, method, path, body),
'OK-ACCESS-TIMESTAMP': ts,
'OK-ACCESS-PASSPHRASE': PASSPHRASE,
'Content-Type': 'application/json',
}
With this module, every request you make simply calls build_headers() with the method and path. The signature is computed fresh each time — you can't reuse signatures across requests.
Let's put authentication to work with two practical examples: fetching your account balance and pulling real-time order book data. These cover the two patterns you'll use constantly — authenticated private endpoints and public market data endpoints.
import requests
import json
# Authenticated: fetch account balance
def get_account_balance(currency: str = 'USDT') -> dict:
path = '/api/v5/account/balance'
params = f'?ccy={currency}'
headers = build_headers('GET', path + params)
resp = requests.get(BASE_URL + path + params, headers=headers, timeout=10)
resp.raise_for_status()
data = resp.json()
if data['code'] != '0':
raise RuntimeError(f"OKX API error {data['code']}: {data['msg']}")
return data['data']
# Public: fetch order book (no auth needed)
def get_order_book(inst_id: str = 'BTC-USDT', depth: int = 20) -> dict:
path = f'/api/v5/market/books?instId={inst_id}&sz={depth}'
resp = requests.get(BASE_URL + path, timeout=10)
resp.raise_for_status()
return resp.json()['data'][0]
# Example usage
if __name__ == '__main__':
balance = get_account_balance('USDT')
print(f"USDT balance: {balance}")
book = get_order_book('BTC-USDT')
best_bid = book['bids'][0][0]
best_ask = book['asks'][0][0]
print(f"BTC-USDT — Bid: {best_bid}, Ask: {best_ask}")
Notice how public endpoints like order books don't need headers at all — only requests to private endpoints (account, orders, positions) require the signed headers. This split is consistent across most exchanges: Binance, Coinbase Advanced Trade, and OKX all follow this public/private pattern, but OKX's three-credential system makes it uniquely explicit about passphrase management.
Reading data is safe. Placing orders is where bugs cost money. Here's a complete order placement function with the request body serialized correctly — OKX is strict about the body being an exact JSON string match between what you sign and what you send.
import json
def place_order(
inst_id: str,
side: str, # 'buy' or 'sell'
order_type: str, # 'market' or 'limit'
size: str, # quantity as string
price: str = '', # required for limit orders
td_mode: str = 'cash' # 'cash', 'cross', 'isolated'
) -> dict:
path = '/api/v5/trade/order'
payload = {
'instId': inst_id,
'tdMode': td_mode,
'side': side,
'ordType': order_type,
'sz': size,
}
if order_type == 'limit' and price:
payload['px'] = price
# Body must be serialized with separators to avoid whitespace differences
body = json.dumps(payload, separators=(',', ':'))
headers = build_headers('POST', path, body)
resp = requests.post(
BASE_URL + path,
headers=headers,
data=body,
timeout=10
)
resp.raise_for_status()
result = resp.json()
if result['code'] != '0':
raise RuntimeError(
f"Order failed — code: {result['code']}, msg: {result['msg']}, "
f"details: {result.get('data', [])}"
)
order_id = result['data'][0]['ordId']
print(f"Order placed: {order_id}")
return result['data'][0]
# Place a limit buy for 0.001 BTC at $60,000
try:
order = place_order(
inst_id='BTC-USDT',
side='buy',
order_type='limit',
size='0.001',
price='60000'
)
except RuntimeError as e:
print(f"Order error: {e}")
except requests.exceptions.RequestException as e:
print(f"Network error: {e}")
json.dumps with separators=(',', ':') is critical — if your signed body has spaces but the sent body doesn't (or vice versa), you'll get signature mismatch errors that are painful to debug.
OKX V5 returns structured error codes in the response body even when the HTTP status is 200 — you must check data['code'] on every response, not just the HTTP status. Code '0' means success; anything else is an error. Common codes you'll hit in production:
| Error Code | Meaning | Fix |
|---|---|---|
| 50111 | Invalid signature | Check SECRET_KEY encoding, body whitespace, timestamp format |
| 50113 | Timestamp expired | Sync system clock, ensure < 30s drift from OKX server time |
| 50119 | Invalid passphrase | Verify PASSPHRASE matches exactly — case-sensitive |
| 51001 | Instrument not found | Check instId format: 'BTC-USDT' not 'BTCUSDT' |
| 51008 | Insufficient balance | Check account balance and margin mode |
| 429 | Rate limit exceeded | Add exponential backoff — OKX limits by endpoint |
OKX rate limits vary by endpoint: market data endpoints allow up to 40 requests/2 seconds per IP, while trading endpoints are stricter. Compare this to Bitget (20 req/2s for trading) or KuCoin (which uses a token bucket system). Build a simple rate limiter or use a library like ratelimit if you're calling OKX heavily. For production signal execution — especially if you're acting on signals from a platform like VoiceOfChain — a small sleep between order calls prevents limit errors from cascading.
VoiceOfChain provides real-time on-chain and order-flow signals that traders route to execution bots exactly like this. Having reliable API auth code means your bot can act on a whale accumulation signal within milliseconds of it firing — the authentication overhead in Python is under 1ms once keys are in memory.
OKX V5 API authentication in Python comes down to three moving parts: the three credentials (key, secret, passphrase), the HMAC-SHA256 signing of timestamp + method + path + body, and getting the timestamp fresh on every request. The auth module built here is production-ready — drop it into any project, load credentials from environment variables, and you're making authenticated calls within minutes. From there, whether you're pulling balance data, executing orders based on signals from VoiceOfChain, or building a full algo trading system, the authentication layer stays exactly the same. Get it solid once, and everything built on top of it is reliable.