◈   ⌘ api · Intermediate

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.

Uncle Solieditor · voc · 06.05.2026 ·views 9
◈   Contents
  1. → How OKX API Authentication Works
  2. → Generating the HMAC Signature in Python
  3. → Placing a Signed Order via POST Request
  4. → Error Handling and Common Signature Failures
  5. → Using Signed API Calls with Trading Signals
  6. → Frequently Asked Questions
  7. → Wrapping Up

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.

How OKX API Authentication Works

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.

Generating the HMAC Signature in Python

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.

Placing a Signed Order via POST Request

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.

Error Handling and Common Signature Failures

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.

Common OKX API signature errors and fixes
Error CodeMessageMost Likely CauseFix
50111Invalid OK-ACCESS-KEYWrong API key or key not activatedCheck dashboard, wait 60s after creation
50112Invalid OK-ACCESS-TIMESTAMPClock skew > 30 secondsSync system clock or fetch server time first
50113Invalid OK-ACCESS-SIGNWrong prehash constructionVerify concatenation order: ts+method+path+body
50114Invalid OK-ACCESS-PASSPHRASEWrong passphrasePassphrase is case-sensitive, check for spaces
50119API key doesn't match environmentUsing mainnet key on demo or vice versaMatch 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.

Using Signed API Calls with Trading Signals

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.

Frequently Asked Questions

Why does my OKX signature keep returning error 50113 even though the code looks correct?
The most common cause is a mismatch between the body string used during signing and the body actually transmitted. Make sure you serialize to JSON exactly once, store it as a variable, and use that same variable for both the signature and the request payload. Also double-check that the method string is uppercase (GET, POST) and that your path includes the query string for GET requests with parameters.
Does the OKX HMAC signature expire?
The signature itself doesn't expire, but the timestamp embedded in it does — OKX rejects requests where the timestamp is more than 30 seconds old. This means you must generate a fresh signature for every request. You cannot cache or reuse signatures.
What permissions do I need to enable on my OKX API key for trading?
For reading account data you need the Read permission. For placing and canceling orders you need Trade permission. For withdrawals you need Withdraw permission — only enable this if your bot actually needs it, since it significantly increases risk if the key is ever compromised. IP whitelisting is strongly recommended for any key with Trade permissions.
How is OKX API authentication different from Binance?
Binance appends the signature as a query parameter (signature=...) and uses the request body or query string as the message to sign. OKX uses HTTP headers for all auth fields and constructs the prehash as timestamp+method+path+body. The HMAC-SHA256 algorithm is the same, but the conventions differ enough that you can't swap implementations directly.
Can I use the same API key for both spot and futures trading on OKX?
Yes, a single OKX API key covers all product types — spot, futures, options, and perpetual swaps. The difference is the tdMode parameter in your order requests: use 'cash' for spot, 'cross' or 'isolated' for margin and derivatives. Make sure your key has the appropriate trade permissions enabled in the OKX dashboard.
Is it safe to run an OKX trading bot on a VPS?
It's common practice and generally safe if done correctly. Use environment variables for credentials, never hardcode secrets, enable IP whitelisting on your API key restricting access to only your VPS IP, and disable withdrawal permissions unless essential. Treat your secret key like a private key — if it leaks, rotate it immediately from the OKX dashboard.

Wrapping Up

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.

◈   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