OKX API Authentication Headers: Complete Setup Guide
Master OKX API authentication headers with step-by-step code examples. Learn to sign requests, handle errors, and build reliable trading bots.
Master OKX API authentication headers with step-by-step code examples. Learn to sign requests, handle errors, and build reliable trading bots.
If you've ever tried to hit the OKX API and got back a 401 or a cryptic 'Invalid Sign' error, you already know that authentication is where most developers get stuck. OKX uses a four-header authentication scheme that's stricter than what you'll find on Binance or Coinbase — every request needs a precise HMAC-SHA256 signature built from the timestamp, method, path, and body. Get one character wrong and the whole thing fails silently. This guide walks through exactly how to construct those headers, sign requests correctly, and avoid the common traps that waste hours of debugging time.
OKX requires four specific headers on every authenticated request. These are not optional — missing any one of them will result in an authentication failure regardless of how correct the others are.
| Header | Value | Example |
|---|---|---|
| OK-ACCESS-KEY | Your API key | abc123def456... |
| OK-ACCESS-SIGN | HMAC-SHA256 signature (base64) | computed per request |
| OK-ACCESS-TIMESTAMP | ISO 8601 UTC timestamp | 2024-01-15T10:30:00.000Z |
| OK-ACCESS-PASSPHRASE | Passphrase set during key creation | mySecurePassphrase |
Unlike Bybit, which accepts Unix epoch integers for timestamps, OKX wants ISO 8601 format with milliseconds. And unlike KuCoin's passphrase which is hashed before sending, OKX sends your passphrase raw — which means it needs to be stored securely in environment variables, never hardcoded.
Never hardcode API credentials in your source code. Use environment variables or a secrets manager. OKX keys with withdrawal permissions are especially sensitive — a leaked key can drain a wallet within seconds.
The signature is the most error-prone part of OKX authentication. The formula is straightforward but the details matter: you concatenate the timestamp, HTTP method (uppercase), request path including query string, and the request body, then sign that string with your secret key using HMAC-SHA256, and base64-encode the result.
The prehash string follows this exact format: `{timestamp}{method}{requestPath}{body}`. For GET requests with no body, the body portion is an empty string — not null, not a space, just nothing. For POST requests, the body is the raw JSON string exactly as it will be sent. Any difference between the body used for signing and the body actually sent will cause signature verification to fail.
import hmac
import hashlib
import base64
import datetime
import os
def get_timestamp():
"""Returns ISO 8601 timestamp with milliseconds in UTC."""
return datetime.datetime.utcnow().strftime('%Y-%m-%dT%H:%M:%S.') + \
str(datetime.datetime.utcnow().microsecond // 1000).zfill(3) + 'Z'
def sign_request(secret_key: str, timestamp: str, method: str,
request_path: str, body: str = '') -> str:
"""
Build HMAC-SHA256 signature for OKX API request.
Args:
secret_key: Your OKX API secret key
timestamp: ISO 8601 UTC timestamp (from get_timestamp())
method: HTTP method uppercase ('GET', 'POST', 'DELETE')
request_path: Full path including query params e.g. '/api/v5/account/balance'
body: Raw JSON string for POST requests, empty string for GET
Returns:
base64-encoded HMAC-SHA256 signature
"""
prehash = timestamp + method.upper() + request_path + body
mac = hmac.new(
secret_key.encode('utf-8'),
prehash.encode('utf-8'),
hashlib.sha256
)
return base64.b64encode(mac.digest()).decode('utf-8')
# Example usage
API_KEY = os.environ.get('OKX_API_KEY')
SECRET_KEY = os.environ.get('OKX_SECRET_KEY')
PASSPHRASE = os.environ.get('OKX_PASSPHRASE')
timestamp = get_timestamp()
method = 'GET'
path = '/api/v5/account/balance'
signature = sign_request(SECRET_KEY, timestamp, method, path)
headers = {
'OK-ACCESS-KEY': API_KEY,
'OK-ACCESS-SIGN': signature,
'OK-ACCESS-TIMESTAMP': timestamp,
'OK-ACCESS-PASSPHRASE': PASSPHRASE,
'Content-Type': 'application/json'
}
print('Headers ready:', list(headers.keys()))
With the signature function in place, making actual API calls is straightforward. The key difference between GET and POST is how query parameters are handled in the signature — for GET requests they must be included in the path string, not as a separate dict passed to requests.
import requests
import json
BASE_URL = 'https://www.okx.com'
def okx_get(path: str, params: dict = None) -> dict:
"""
Authenticated GET request to OKX API.
Query params must be appended to path for correct signature.
"""
if params:
query_string = '?' + '&'.join(f'{k}={v}' for k, v in params.items())
signed_path = path + query_string
else:
signed_path = path
ts = get_timestamp()
sig = sign_request(SECRET_KEY, ts, 'GET', signed_path)
headers = {
'OK-ACCESS-KEY': API_KEY,
'OK-ACCESS-SIGN': sig,
'OK-ACCESS-TIMESTAMP': ts,
'OK-ACCESS-PASSPHRASE': PASSPHRASE,
'Content-Type': 'application/json'
}
resp = requests.get(BASE_URL + signed_path, headers=headers, timeout=10)
resp.raise_for_status()
return resp.json()
def okx_post(path: str, payload: dict) -> dict:
"""
Authenticated POST request to OKX API.
Body must be serialized to string before signing.
"""
body = json.dumps(payload) # must use exact same string for signing and sending
ts = get_timestamp()
sig = sign_request(SECRET_KEY, ts, 'POST', path, body)
headers = {
'OK-ACCESS-KEY': API_KEY,
'OK-ACCESS-SIGN': sig,
'OK-ACCESS-TIMESTAMP': ts,
'OK-ACCESS-PASSPHRASE': PASSPHRASE,
'Content-Type': 'application/json'
}
resp = requests.post(BASE_URL + path, headers=headers, data=body, timeout=10)
resp.raise_for_status()
return resp.json()
# Fetch account balance
try:
balance = okx_get('/api/v5/account/balance')
print('Balance data:', json.dumps(balance['data'][0]['details'][:2], indent=2))
except requests.exceptions.HTTPError as e:
print(f'HTTP error: {e.response.status_code} — {e.response.text}')
except Exception as e:
print(f'Request failed: {e}')
# Place a limit order
order_payload = {
'instId': 'BTC-USDT',
'tdMode': 'cash',
'side': 'buy',
'ordType': 'limit',
'px': '40000',
'sz': '0.001'
}
try:
result = okx_post('/api/v5/trade/order', order_payload)
print('Order result:', result)
except Exception as e:
print(f'Order failed: {e}')
Notice that `data=body` is used instead of `json=payload` in the POST call. This is intentional — using `json=` would let requests re-serialize the dict, potentially changing key ordering or whitespace and breaking the signature. Always sign the exact bytes you're sending.
OKX returns error codes in the response body even on 200 status responses. A successful request returns `'code': '0'`, while errors return specific codes that tell you exactly what went wrong. This is different from how Binance handles errors — where HTTP status codes carry more weight.
import ntplib
from time import ctime
OKX_ERROR_MESSAGES = {
'50111': 'Invalid API key — check OK-ACCESS-KEY value',
'50112': 'Timestamp invalid — sync system clock (skew > 30s)',
'50113': 'Signature invalid — check body serialization and path format',
'50114': 'Wrong passphrase for this API key',
'50102': 'Request expired — regenerate timestamp closer to sending',
}
def handle_okx_response(response: dict) -> dict:
"""
Parse OKX response and raise descriptive errors on failure.
OKX returns code '0' on success, other codes on error.
"""
code = response.get('code', '0')
if code != '0':
msg = response.get('msg', 'Unknown error')
friendly = OKX_ERROR_MESSAGES.get(code, msg)
raise ValueError(f'OKX API Error {code}: {friendly}')
return response.get('data', [])
def check_clock_sync() -> float:
"""Returns clock offset in seconds vs NTP. OKX allows max 30s skew."""
try:
c = ntplib.NTPClient()
response = c.request('pool.ntp.org', version=3)
offset = response.offset
if abs(offset) > 25:
print(f'WARNING: Clock offset {offset:.1f}s — approaching OKX 30s limit')
return offset
except Exception:
print('Could not check NTP — ensure system time is synced')
return 0.0
# Usage in your trading loop
try:
raw = okx_get('/api/v5/market/ticker', {'instId': 'ETH-USDT'})
data = handle_okx_response(raw)
ticker = data[0]
print(f"ETH/USDT last price: {ticker['last']} | 24h vol: {ticker['vol24h']}")
except ValueError as e:
print(f'API logic error: {e}')
except requests.exceptions.Timeout:
print('Request timed out — OKX may be experiencing issues')
except requests.exceptions.ConnectionError:
print('Network error — check connectivity')
The most common cause of error 50113 (invalid signature) on POST requests is using json= instead of data= in requests.post(), or building the body dict differently than what was signed. Always sign the exact serialized string you send.
Raw API access becomes far more powerful when combined with real-time signal data. Platforms like VoiceOfChain provide live trading signals across major pairs — BTC, ETH, SOL, and others — that you can consume programmatically and act on via exchange APIs. The typical pattern is: receive signal from VoiceOfChain → validate against current OKX order book via API → execute if conditions match → manage position via authenticated endpoints.
Compared to Binance's API, OKX has more granular order types available at the REST level — including algo orders like iceberg and TWAP directly through the `/api/v5/trade/order-algo` endpoint. Bybit has similar depth, but OKX's unified account model means one authenticated session covers spot, margin, futures, and options simultaneously. For traders running multi-strategy bots, this reduces the complexity of managing separate credential sets per product type.
When building a signal-to-execution pipeline, structure your authentication layer as a thin wrapper that all strategy modules share. This way credential rotation — which should happen every 90 days — only requires updating one place. Gate.io and Bitget follow similar four-header patterns, so the wrapper architecture translates across exchanges with minimal changes to the signing logic.
OKX API authentication comes down to four headers, one HMAC-SHA256 signature, and careful attention to how you serialize request bodies. The authentication pattern is consistent across every OKX endpoint — once it works for account balance, it works for order placement, position management, and market data. The code examples here give you a production-ready foundation: timestamp generation, signature building, response error handling, and clock sync checks. From here, the logical next step is wrapping these helpers into a lightweight OKX client class and connecting it to a signal feed — whether that's from VoiceOfChain or your own technical analysis engine — to close the loop from signal to execution automatically.