OKX API Signing in Python: Complete Setup Guide
Learn how to authenticate with the OKX API using Python, including HMAC signature generation, request headers, and live trading examples with error handling.
Learn how to authenticate with the OKX API using Python, including HMAC signature generation, request headers, and live trading examples with error handling.
The OKX API is one of the most powerful in crypto — it gives you access to spot, futures, options, and copy trading programmatically. But before you can place a single order, you need to get authentication right. The signing process trips up a lot of traders new to algorithmic trading, because even a one-character mistake in the signature construction results in a silent 401 rejection. This guide walks you through exactly how OKX request signing works in Python, from key setup to parsing live responses.
OKX uses HMAC-SHA256 signing for all private API endpoints. Unlike Coinbase's older APIs or Binance's straightforward query string approach, OKX requires you to sign a specific pre-hash string built from the timestamp, HTTP method, request path, and request body. Get any of these out of order and the request fails. You'll need three credentials from the OKX API management panel: API Key, Secret Key, and Passphrase. The passphrase is OKX-specific — Binance and Bybit don't require it, so it catches people off guard.
Always create a separate API key for each bot or strategy. If one key is compromised, you can revoke it without disrupting your other automations. Enable IP whitelisting on OKX for any key that has withdrawal permissions.
You don't need any OKX-specific SDK to get started — the standard library handles most of it. The only external dependency worth adding is `requests` for HTTP and optionally `python-dotenv` to keep credentials out of your source code. Store your API keys in a `.env` file and add it to `.gitignore` immediately. One pushed credential to GitHub and you'll find bots draining your account within minutes — this has happened to traders on every exchange including OKX, Bybit, and Gate.io.
pip install requests python-dotenv
# .env file (never commit this)
OKX_API_KEY=your_api_key_here
OKX_SECRET_KEY=your_secret_key_here
OKX_PASSPHRASE=your_passphrase_here
OKX_BASE_URL=https://www.okx.com
The OKX signing algorithm requires you to concatenate four components in a specific order: ISO 8601 timestamp, HTTP method in uppercase, the request path (including query string), and the request body as a JSON string. This concatenated string is then signed using HMAC-SHA256 with your Secret Key, and the result is base64-encoded. The timestamp must be within 30 seconds of OKX's server time — use UTC and sync your system clock if you're seeing timestamp errors.
import hmac
import hashlib
import base64
import datetime
import json
import os
import requests
from dotenv import load_dotenv
load_dotenv()
API_KEY = os.getenv('OKX_API_KEY')
SECRET_KEY = os.getenv('OKX_SECRET_KEY')
PASSPHRASE = os.getenv('OKX_PASSPHRASE')
BASE_URL = os.getenv('OKX_BASE_URL', 'https://www.okx.com')
def get_timestamp() -> str:
"""Return current UTC time in ISO 8601 format required by OKX."""
return datetime.datetime.utcnow().strftime('%Y-%m-%dT%H:%M:%S.') + \
str(datetime.datetime.utcnow().microsecond // 1000).zfill(3) + 'Z'
def sign_request(timestamp: str, method: str, path: str, body: str = '') -> str:
"""Generate HMAC-SHA256 signature for OKX API request."""
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('utf-8')
def get_headers(method: str, path: str, body: str = '') -> dict:
"""Build the full set of authenticated headers OKX requires."""
timestamp = get_timestamp()
signature = sign_request(timestamp, method, path, body)
return {
'OK-ACCESS-KEY': API_KEY,
'OK-ACCESS-SIGN': signature,
'OK-ACCESS-TIMESTAMP': timestamp,
'OK-ACCESS-PASSPHRASE': PASSPHRASE,
'Content-Type': 'application/json',
}
With the signing function in place, authenticated GET and POST requests follow the same pattern. For GET requests the body is an empty string. For POST requests you serialize your payload as a compact JSON string (no extra spaces) and include it both as the body parameter in signing and as the actual request body. The path used in signing must match exactly what you're calling, including any query parameters appended for GET requests.
def get_account_balance(currency: str = 'USDT') -> dict:
"""Fetch account balance for a specific currency."""
path = f'/api/v5/account/balance?ccy={currency}'
headers = get_headers('GET', path)
try:
response = requests.get(BASE_URL + path, headers=headers, timeout=10)
response.raise_for_status()
data = response.json()
if data.get('code') != '0':
raise ValueError(f"OKX API error {data['code']}: {data.get('msg', 'Unknown error')}")
return data['data']
except requests.exceptions.Timeout:
print('Request timed out — OKX may be experiencing delays')
return {}
except requests.exceptions.RequestException as e:
print(f'HTTP error: {e}')
return {}
def place_limit_order(inst_id: str, side: str, price: str, size: str) -> dict:
"""Place a limit order on OKX spot market."""
path = '/api/v5/trade/order'
payload = {
'instId': inst_id,
'tdMode': 'cash',
'side': side,
'ordType': 'limit',
'px': price,
'sz': size,
}
body = json.dumps(payload, separators=(',', ':'))
headers = get_headers('POST', path, body)
try:
response = requests.post(
BASE_URL + path,
headers=headers,
data=body,
timeout=10
)
response.raise_for_status()
data = response.json()
if data.get('code') != '0':
raise ValueError(f"Order failed {data['code']}: {data.get('msg')}")
order_id = data['data'][0]['ordId']
print(f'Order placed successfully: {order_id}')
return data['data'][0]
except ValueError as e:
print(f'API rejection: {e}')
return {}
except requests.exceptions.RequestException as e:
print(f'Network error placing order: {e}')
return {}
# Example usage
if __name__ == '__main__':
balance = get_account_balance('USDT')
print(f'USDT Balance: {balance}')
# Place a small test limit order (BTC-USDT)
order = place_limit_order(
inst_id='BTC-USDT',
side='buy',
price='60000',
size='0.001'
)
print(f'Order result: {order}')
Public endpoints — ticker data, order book, candlesticks — don't require authentication. This is useful for backtesting or pulling market data without exposing credentials. OKX's public API is fast and reliable, comparable to what Binance and KuCoin offer. Pair the OKX market data feed with a signal platform like VoiceOfChain to get context on order flow before executing, so you're not trading blind on raw price alone.
def get_ticker(inst_id: str) -> dict:
"""Fetch current ticker for an instrument — no auth required."""
path = f'/api/v5/market/ticker?instId={inst_id}'
try:
response = requests.get(BASE_URL + path, timeout=10)
response.raise_for_status()
data = response.json()
if data['code'] != '0' or not data['data']:
return {}
ticker = data['data'][0]
return {
'symbol': ticker['instId'],
'last_price': float(ticker['last']),
'bid': float(ticker['bidPx']),
'ask': float(ticker['askPx']),
'volume_24h': float(ticker['vol24h']),
'change_24h': float(ticker['open24h']),
}
except (requests.exceptions.RequestException, KeyError, ValueError) as e:
print(f'Error fetching ticker: {e}')
return {}
def get_open_orders(inst_id: str = None) -> list:
"""Retrieve all open orders, optionally filtered by instrument."""
path = '/api/v5/trade/orders-pending'
if inst_id:
path += f'?instType=SPOT&instId={inst_id}'
else:
path += '?instType=SPOT'
headers = get_headers('GET', path)
response = requests.get(BASE_URL + path, headers=headers, timeout=10)
data = response.json()
if data['code'] != '0':
return []
return [{
'order_id': o['ordId'],
'symbol': o['instId'],
'side': o['side'],
'price': float(o['px']),
'size': float(o['sz']),
'filled': float(o['fillSz']),
'status': o['state'],
} for o in data['data']]
# Quick test
ticker = get_ticker('ETH-USDT')
print(f"ETH last price: ${ticker.get('last_price', 'N/A')}")
print(f"24h volume: {ticker.get('volume_24h', 'N/A')} ETH")
Most OKX API authentication failures come down to a small set of repeatable mistakes. The error codes OKX returns are specific enough to diagnose quickly if you know what to look for. Unlike Bitget which sometimes returns vague 400 errors, OKX error messages are relatively descriptive. Here are the ones you'll hit most often and what actually causes them.
| Error Code | Meaning | Fix |
|---|---|---|
| 50111 | Invalid signature | Check message string order: timestamp+method+path+body |
| 50113 | Timestamp expired | Ensure system clock is UTC and within 30s of OKX server time |
| 50119 | Invalid passphrase | Passphrase is case-sensitive — copy exactly as set in OKX panel |
| 50102 | IP not in whitelist | Add your server IP to the API key whitelist in OKX settings |
| 51000 | Parameter error | Verify instId format (e.g. BTC-USDT not BTC/USDT) |
| 58350 | Insufficient balance | Check available margin vs order size including fees |
When debugging signature failures, print the raw pre-hash message string before signing. Compare it character by character against what OKX expects. The most common culprit is extra whitespace in the JSON body — always use json.dumps with separators=(',', ':') for compact output.
Once you have signing working reliably, the OKX API opens up a lot: automated DCA strategies, grid bots, stop-loss managers that aren't dependent on OKX's native tools, and integration with external signal sources. Platforms like VoiceOfChain provide real-time order flow signals — large buy walls forming, whale accumulation patterns, unusual volume spikes — that you can feed directly into your Python bot to trigger entries and exits with context behind them rather than just reacting to price. That combination of programmatic execution via OKX API and intelligent signal filtering is where algorithmic trading on crypto actually starts to have an edge.
The signing code here is intentionally minimal and dependency-free. In production you'd want to wrap it in a class, add request retry logic with circuit breaking, and log all API interactions to a local file for post-trade analysis. But the core authentication pattern stays exactly the same whether you're running a simple rebalancing script or a multi-exchange strategy spanning OKX, Bybit, and Binance simultaneously.