OKX API HMAC Signature: Complete Setup Guide
Learn how to generate OKX API HMAC signatures correctly, authenticate requests, and start trading programmatically with working Python code examples.
Learn how to generate OKX API HMAC signatures correctly, authenticate requests, and start trading programmatically with working Python code examples.
If you've ever stared at a 401 Unauthorized response from the OKX API wondering what went wrong with your signature, you're not alone. HMAC authentication is the single most common stumbling block for traders getting started with programmatic trading on OKX. Get it right once, and the whole ecosystem opens up — order placement, position management, account data, everything. Get it wrong, and you're debugging hex strings at 2am.
OKX uses HMAC-SHA256 signatures to verify that API requests actually come from you and haven't been tampered with in transit. Unlike Binance which uses a simpler query-string approach, or Coinbase Advanced Trade which leans on JWT tokens, OKX has its own specific signing convention that catches a lot of developers off guard the first time. This guide walks through exactly how it works, why each piece matters, and gives you working code you can drop straight into a trading bot.
Every authenticated request to OKX requires four HTTP headers: OK-ACCESS-KEY (your API key), OK-ACCESS-SIGN (the HMAC signature), OK-ACCESS-TIMESTAMP (ISO 8601 UTC timestamp), and OK-ACCESS-PASSPHRASE (the passphrase you set when creating the key). The signature is what proves ownership — it's a hash of the timestamp, HTTP method, request path, and body, all signed with your secret key.
The core signing formula is: HMAC-SHA256(secret_key, timestamp + method + request_path + body), then Base64-encoded. The timestamp must be within 30 seconds of OKX's server time, which is a common gotcha — if your machine clock drifts, every request fails with a timestamp error.
The Python implementation is clean once you understand the prehash string construction. The most common mistake is getting the concatenation order wrong — timestamp comes first, then method (uppercase), then the full request path including query string, then the request body (empty string for GET requests).
import hmac
import hashlib
import base64
import datetime
import requests
import json
API_KEY = 'your-api-key'
SECRET_KEY = 'your-secret-key'
PASSPHRASE = 'your-passphrase'
BASE_URL = 'https://www.okx.com'
def get_timestamp():
"""Returns current UTC timestamp in OKX format."""
now = datetime.datetime.utcnow()
return now.strftime('%Y-%m-%dT%H:%M:%S.') + f'{now.microsecond // 1000:03d}Z'
def sign(secret: str, timestamp: str, method: str, path: str, body: str = '') -> str:
"""Generate HMAC-SHA256 signature for OKX API."""
prehash = timestamp + method.upper() + path + body
mac = hmac.new(
secret.encode('utf-8'),
prehash.encode('utf-8'),
hashlib.sha256
)
return base64.b64encode(mac.digest()).decode('utf-8')
def get_headers(method: str, path: str, body: str = '') -> dict:
"""Build authenticated headers for an OKX API request."""
ts = get_timestamp()
return {
'OK-ACCESS-KEY': API_KEY,
'OK-ACCESS-SIGN': sign(SECRET_KEY, ts, method, path, body),
'OK-ACCESS-TIMESTAMP': ts,
'OK-ACCESS-PASSPHRASE': PASSPHRASE,
'Content-Type': 'application/json'
}
# Example: Fetch account balance
path = '/api/v5/account/balance'
headers = get_headers('GET', path)
response = requests.get(BASE_URL + path, headers=headers)
print(response.json())
Never hardcode credentials in source files. Use environment variables or a .env file (with python-dotenv) and add your secrets file to .gitignore before your first commit. One accidental push to a public repo and you'll be rotating keys while watching your balance drain.
POST requests require the body to be included in the signature. This is where many implementations break — developers sign with an empty body but send JSON, or serialize the body differently between signing and sending. The body string you sign must be byte-for-byte identical to what you transmit.
def place_order(inst_id: str, side: str, sz: str, px: str = None, ord_type: str = 'market') -> dict:
"""
Place an order on OKX.
inst_id: e.g. 'BTC-USDT'
side: 'buy' or 'sell'
sz: order size (base currency)
px: price (required for limit orders)
ord_type: 'market' or 'limit'
"""
path = '/api/v5/trade/order'
payload = {
'instId': inst_id,
'tdMode': 'cash', # 'cash' for spot, 'cross' or 'isolated' for margin
'side': side,
'ordType': ord_type,
'sz': sz
}
if px:
payload['px'] = px
body = json.dumps(payload) # Must use this exact string for signing
headers = get_headers('POST', path, body)
response = requests.post(
BASE_URL + path,
headers=headers,
data=body # Send the same string, not re-serialized
)
result = response.json()
if result.get('code') != '0':
raise Exception(f"OKX order failed: {result.get('msg')} (code {result.get('code')})")
return result['data'][0]
# Place a market buy of 0.001 BTC
try:
order = place_order('BTC-USDT', 'buy', '0.001')
print(f"Order placed: {order['ordId']}")
except Exception as e:
print(f"Error: {e}")
Notice that body is serialized once and reused for both signing and the request payload. If you call json.dumps() again inside the headers function, you risk key ordering differences depending on your Python version and dict state, which would produce a different signature than what you send.
OKX returns standardized error codes in the response body, not always as HTTP error status codes. A 200 response can still contain a failed operation — you have to inspect the code field. The most common signature-related errors are worth knowing by number.
| Error Code | Message | Most Likely Cause | Fix |
|---|---|---|---|
| 50111 | Invalid OK-ACCESS-KEY | Wrong API key or key not activated | Check dashboard, wait 60s after creation |
| 50112 | Invalid OK-ACCESS-TIMESTAMP | Clock skew > 30 seconds | Sync system clock or fetch server time first |
| 50113 | Invalid OK-ACCESS-SIGN | Wrong prehash construction | Verify concatenation order: ts+method+path+body |
| 50114 | Invalid OK-ACCESS-PASSPHRASE | Wrong passphrase | Passphrase is case-sensitive, check for spaces |
| 50119 | API key doesn't match environment | Using mainnet key on demo or vice versa | Match BASE_URL to your key's environment |
import time
def get_server_time() -> str:
"""Fetch OKX server time to avoid clock-skew errors."""
response = requests.get(f'{BASE_URL}/api/v5/public/time')
data = response.json()
if data['code'] == '0':
# OKX returns epoch milliseconds as string
epoch_ms = int(data['data'][0]['ts'])
dt = datetime.datetime.utcfromtimestamp(epoch_ms / 1000)
return dt.strftime('%Y-%m-%dT%H:%M:%S.') + f'{dt.microsecond // 1000:03d}Z'
raise Exception('Failed to fetch server time')
def robust_get_headers(method: str, path: str, body: str = '') -> dict:
"""Headers using server time — eliminates clock-skew 50112 errors."""
ts = get_server_time() # Use server time instead of local
return {
'OK-ACCESS-KEY': API_KEY,
'OK-ACCESS-SIGN': sign(SECRET_KEY, ts, method, path, body),
'OK-ACCESS-TIMESTAMP': ts,
'OK-ACCESS-PASSPHRASE': PASSPHRASE,
'Content-Type': 'application/json'
}
Always test new signature logic against the OKX demo environment (https://www.okx.com/api/v5/ with demo API keys) before going live. Platforms like Bybit and OKX both offer paper trading APIs specifically so you can validate your auth code without touching real funds.
Once your signature layer is solid, the practical next step is wiring it to a signal source. A common pattern is listening to real-time trading signals — from a platform like VoiceOfChain, which aggregates on-chain data and market signals — then executing orders automatically via the OKX API when a signal fires.
The architecture is straightforward: signal webhook hits your server, your server validates the signal, calls place_order() with the appropriate parameters, and logs the result. Compared to manually watching charts on Binance or Gate.io and clicking buttons, this latency difference is significant — especially in volatile markets where a 10-second delay can be the difference between a good fill and chasing price.
from flask import Flask, request, jsonify
app = Flask(__name__)
SIGNAL_SECRET = 'your-webhook-secret' # Shared secret with your signal provider
@app.route('/webhook/signal', methods=['POST'])
def handle_signal():
# Validate the webhook came from your signal source
auth = request.headers.get('X-Signal-Secret', '')
if auth != SIGNAL_SECRET:
return jsonify({'error': 'unauthorized'}), 401
data = request.json
action = data.get('action') # 'buy' or 'sell'
symbol = data.get('symbol') # e.g. 'BTC-USDT'
size = data.get('size', '0.001')
if action not in ('buy', 'sell'):
return jsonify({'error': 'invalid action'}), 400
try:
order = place_order(symbol, action, size)
return jsonify({
'status': 'ok',
'orderId': order['ordId'],
'symbol': symbol,
'side': action
})
except Exception as e:
# Log to your monitoring system
print(f'Order failed for signal {data}: {e}')
return jsonify({'error': str(e)}), 500
if __name__ == '__main__':
app.run(port=5000)
This webhook pattern works with any signal source that can POST JSON. VoiceOfChain, for example, provides real-time alerts with asset, direction, and confidence level — you map those fields to your OKX order parameters and you have a functional signal-to-trade pipeline. Similar setups work equally well with Bitget and KuCoin APIs, which use comparable HMAC authentication schemes with minor variations in header names.
OKX's HMAC signature system is more precise than it is complex — once you internalize the prehash format and the four required headers, authenticated API calls become routine. The signature function itself is about 10 lines of code. The rest is just building reliable request handling around it: consistent body serialization, server-time synchronization to avoid clock skew, and structured error handling for the OKX error codes.
From there, the platform opens up. Combine signed OKX API calls with a real-time signal source like VoiceOfChain and you have the foundation of a genuine algorithmic trading system — one that reacts in milliseconds instead of the seconds or minutes it takes to manually execute on any exchange. Whether you're building a simple signal executor or a full portfolio management bot, getting authentication solid is the foundation everything else rests on.