OKX · API Hands-On
OKX API Errors and Troubleshooting Checklist: Fix It With This Table
When you write quant scripts, you may get stuck not on the strategy but on a single line of red error text: the script starts up, the terminal spits out a string of incomprehensible codes and English, and you freeze. The good news is that OKX API errors come in just a handful of high-frequency flavors, and once you know what each looks like and what cause it maps to, troubleshooting gets a lot faster.
This piece collects the common OKX API errors into one checklist. The heart of it is a single error-code reference table: when something breaks, find your error by code or keyword in the table, then read the matching item-by-item fix below. By the end you should be able to resolve most errors yourself in minutes, instead of staring blankly at red text.
Learn to read the error first: code + msg
Step one in debugging is reading the error itself. When an OKX endpoint errors out, it returns three pieces of information:
- HTTP status code: the coarsest layer, like 401 (auth problem), 429 (too many requests), 400 (the request itself is wrong).
- code: OKX's own numeric error code, pointing most precisely at the type of problem. This is your main basis for troubleshooting.
- msg: a text description that helps confirm the direction, sometimes pointing out exactly which field is wrong.
If you use ccxt, it wraps these raw errors into more readable exception types—an auth problem throws AuthenticationError, a rate limit throws RateLimitExceeded, insufficient balance throws InsufficientFunds. The exception message generally carries OKX's original code and msg too. A good habit is to print them clearly with try/except:
import ccxt
okx = ccxt.okx({
'apiKey': 'YOUR_apiKey',
'secret': 'YOUR_secret',
'password': 'YOUR_Passphrase',
'enableRateLimit': True,
})
try:
balance = okx.fetch_balance()
print('USDT free:', balance['USDT']['free'])
except ccxt.InvalidNonce as e:
print('Timestamp expired (50102), resync the clock and retry:', e)
except ccxt.AuthenticationError as e:
print('Auth failed, read the code in the error before deciding what to check:', e)
except ccxt.RateLimitExceeded as e:
print('Rate limited, slow down requests:', e)
except ccxt.BaseError as e:
print('Other error, look at the raw code/msg:', e)
Note down that code and msg from the exception as-is, cross-check the table below, and you can basically pin down which class of problem it is.
High-frequency error reference table
The table below covers the errors beginners easily hit. The error-code meanings in the table were checked in 2026-10 against the Error Code section of OKX's API v5 docs (OKX occasionally adjusts code values, so if one doesn't match, defer to the msg text and the official docs); the point is to line up "cause → fix." The auth class especially has to be worked by code: both look like "can't connect," but 50113 and 50105 send you looking at completely different things.
| Error (code / keyword) | Likely cause | Fix |
|---|---|---|
| 50011 Too Many Requests / rate limit |
Requests too dense, tripping rate-limit protection | Turn on enableRateLimit; if still hitting it, add exponential backoff retry—no delay-free while loops |
| 50113 Invalid signature |
The signature itself failed validation — the problem is at the signing step, not necessarily a mistyped credential | Check how the signature payload is assembled (request method, request path, request body), whether the HMAC key really is the secret, and whether you ran HMAC-SHA256 then Base64; with ccxt you don't sign by hand, so suspect a miscopied secret first |
| 50111 Invalid OK-ACCESS-KEY |
The apiKey itself is invalid: miscopied (stray spaces, dropped characters) or deleted | Recopy the apiKey from the dashboard; confirm this Key still exists |
| 50101 APIKey does not match current environment |
Key and environment don't line up: a demo Key without set_sandbox_mode(True), or a live Key with it on |
Match the Key to the switch: demo Key with set_sandbox_mode(True), live Key without it; self-check with okx.headers.get('x-simulated-trading'), which should print 1 on demo |
| 50105 OK-ACCESS-PASSPHRASE incorrect (50104 = this field is empty) |
The passphrase doesn't match: ccxt's password field holds the login password, not the Passphrase set at Key creation |
Change password to the Passphrase you set when creating that Key; if you can't recall it, delete and recreate the Key |
| Insufficient balance | Order amount exceeds available balance, or funds are in another account (e.g. funding vs trading) | Reduce the order size; confirm funds are in the trading account; on the demo, reset the virtual funds |
| 50102 Timestamp request expired 50112 Invalid OK-ACCESS-TIMESTAMP |
The timestamp in the request doesn't line up with server time: per the official docs, more than 30 seconds apart and it's rejected outright (50112 means the field's own format is wrong) | Turn on NTP auto-sync to bring system time in sync; cloud servers usually have it on by default |
| 50120 This API key doesn't have permission for this function |
This Key lacks the needed permission (commonly: only Read ticked, not Trade); it comes back as HTTP 200, not as a 401 like the other auth errors, so you only see it by reading the code in the body | Go to API management and add the "Trade" permission to this Key—no need to recreate |
| 50110 Your IP is not included in your API key's IP whitelist |
An IP whitelist is set on the Key, but the current outbound IP isn't on it (network change/proxy/broadband IP change) | Find out what your current outbound IP actually is, then add it to this Key's whitelist (up to 20 IPs per Key, CIDR ranges supported); for an unstable outbound IP see the section below — don't make "remove the whitelist" your routine fix |
| instrument does not exist | Wrong symbol format, or a futures format used for a spot order | Write spot as BTC/USDT (uppercase with slash); run load_markets() first to see available pairs |
| size too small | Order amount below the pair's minimum order size, or wrong precision | Use load_markets() to check the pair's limits and precision, and round per its requirements |
How to fix each one
The table gives direction; this section gives detail. Read down from whichever one you hit.
Auth class (50101 / 50102 / 50105 / 50110 / 50111 / 50112 / 50113) and permission class (50120)
At the HTTP layer every auth failure is a 401, and in ccxt most of them raise AuthenticationError (the PermissionDenied that 50110 raises is a subclass of it). The exception is 50102, timestamp expired: in ccxt's master-branch source as of 2026-09 it maps to InvalidNonce, which sits under NetworkError, so a bare except AuthenticationError won't catch it and it needs its own branch. So don't work through "the most common causes" one by one — dig the code out first, then follow the line that matches it. 50120 "insufficient permission" has to be pulled out on its own: it isn't in that 401 pile. Per the official error-code table it comes back as HTTP 200 and is identified by the code in the body, so at the protocol level the request "succeeded" — going by the status code alone, you can't tell anything went wrong at all.
- 50113 Invalid signature: look at the signing step. How the signature payload is assembled (request method, request path, request body), whether the key used for the HMAC really is the secret, whether you ran HMAC-SHA256 and then Base64. With ccxt the library does the signing, so at that point suspect a miscopied secret first.
- 50105 wrong Passphrase (50104 = not supplied): look only at the passphrase. The
passwordfield in ccxt must hold the Passphrase you set when creating the Key—not your login password, and not the secret; if you can't recall it, the only way out is to delete the Key and recreate it. - 50102 / 50112 timestamp problems: look only at the clock. Per the official docs, a request timestamp more than 30 seconds away from server time is rejected. Turn on NTP auto-sync, and on long-running machines check periodically that it really is syncing. In ccxt, 50102 raises
InvalidNonce: resync the clock in that branch before retrying, and keep a finalBaseErrorcatch-all. - 50111 Invalid OK-ACCESS-KEY: look at the apiKey itself — a leading or trailing space from pasting, a dropped character, or a Key that's been deleted.
- 50101 APIKey does not match current environment: the Key is fine, the environment isn't — a demo Key sent without
set_sandbox_mode(True)(no demo header on the request, so it's treated as live), or a live Key sent with it on. Line the Key and the switch up, then printokx.headers.get('x-simulated-trading')as a self-check; on demo it should be 1. In ccxt this one raisesAuthenticationErrortoo. - 50110 IP not on the whitelist: look at the network egress, not at the credentials. See the "IP whitelist" section below.
- 50120 insufficient permission (HTTP 200): the credentials are all fine, this Key simply doesn't have the box ticked. It never shows up as a 401 — the only way to see it is to read the code in the body, so don't write your handling as just
if status == 401. See "Permission denied" below.
One more thing worth mentioning: OKX's msg text will sometimes name the exact header that's at fault, so write it down verbatim alongside the code — far quicker than guessing from impressions.
Permission denied
The classic symptom is "can check the balance, can't place orders." Checking the balance uses the read permission; placing orders uses the trade permission, and they're granted separately. Go back to API management on OKX, find this Key, and add "Trade"—no need to recreate. And if you happened to tick withdrawal—don't leave it on; a quant Key should never have withdrawal permission. Also, don't read "trade permission" too narrowly: by OKX's own definition it covers not just placing and cancelling orders but funding transfers and part of the account settings too, so there's no need to tick withdrawal just to move funds between your own accounts.
IP whitelist (50110)
This error says "the outbound IP you're on right now isn't on this Key's list," so troubleshooting starts at the network egress, not at the credentials:
- Find out what your current outbound IP is first and check it against the list you filled in for this Key in the dashboard. Note that it's the outbound IP: with a proxy on, through a corporate gateway, or after your home broadband has redialled, the egress may well not be the one you think.
- If the outbound IP is unstable, the fix is to pin the egress down, not to take the lock off: move the strategy onto a machine with a fixed IP, or update the whitelist on a controlled schedule when the egress genuinely does change. OKX allows up to 20 IPs per Key, IPv4, IPv6 and CIDR ranges included, so leaving yourself headroom isn't hard.
- While debugging, use a read-only Key or a demo Key rather than repeatedly testing with the main key that carries trade permission.
- Don't make "remove the whitelist" a routine troubleshooting move. Officially, binding an IP is strongly recommended rather than mandatory, but one rule is worth remembering: a Key with no IP bound that carries trade or withdraw permission expires after 14 days of inactivity — so an unbound Key isn't only riskier, it may also quietly stop working one day. Demo Keys are not subject to that expiry rule, which is one more reason to debug with one.
Insufficient balance
Beyond the literal "not enough money," there's a sneaky cause: OKX has different accounts (funding, trading, etc.), and your money may not be in the one used for ordering. Confirm the funds have been transferred to the trading account. On the demo, if the virtual funds run out, just reset them on the demo interface—another upside of practicing on the demo first, where trial and error costs no real money.
Symbol / precision class
Spot pairs in ccxt are uniformly UPPER/UPPER with a forward slash, like BTC/USDT. Writing it as BTCUSDT, lowercase, or in a futures format all throws errors. If the amount is too small or the precision is off, use load_markets() to check the pair's minimum order size and precision and go by what it requires. There's a full order example for this in our API quant intro.
Rate limits and backoff retry
The rate limit (50011 / Too Many Requests) is one script writers easily run into and should defend against ahead of time. It's not that you did something wrong—your requests were just too dense and tripped the exchange's protection. Handle it in two layers:
Layer one: turn on ccxt's built-in rate limiter. Add 'enableRateLimit': True at init, and ccxt will automatically insert reasonable gaps between requests. For most cases this step alone is enough:
okx = ccxt.okx({
'apiKey': 'YOUR_apiKey',
'secret': 'YOUR_secret',
'password': 'YOUR_Passphrase',
'enableRateLimit': True, # auto rate-limit, turn this on first
})
Layer two: add exponential backoff retry. If your strategy really does send dense requests at certain moments (like polling many pairs at once), auto rate-limiting alone may still occasionally hit. Wrap key requests in a retry layer then: wait 1 second on the first hit and retry, then 2, then 4, stretching it out, and give up with an error after enough hits.
import ccxt, time
def with_retry(fn, max_tries=5):
delay = 1
for i in range(max_tries):
try:
return fn()
except ccxt.RateLimitExceeded:
print(f'Rate limited, attempt {i+1}, waiting {delay}s before retry')
time.sleep(delay)
delay *= 2 # exponential backoff: 1 → 2 → 4 → 8 ...
raise RuntimeError('Still rate limited after retries, giving up')
# Usage: drop the call that gets rate limited in here
ticker = with_retry(lambda: okx.fetch_ticker('BTC/USDT'))
print(ticker['last'])
A cautionary counter-example: don't write a while True: that hammers the endpoint immediately with no delay in between. That kind of loop fires off dozens to hundreds of requests a second and instantly hits the rate limit. Any high-frequency loop needs gaps.
A common gotcha
fetch_ticker on a dozen-odd pairs back-to-back in a loop, with no delay in between, and forgetting to turn on enableRateLimit. Written that way it keeps tripping the endpoint's rate-limit protection: the terminal starts spamming RateLimitExceeded (which maps to 50011) and a large batch of the following requests fail. The fix is two steps: first add 'enableRateLimit': True at init, then wrap each pair's request in that with_retry backoff function above. Run it again after that and the requests have gaps between them; when one does hit the limit, it backs off and retries instead of simply being lost. The lesson is direct: hammering an endpoint in a loop, rate-limiting and backoff are standard issue, not optional.
FAQ
How do I read an OKX API error?
When an OKX endpoint errors out, it returns a code (a numeric error code) and a msg (text description), plus an HTTP status code. Look at the code first—it points most precisely at the type of problem; the msg helps you confirm the direction. In ccxt, errors are usually wrapped into matching exception types (like AuthenticationError, RateLimitExceeded, InsufficientFunds), and the exception message generally carries OKX's original code and msg too. Search that string as-is and you can usually pinpoint it.
Why do I keep getting auth failed / Invalid sign?
Don't treat auth failure as one error. At the HTTP layer they are all 401 and in ccxt most of them raise AuthenticationError — the exception is 50102 (timestamp expired), which raises InvalidNonce, a NetworkError, and needs its own branch — but the code OKX returns points at different things: 50113 means the signature didn't pass, so check how the signature payload is assembled and check the secret; 50105 means the Passphrase is wrong, and ccxt's password field must hold the passphrase you set when creating the Key; 50102 and 50112 are timestamp problems — a request more than 30 seconds away from server time is rejected, so turn on NTP; 50111 means the apiKey itself is invalid; 50101 means the Key and the environment don't match, a demo Key without set_sandbox_mode or a live Key with it on; 50110 means your current outbound IP is not on the whitelist. 50120, “insufficient permission”, has to be pulled out on its own: it isn't in that 401 pile — per the official error-code table it comes back as HTTP 200 and is identified by the code in the body, the credentials are all fine, this Key simply doesn't have the box ticked. Dig the code out first, then decide what to check — it's faster than working through causes by hunch.
What do I do about a rate limit (50011 / Too Many Requests)?
A rate limit is protection triggered by requests being too dense. The easiest fix is to turn on enableRateLimit when initializing ccxt, which automatically inserts gaps between requests. If you still hit it, add exponential backoff retry in the code—wait 1 second on the first hit, then 2, then 4, stretching it out. Don't use a while True with no delay to hammer the endpoint.
I can check the balance but ordering says permission denied—why?
Checking the balance is a read permission; placing orders is a trade permission, and they're granted separately when you create the Key. Being able to check but not order basically means you only ticked Read and not Trade. Go back to API management on OKX and add the Trade permission to this Key—no need to recreate.
What does timestamp expired mean?
OKX has a tolerance for the timestamp in a request; if your computer's or server's system time is off and exceeds the tolerance, the signature gets rejected as an expired timestamp. The fix is to sync your system time to accurate network time and turn on NTP auto-sync. Cloud servers usually have it on by default; local machines occasionally drift.
What's going on when it says instrument does not exist?
Read the code and msg in the error first; this class of error points at the symbol field. In ccxt, spot pairs are uppercase with a forward slash, like BTC/USDT; writing it as BTCUSDT, btc/usdt, or using a futures format for a spot order all throw instrument does not exist. The msg will sometimes name the offending field outright, and checking the field it names is the least work; when in doubt, run print(okx.load_markets().keys()) first to see what the available pairs actually look like.
Clear the error-troubleshooting hurdle and your script stops freezing up at every turn. The next thing worth your time is writing risk control into the code—scarier than an error is a script that doesn't error but keeps placing the wrong orders. And don't forget to run new code on the demo account first—debugging there costs no real money.