# OKX API in Python: passphrase, demo trading and limits

URL: https://fincodelabs.com/en/blog/okx-api-python · Publisher: Fincode (Fincode Yazılım ve Teknoloji A.Ş.), Istanbul, Turkey · Category: Exchange APIs · Published: 2026-11-05 · Updated: 2026-11-05

OKX API Python guide: key and passphrase, Base64 HMAC signing, demo trading with x-simulated-trading, error 50119, rate limits, WebSocket and the API Agreement.

**Short answer:** To use the **OKX API in Python**, create a key, secret and passphrase, sign each private request with Base64 HMAC-SHA256 over timestamp + method + request path + body, and send it in OK-ACCESS headers to `openapi.okx.com`. OKX has no separate testnet REST host: demo trading uses that same host with a demo key and the header `x-simulated-trading: 1`. Public market data needs no key.

Three things stand out on OKX: a third credential, a test mode selected by a header, and an API agreement that restricts running the API as a service for others. This guide follows OKX's official V5 documentation and API Agreement as read on 4 November 2026; general concepts are in [crypto exchange API integration](https://fincodelabs.com/en/blog/crypto-exchange-api-integration).

**What was run and what was not.** Public REST and WebSocket samples ran on 4 November 2026 on Python 3.11.3. Signed calls reached OKX with a placeholder key and were rejected at the key check, so server acceptance of the signature, a demo order and live order-level errors are untested here.

This is a technical guide, not a recommendation to use OKX or any strategy, and availability depends on your jurisdiction and OKX's terms. For readers in Turkey, Article 99/A of Capital Markets Law No. 6362 treats activity by foreign-based crypto-asset platforms directed at Turkish residents as unauthorized service provision, so foreign platforms may not target them. Automated trading can lose money.

## Does OKX have a testnet API? Demo trading explained

OKX has no separate test REST host: demo trading runs on the production host, selected by a header and a demo key, with its own WebSocket host.

**OKX production and demo endpoints, from the V5 overview, 4 November 2026**

| Item | Production | Demo trading |
| --- | --- | --- |
| REST | `https://openapi.okx.com` | Same host, plus `x-simulated-trading: 1` |
| WebSocket, public and private | `wss://ws.okx.com/ws/v5/...` | `wss://wspap.okx.com/ws/v5/...` |
| Key | Created on the API page | Created under Demo Trading in the personal center |

Because the host is shared, make the mode an explicit setting and refuse to start if header and key disagree. Error 50101 reports a key that does not match the environment. The docs say functions such as withdrawals, deposits and purchase or redemption are not supported in demo.

The overview also sets regional domains by where you registered: `us.okx.com` for US and Australian users of app.okx.com, and `eea.okx.com` for EU users of my.okx.com. It states that `openapi.okx.com` will not work for those regions. Demo results test software, not a strategy.

## OKX API Python example: public market data

Public endpoints need no key, and the instrument record gives the increments every order must respect.

```python
from decimal import Decimal, ROUND_DOWN
import requests

BASE = "https://openapi.okx.com"        # us.okx.com or eea.okx.com for those registrations

def get(path, **params):
    body = requests.get(BASE + path, params=params, timeout=10).json()
    if int(body["code"]):
        raise RuntimeError(body["code"], body["msg"])
    return body["data"]

def floor_to(value, step):
    step = Decimal(step)
    return (Decimal(value) / step).to_integral_value(rounding=ROUND_DOWN) * step

print(get("/api/v5/public/time")[0]["ts"])
inst = get("/api/v5/public/instruments", instType="SPOT", instId="BTC-USDT")[0]
last = get("/api/v5/market/ticker", instId="BTC-USDT")[0]["last"]
price = floor_to(Decimal(last) * Decimal("0.95"), inst["tickSz"])   # hypothetical price
size = floor_to("0.0012345", inst["lotSz"])                         # hypothetical size
print(inst["state"], last, inst["tickSz"], inst["lotSz"], inst["minSz"])
print(price, size.normalize(), size >= Decimal(inst["minSz"]))
```

Output on 4 November 2026:

```
1793821821441
live 80056.7 0.1 0.00000001 0.00001
76053.8 0.0012345 True
```

> **Hypothetical example.** A limit 5% below 80,056.7 USDT is 76,053.865, which floors to 76,053.8 at a `tickSz` of 0.1. A size of 0.0012345 BTC already fits the `lotSz` and is above the `minSz` of 0.00001. This shows the rounding only.

## OKX API key and passphrase: what to keep

An OKX API key has three parts, the key, the secret and a passphrase you choose, and a lost passphrase means creating a new key.

- **Passphrase.** OKX stores only a salted hash and says it cannot recover it.
- **Permissions.** Read, Trade and Withdraw. A trading bot has no need for Withdraw; see [API key security for trading bots](https://fincodelabs.com/en/blog/api-key-security-trading-bots).
- **IP binding.** Each key can bind up to 20 addresses or ranges, and OKX strongly recommends it.
- **Expiry.** Keys with Trade or Withdraw and no bound IP expire after 14 days of inactivity; demo keys do not.

The account steps are in the [exchange API key guide](https://fincodelabs.com/en/blog/exchange-api-key-how-to).

## How to sign OKX API requests in Python

The signature is a Base64-encoded HMAC-SHA256 of timestamp + uppercase method + request path + body, using your secret.

```python
import base64, hashlib, hmac, json, os
from datetime import datetime, timezone
import requests

BASE = "https://openapi.okx.com"
KEY = os.environ.get("OKX_DEMO_KEY", "00000000-0000-0000-0000-000000000000")   # demo key only
SECRET = os.environ.get("OKX_DEMO_SECRET", "placeholder-secret")
PASSPHRASE = os.environ.get("OKX_DEMO_PASSPHRASE", "placeholder-passphrase")

def request(method, path, body=None):
    text = json.dumps(body, separators=(",", ":")) if body else ""
    ts = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] + "Z"  # UTC, ms
    prehash = ts + method + path + text              # GET parameters belong in path
    sign = base64.b64encode(hmac.new(SECRET.encode(), prehash.encode(),
                                     hashlib.sha256).digest()).decode()
    headers = {"OK-ACCESS-KEY": KEY, "OK-ACCESS-SIGN": sign, "OK-ACCESS-TIMESTAMP": ts,
               "OK-ACCESS-PASSPHRASE": PASSPHRASE, "Content-Type": "application/json",
               "x-simulated-trading": "1"}           # demo trading; omit only for production
    r = requests.request(method, BASE + path, data=text or None, headers=headers, timeout=10)
    return r.status_code, r.json()

print(request("GET", "/api/v5/account/balance?ccy=USDT"))
print(request("POST", "/api/v5/trade/order",
              {"instId": "BTC-USDT", "tdMode": "cash", "side": "buy", "ordType": "limit",
               "px": "20000", "sz": "0.001", "clOrdId": "demo0001"}))
```

Output on 4 November 2026 with the placeholder credentials:

```
(401, {'msg': "API key doesn't exist", 'code': '50119'})
(401, {'msg': "API key doesn't exist", 'code': '50119'})
```

OKX stopped at the key, so this confirms the headers arrive, not that OKX accepts the signature. Offline, on the same inputs, the signature matched the signer in python-okx 0.4.4. With a demo key, the second call places a limit order priced far from the market.

- **Timestamp.** ISO 8601 in UTC with milliseconds; a difference of more than 30 seconds from server time returns 50102. OKX names local timezone offsets as the most common cause.
- **Order results.** When the response data carries `sCode` and `sMsg`, those fields hold the order's result, so a request can succeed while an order fails.
- **`clOrdId`.** Up to 32 case-sensitive letters and numbers, with no dashes; use it to find an order after a timeout.

OKX's docs link to `python-okx` on PyPI as their Python SDK, and it handles this signing. Release 0.4.4 (7 September 2026) defaults to `www.okx.com`, so pass the documented host:

```python
import okx.MarketData as MarketData                # pip install python-okx
import okx.Trade as Trade

HOST, DEMO = "https://openapi.okx.com", "1"        # flag: "1" demo, "0" production
market = MarketData.MarketAPI(flag=DEMO, domain=HOST, debug=False)
print(market.get_ticker("BTC-USDT")["data"][0]["instId"])

trade = Trade.TradeAPI("00000000-0000-0000-0000-000000000000", "placeholder-secret",
                       "placeholder-passphrase", False, DEMO, domain=HOST, debug=False)
print(trade.place_order(instId="BTC-USDT", tdMode="cash", side="buy", ordType="limit",
                        px="20000", sz="0.001", clOrdId="demo0001"))
```

It printed `BTC-USDT`, then the same 50119 error. For a multi-exchange interface, see the [CCXT Python guide](https://fincodelabs.com/en/blog/ccxt-python-trading-bot).

## OKX API key doesn't exist and other auth errors

Error 50119 means OKX found no key matching the value you sent, so check the key value, the regional host and whether the key has expired.

**Authentication errors, from OKX's error code list, 4 November 2026**

| Code | Message | Where to look |
| ---: | --- | --- |
| 50119 | API key doesn't exist | Copy error, deleted or expired key, wrong regional host |
| 50111 | Invalid OK-ACCESS-KEY | Malformed value; a non-UUID placeholder returned this in our test |
| 50101 | APIKey does not match current environment | Demo key without the header, or the reverse |
| 50113 | Invalid signature | Prehash string, query in the path, body sent differs from body signed |
| 50105 | Request header OK-ACCESS-PASSPHRASE incorrect | Passphrase; a lost one needs a new key |
| 50102 | Timestamp request expired | UTC format and clock sync |

The list gives messages, not causes, so the last column is our checklist. It also lists HTTP 200 for 50119, while our test received HTTP 401, so branch on the `code` field.

## OKX API rate limits

OKX sets limits per endpoint and returns error 50011 when one is reached, while order-placement limits apply per instrument and are shared by REST and WebSocket.

**OKX limits, from the V5 overview and endpoint pages, 4 November 2026**

| Scope | Limit | Basis | Error |
| --- | --- | --- | --- |
| Public REST | Per endpoint; the ticker is 20 requests per 2 seconds | IP | 50011 |
| Private REST | Per endpoint | User ID; sub-accounts have their own | 50011 |
| Place order | 60 requests per 2 seconds | User ID + instrument ID (options: instrument family) | 50011 |
| Sub-account orders | 1,000 new and amended orders per 2 seconds; higher tiers at VIP5 and above | Sub-account | 50061 |
| WebSocket connections | 3 per second | IP | Not stated |
| WebSocket requests | 480 subscribe, unsubscribe or login per hour | Connection | Not stated |

Placing, amending and canceling have independent limits, and batch endpoints are counted apart from single-order endpoints. The docs exempt spot and margin orders, among others, from the sub-account limit, but the per-instrument limit still applies. The Agreement separately forbids circumventing rate limits. Add your own limits as in [trading bot risk management](https://fincodelabs.com/en/blog/trading-bot-risk-management).

## OKX WebSocket example in Python

Connect to the public host, send a `subscribe` message, and send the literal string `ping` when nothing has arrived for a period shorter than 30 seconds.

```python
import asyncio, json
import websockets                                  # pip install websockets

async def tickers(url, updates=2):
    async with websockets.connect(url) as ws:
        await ws.send(json.dumps({"op": "subscribe",
                                  "args": [{"channel": "tickers", "instId": "BTC-USDT"}]}))
        seen = 0
        while seen < updates:
            try:
                raw = await asyncio.wait_for(ws.recv(), 25)   # under the 30-second cut-off
            except asyncio.TimeoutError:
                await ws.send("ping")              # the literal string; expect "pong"
                continue
            if raw == "pong":
                continue
            msg = json.loads(raw)
            if "event" in msg:
                print(msg["event"], msg.get("arg", {}).get("channel"))
            elif "data" in msg:
                seen += 1
                print(msg["data"][0]["instId"], msg["data"][0]["bidPx"], msg["data"][0]["askPx"])

asyncio.run(tickers("wss://ws.okx.com/ws/v5/public"))         # production
asyncio.run(tickers("wss://wspap.okx.com/ws/v5/public", 1))   # demo trading
```

Output on 4 November 2026:

```
subscribe tickers
BTC-USDT 80088 80088.1
BTC-USDT 80085.9 80086
subscribe tickers
BTC-USDT 80085.9 80086
```

OKX closes a connection with no subscription or no data for more than 30 seconds. Use port 443, the default: OKX's change log ends port 8443 on 31 October 2026, yet a 4 November test still connected, with notice 64010. Private channels need a login message signed in a similar way, which was not run because it needs a real key. Depth books follow the logic in the [local order book guide](https://fincodelabs.com/en/blog/websocket-order-book).

## OKX API Agreement Section 9.3 in plain words

Using the API for your own account is allowed, but running it as a service for others needs OKX's written approval.

Our plain reading of the Agreement (last updated 28 July 2026), which is not legal advice:

- **Section 9.2.** The license covers your own internal purposes and your own OKX account.
- **Section 9.3.** Without written authorization you may not resell the API, offer it as part of a commercial product, SaaS platform, brokerage, signal service, strategy marketplace or managed trading service, or execute orders for third parties.
- **Section 9.4.** Market data is for personal, non-commercial trading use, and it may not be redistributed or displayed to third parties without written consent. This applies to public endpoints too, which matters for dashboards and price widgets.

A multi-customer hosted bot or a public price page is the kind of thing these sections address, so get OKX's position in writing first. Terms differ by entity: OKX TR publishes its own agreement (dated 26 March 2026; page metadata shows a change on 16 September 2026) with a matching Section 9.3. A company named OKX TR Kripto Varlık Alım Satım Platformu AŞ is on the provisional list of Turkey's Capital Markets Board as checked on 4 November 2026, and the Board states that being listed does not mean a firm is authorized.

## Frequently asked questions

### Does OKX have a testnet?

Not as a separate REST host. Demo trading uses openapi.okx.com with a demo key and the x-simulated-trading: 1 header, and WebSocket uses wspap.okx.com.

### Can I recover a lost OKX API passphrase?

No. OKX stores a salted hash and says it cannot recover the passphrase, so you need to create a new API key.

### What does OKX API key doesn't exist mean?

It is error 50119. Check for a copy error, a deleted or expired key, and the regional host: keys from the US, Australian or EU sites use us.okx.com or eea.okx.com.

### What is the OKX API rate limit error?

Error 50011 for an endpoint's limit, and 50061 for the sub-account order limit of 1,000 new and amended orders per 2 seconds, as listed on 4 November 2026.

### Can I run an OKX bot as a service for clients?

Section 9.3 of the OKX API Agreement requires written authorization for commercial products, SaaS, signal services, managed trading and orders for third parties. Ask OKX in writing and take legal advice.

## Sources

- [OKX API v5: Overview (regional domains, API Agreement notice)](https://www.okx.com/docs-v5/en/#overview)
- [OKX API v5: API key creation (passphrase, permissions, IP binding, expiry)](https://www.okx.com/docs-v5/en/#overview-api-key-creation)
- [OKX API v5: REST authentication (headers, signature)](https://www.okx.com/docs-v5/en/#overview-rest-authentication)
- [OKX API v5: Demo trading services](https://www.okx.com/docs-v5/en/#overview-demo-trading-services)
- [OKX API v5: Rate limits](https://www.okx.com/docs-v5/en/#overview-rate-limits)
- [OKX API v5: WebSocket (limits, ping, port 8443 notice)](https://www.okx.com/docs-v5/en/#overview-websocket)
- [OKX API v5: Change log (port 8443 discontinuation, 22 October 2026)](https://www.okx.com/docs-v5/log_en/)
- [OKX API v5: Place order](https://www.okx.com/docs-v5/en/#order-book-trading-trade-post-place-order)
- [OKX API v5: REST error codes](https://www.okx.com/docs-v5/en/#error-code-rest-api-public)
- [OKX API Agreement (last updated 28 July 2026)](https://www.okx.com/help/okx-api-agreement)
- [OKX TR API Agreement (Turkish, dated 26 March 2026, page metadata modified 16 September 2026)](https://web3.okx.com/tr/help/okx-api-agreement)
- [PyPI: python-okx (release 0.4.4)](https://pypi.org/project/python-okx/)
- [SPK: Provisional list of operating crypto-asset service providers (Turkish)](https://spk.gov.tr/kurumlar/kripto-varlik-hizmet-saglayicilar/faaliyette-bulunanlar-listesi)
- [SPK: Announcement on crypto-asset service providers, 5 August 2024 (Turkish)](https://spk.gov.tr/duyurular/basin-duyurulari/2024/kripto-varlik-hizmet-saglayicilara-iliskin-duyuru_05082024)
- [Capital Markets Law No. 6362 (consolidated text, Article 99/A, Turkish)](https://www.mevzuat.gov.tr/mevzuatmetin/1.5.6362.pdf)

> This guide is general information about software and markets, not investment, legal or tax advice. Automated trading can lose money, past or backtested performance does not guarantee future results, and the rules that apply depend on your jurisdiction and your exchange's terms. Sources, checks and corrections: see the [editorial policy](https://fincodelabs.com/en/editorial-policy).

---

Published by Fincode (Fincode Yazılım ve Teknoloji A.Ş.), a software company in Istanbul, Turkey: https://fincodelabs.com/en/blog/okx-api-python

Cite as: Fincode, "OKX API in Python: passphrase, demo trading and limits", updated November 5, 2026, https://fincodelabs.com/en/blog/okx-api-python
