— Contents 18 sections
  1. 01 How the KuCoin API works
  2. 02 Before you start: keys, passphrase, no sandbox, and one auth helper
  3. 03 Getting the current price from the KuCoin API
  4. 04 Fetching KuCoin candlestick data (klines)
  5. 05 Reading a KuCoin symbol’s trading rules (increments and minimums)
  6. 06 Checking your KuCoin balance in Python
  7. 07 Placing a market order with the KuCoin API
  8. 08 Placing a limit order with the KuCoin API (and where stop orders fit)
  9. 09 Canceling orders and listing open KuCoin orders
  10. 10 Checking KuCoin order status and trade history
  11. 11 Getting a KuCoin deposit address (and deposit history)
  12. 12 Withdrawing funds with the KuCoin API
  13. 13 Moving funds between KuCoin accounts (Funding, Trading, Futures)
  14. 14 KuCoin WebSocket streams: live prices in Python
  15. 15 KuCoin API rate limits and common errors
  16. 16 Running your KuCoin API code as a bot
  17. 17 Conclusion and next steps
  18. 18 Frequently asked questions

The official KuCoin API documentation is a reference: one page per endpoint, current, and hard to use when all you want is the ten lines of Python that place an order or read your balance. This guide is the other half — for each common operation, what the docs specify in one or two sentences, then the shortest code that actually works, then the mistakes that cost people time. Everything here was verified against the current official documentation and, where the endpoint allows it, against the live API. The scope is spot trading plus the account operations (deposits, withdrawals, transfers between your own KuCoin accounts) and the WebSocket streams a typical account needs.

How the KuCoin API works

A person holding a small route map stands before a domed building with several wings, colored paths leading to different entrances while two old side doors are boarded up

KuCoin documents everything on one site — www.kucoin.com/docs-new — but the site is split twice: first by account model, then by product line. The first split is the one that trips people up. The documentation has a UTA section (REST and WebSocket for the Unified Trading Account, also called the Pro API) and a Classic section. UTA launched in 2026 for VIP 2 and above, the Help Center still describes it as a limited beta, and the API change log says the Pro API “should not be used in production trading environments”. If you never switched account modes, you have a Classic account, and everything in this guide is the Classic API. Inside Classic, these are the product lines:

SectionHost and path styleWhat it isIn this guide
Spot Tradingapi.kucoin.com + /api/v1/…, /api/v2/…, /api/v3/…prices, candles, order book, symbol rules; placing and managing spot orders (the hf order endpoints)Yes
Account Infoapi.kucoin.combalances, key info, deposits, withdrawals, transfers between your own accounts, fee ratesYes
Spot / Margin WebSocketdynamic host from a token call, currently wss://ws-api-spot.kucoin.comlive tickers, candles, order book, your order and balance eventsYes (spot topics)
Margin Tradingapi.kucoin.com (own section)borrowing to trade spot with leverage — same host, separate endpointsNo
Futures Tradingapi-futures.kucoin.com (symbols like XBTUSDTM)perpetuals with positions and leverage; own base URL, own rate-limit pool, own WebSocket tokenNo — separate tree
Earn, VIP Lending, Copy Trading, Convert, Affiliate, Brokervarious (api-broker.kucoin.com for brokers)product-specific APIsNo
UTA REST / WebSocket (“Pro API”)api.kucoin.com + /api/ua/v1/…, wss://x-push-… (public)the Unified Trading Account API — VIP 2+, betaNo

This guide covers the first three rows — the Classic spot API, the account operations every account needs, and the spot WebSocket. Futures is a complete, separate tree: different host, different symbols, a position and leverage model of its own, and a separate rate-limit pool. Mixing futures values into spot code is exactly the confusion this guide exists to prevent, so futures belongs in its own guide rather than a shared chapter here.

Three things to know before reading any KuCoin endpoint page:

  • The docs moved, and the old ones redirect. docs.kucoin.com and docs.kucoin.com/futures now 301-redirect to docs-new; the GitHub repo behind the old docs has been archived since 2023. Any tutorial built on the old URLs is at least that old.
  • “Abandoned Endpoints” is a real directory in the docs. It holds endpoints that still answer but are “no longer recommended”: the old spot order family (POST /api/v1/orders), the v2 inner-transfer, the v1/v2 deposit-address and v1 withdrawal calls. Every recipe below uses the current replacement.
  • Path versions and key versions are unrelated. /api/v1/… and /api/v3/… in a URL are just endpoint versions that coexist. The API key version (KC-API-KEY-VERSION) is a separate thing, and only version 3 keys work today (older key versions were invalidated, the last of them in August 2024).

Two transports matter for most users. REST is request/response over HTTPS — one call, one answer — right for orders, balances and anything occasional. WebSocket pushes data to you continuously — right for live prices; the official docs say outright that they “highly recommend that API users utilize Websocket to get the real-time data” rather than polling REST. Every response, success or failure, is a JSON envelope with a six-digit string code; success is exactly "200000", and — verified live while writing this — some failures arrive with HTTP 200 and a non-success code, so your code must branch on code, not on the HTTP status. Requests that touch your account are signed: a timestamp, an HMAC-SHA256 signature over the request, and a signed passphrase, sent as headers. Rate limiting is weight-based resource pools refilled every 30 seconds — the spot pool has 4,000 weight per 30 s at VIP 0 (the base volume tier), public market data 2,000 per 30 s per IP.

One more thing that is easy to miss: KuCoin runs regional sites (Europe, Turkey, Australia, Thailand) alongside the global exchange, and a key created on one of them is not a global key. Everything in this guide is for the global site, www.kucoin.com.

Before you start: keys, passphrase, no sandbox, and one auth helper

A person seals an envelope with a round medallion holding three small emblems: a key, a heart-shaped locket and a speech bubble

You need a KuCoin account with identity verification (mandatory for new users since 2023-08-31), Google two-factor authentication and a trading password — creating a key asks for the trading password, an email code and a Google code. If you don’t have an account, our KuCoin review covers registration. Create the key under avatar → API Management → Create API, choose API Trading, give it a name and type an API passphrase of your own. KuCoin then shows three values — Key, Secret and your Passphrase — once. They cannot be recovered; lose one and you create a new key.

Permissions are named after product lines. For the recipes here:

  • General — read-only: balances, key info, order history. Enough for monitoring, tax and portfolio tools.
  • Spot — place and cancel spot orders. General + Spot is the minimum for a spot bot.
  • Withdrawal — withdraw and cancel withdrawals. KuCoin only allows it on a key with IP restriction enabled, and its own docs warn that with this permission the API can move money out “without email verification or Google verification”. Leave it off unless a recipe below needs it.
  • FlexTransfers — move funds between your own accounts (Funding ↔ Trading ↔ Futures). Also requires IP restriction, and only a master-account key can use it.

Restrict the key to your IP even if you skip the two fund-moving permissions: KuCoin deletes trading-enabled keys that have no IP whitelist, or strips their trade permission, after 30 days of inactivity. General-only keys have no expiry. Note the vocabulary shift when you inspect a key through the API: GET /api/v1/user/api-key reports the Withdrawal permission as Transfer and FlexTransfers as InnerTransfer.

There is no sandbox. KuCoin delisted its sandbox environment on 2023-07-10; the old hosts (openapi-sandbox.kucoin.com, sandbox.kucoin.com) no longer resolve, and any tutorial or library that offers a sandbox=True switch is out of date. Three things replace it: every public endpoint and the public WebSocket work with no key at all; POST /api/v1/hf/orders/test takes a real order request, checks the signature and parameters, and never sends it to the matching engine; and every USDT pair accepts orders worth as little as 0.1 USDT, so a first live order costs cents. Every recipe below says which of these applies.

Two diagnostics worth knowing before you debug anything the hard way:

  • Ask KuCoin what your key can do. GET /api/v1/user/api-key returns the key’s permission list, ipWhitelist, apiVersion (must be 3), expiredAt, KYC status and region — the right first authenticated call.
  • Check your signing code offline. The signature is base64(HMAC-SHA256(secret, message)) — base64, not hex, is where most hand-rolled helpers go wrong. Two official values let you check without a key: the unit test in KuCoin’s own SDK signs the message test_plain with the secret test_api_secret and expects jie/gxS38wLFFaBwl/cAzFbAzlI8MIny96jSXrB+iP4=; and the worked example in KuCoin’s original API documentation signs a full request string. That example predates V3 keys, but the signature construction did not change — KuCoin’s own upgrade note says V3 “does not require the client to modify any code” — so it still validates the string-to-sign:
message: 1547015186532POST/api/v1/deposit-addresses{"currency":"BTC"}
secret:  f03a5284-5c39-4aaa-9b20-dea10bdcf8e3
sign:    7QP/oM0ykidMdrfNEUmng8eZjg/ZvPafjIqmxiVfYu4=

The docs’ sample values are safe to print precisely because they are not a real key — real keys never belong in a code block. (Ignore the passphrase header shown in that old example: it is plain base64, the pre-V3 rule.)

All signed examples below share this helper. It builds the message KuCoin signs — timestamp + HTTP method + path (with the raw query string) + JSON body — sends the passphrase in its signed form, and turns any non-200000 code into an exception, because a KuCoin error can arrive with HTTP 200:

import base64, hashlib, hmac, json, os, time, uuid
import requests

BASE = "https://api.kucoin.com"   # one host: KuCoin has no sandbox or testnet
# The key must come from the global site (www.kucoin.com). Keys created on a
# regional KuCoin site (EU / TR / AU / TH) are rejected here as "site mismatch".
KEY = os.environ["KUCOIN_KEY"]
SECRET = os.environ["KUCOIN_SECRET"]
PASSPHRASE = os.environ["KUCOIN_PASSPHRASE"]   # the one you typed when creating the key

def _b64_hmac(message):
    return base64.b64encode(
        hmac.new(SECRET.encode(), message.encode(), hashlib.sha256).digest()).decode()

def _signed(method, path, params=None, body=None):
    query = "&".join(f"{k}={v}" for k, v in (params or {}).items())
    path_q = f"{path}?{query}" if query else path
    payload = json.dumps(body, separators=(",", ":")) if body is not None else ""
    ts = str(int(time.time() * 1000))
    headers = {
        "KC-API-KEY": KEY,
        "KC-API-SIGN": _b64_hmac(ts + method + path_q + payload),
        "KC-API-TIMESTAMP": ts,
        "KC-API-PASSPHRASE": _b64_hmac(PASSPHRASE),   # V3 keys: signed, never plain text
        "KC-API-KEY-VERSION": "3",
        "Content-Type": "application/json",
    }
    r = requests.request(method, BASE + path_q, headers=headers, data=payload or None)
    out = r.json()
    if out.get("code") != "200000":            # HTTP 200 does not mean success on KuCoin
        raise RuntimeError(f"KuCoin HTTP {r.status_code} code {out.get('code')}: {out.get('msg')}")
    return out["data"]

def signed_get(path, **params):    return _signed("GET", path, params=params)
def signed_post(path, **body):     return _signed("POST", path, body=body)
def signed_delete(path, **params): return _signed("DELETE", path, params=params)

Three details in that helper are load-bearing. The body is serialized with no spaces and sent byte-for-byte as signed — if the JSON you sign differs from the JSON you send by a single space, you get 400005 Signature error. The query string goes into the message un-encoded, which is what the docs require. And the timestamp is milliseconds and must sit within roughly ±5 seconds of KuCoin’s clock (400002 Invalid KC-API-TIMESTAMP otherwise) — sync your system clock, or read GET /api/v1/timestamp and offset. The helper signs with HMAC-SHA256 — the only signing scheme the KuCoin docs describe, so there is no alternative key type to choose. As a bonus, the self-check from above is now a one-liner: signed_get("/api/v1/user/api-key").

Getting the current price from the KuCoin API

A person studies a row of candlestick shapes rising across a table, next to a ruler and three stepped blocks

Docs say: GET /api/v1/market/orderbook/level1 returns the last traded price and the best bid and ask for one symbol. Public — no key, no signature. Permission: none.

import requests
BASE = "https://api.kucoin.com"
t = requests.get(f"{BASE}/api/v1/market/orderbook/level1",
                 params={"symbol": "BTC-USDT"}).json()["data"]
print(t["price"], t["bestBid"], t["bestAsk"])

Gotchas: KuCoin symbols are hyphenated and uppercaseBTC-USDT, not BTCUSDT. The trap is what happens when you get it wrong: this endpoint answers an unknown symbol (BTCUSDT, a typo, a delisted pair) with code 200000 and data: null — no error at all — so the line above fails one step later with a TypeError. Null-check data in anything unattended. Use the symbol field, not name, in API calls: name changes when a coin is renamed, symbol does not. For a whole-market snapshot use GET /api/v1/market/allTickers (weight 15, refreshed every 2 seconds); for 24-hour high/low/volume use GET /api/v1/market/stats?symbol=…. And don’t poll level1 in a loop for a live feed — that is what the WebSocket ticker topic is for (see the streams section). Testing: keyless — runs as-is. Reference: Get Ticker

Fetching KuCoin candlestick data (klines)

Docs say: GET /api/v1/market/candles returns candles for a symbol and type (interval), optionally between startAt and endAt, up to 1,500 rows per call. Public. Permission: none.

rows = requests.get(f"{BASE}/api/v1/market/candles",
                    params={"symbol": "BTC-USDT", "type": "1hour"}).json()["data"]
# each row is a list of strings: [0] open time (seconds), [1] open, [2] close,
# [3] high, [4] low, [5] volume (base), [6] turnover (quote) — newest first
closes = [float(r[2]) for r in reversed(rows)]   # oldest → newest

Gotchas: three things differ from other exchanges. The interval vocabulary is KuCoin’s own — 1min, 3min, 5min, 15min, 30min, 1hour, 2hour, 4hour, 6hour, 8hour, 12hour, 1day, 1week, 1month; send 1h and you get 400100 Incorrect candlestick type — with HTTP 200, verified live. Column order is open, close, high, low (close comes second, not last). And rows come newest first, so the first row is the still-forming candle — reverse the list and drop the newest before treating anything as closed. startAt/endAt are in seconds, not milliseconds; without a range you get the latest 100 rows, with a range up to 1,500 — page by time for more. The docs also note that intervals with no trades produce no row, so don’t assume a continuous series. Testing: keyless — runs as-is. Reference: Get Klines

Reading a KuCoin symbol’s trading rules (increments and minimums)

Docs say: GET /api/v2/symbols/{symbol} returns the rules an order must satisfy: baseIncrement (quantity step), priceIncrement (price step), quoteIncrement (step for market-order funds), baseMinSize, quoteMinSize, minFunds (minimum order value) and enableTrading. Public. Permission: none.

from decimal import Decimal, ROUND_DOWN
rules = requests.get(f"{BASE}/api/v2/symbols/BTC-USDT").json()["data"]

def round_price(p):
    return str(Decimal(str(p)).quantize(Decimal(rules["priceIncrement"]), rounding=ROUND_DOWN))
def round_size(q):
    return str(Decimal(str(q)).quantize(Decimal(rules["baseIncrement"]), rounding=ROUND_DOWN))

print(rules["minFunds"], round_price(60000.123), round_size(0.000123456789))
# BTC-USDT on 2026-08-18: priceIncrement 0.1, baseIncrement 0.00000001, minFunds 0.1

Gotchas: every order value must clear minFunds — for a limit order that is price × size, for a market buy the funds you send — and on 2026-08-18 minFunds was 0.1 USDT on every USDT-quoted pair, far below the 5–10 USDT minimums people carry over from other exchanges. That is what makes a live smoke-test order cheap on KuCoin. Fetch the rules at start-up rather than hardcoding them; the docs say increments “may be adjusted in the future”. Also read enableTrading (paused pairs return false) and note that some symbols carry a fee coefficient of 2 (takerFeeCoefficient), meaning double the base fee. Testing: keyless — runs as-is. Reference: Get Symbol

Checking your KuCoin balance in Python

A person leans over an open two-compartment wallet with a magnifying glass, coins sitting in both the pink and the amber pocket

Docs say: GET /api/v1/accounts lists your accounts — one row per currency per account type — with balance, available and holds. Filter with type=main or type=trade and currency=USDT. Signed. Permission: General.

for a in signed_get("/api/v1/accounts", type="trade"):
    print(a["currency"], a["balance"], a["available"], a["holds"])

Gotchas: KuCoin keeps your money in separate accounts, and the names differ between the app and the API. main is the Funding Account in the UI — where deposits land and the only account withdrawals come from. trade is the Trading Account — where spot orders draw funds. So the classic first-day problem, “my deposit arrived but the bot sees zero”, is a type problem: the funds are in main and your order needs them in trade. Move them with the transfer recipe below (or in the web UI). Size orders off available, not balanceholds is what your own open orders have already reserved. Long- time users may see a legacy trade_hf account; GET /api/v1/hf/accounts/opened tells you whether that applies to you (for most accounts it does not). Testing: needs a key; read-only, so a General-only key is enough. Reference: Get Account List - Spot

Placing a market order with the KuCoin API

A worker operates a matching machine that joins a blue and a pink half inside a glass chamber, with a practice ring of pieces beside it and a claw pulling one piece away

Docs say: POST /api/v1/hf/orders with type=market buys or sells immediately. You give either size (amount of the base asset, e.g. BTC) or funds (amount of the quote asset to spend or receive, e.g. USDT) — exactly one of the two. Permission: Spot.

# spend 1 USDT at market price (well above minFunds 0.1 on 2026-08-18)
order = signed_post("/api/v1/hf/orders", clientOid=str(uuid.uuid4()),
                    symbol="BTC-USDT", side="buy", type="market", funds="1")
print(order)   # {'orderId': '...', 'clientOid': '...'}

Gotchas: funds is the beginner-friendly form — “spend a round 1 USDT” — and it must be a multiple of quoteIncrement; size must be a multiple of baseIncrement, and the docs warn that a size-only market order may briefly freeze all funds in the account until it fills or cancels. timeInForce is not supported on market orders. Send a clientOid (a UUID) even though the current endpoint does not require it — it is the only safe way to retry a timed-out request and to cancel or query the order by your own id; a duplicate clientOid is rejected with 102426. All market orders pay the taker fee (a taker order fills immediately against the book; a maker order rests on it). To rehearse the request without trading, send the same parameters to POST /api/v1/hf/orders/test: the docs promise only that a test order never reaches the matching engine — treat a passing test as validating your signing and parameters, not your balance. Insufficient balance (400100) or 200004 are errors of the real endpoint, and usually mean the funds are still in the Funding account. Testing: no sandbox — rehearse on /api/v1/hf/orders/test, then a real order at the minimum value. Reference: Add Order · Add Order Test

Placing a limit order with the KuCoin API (and where stop orders fit)

Docs say: type=limit requires price and size. timeInForce is GTC (default, rests until canceled), GTT (good till a cancelAfter number of seconds), IOC (fill what you can, cancel the rest) or FOK (fill fully or not at all). postOnly=true makes the order maker-only. Permission: Spot.

order = signed_post("/api/v1/hf/orders", clientOid=str(uuid.uuid4()),
                    symbol="BTC-USDT", side="buy", type="limit",
                    price=round_price(60000), size=round_size(0.0001),
                    timeInForce="GTC")

Gotchas: price must sit on the priceIncrement grid, size on the baseIncrement grid, and price × size must clear minFunds — the three rejections behind most first limit orders, all avoidable with the rounding helpers from the symbol-rules recipe. Numbers are strings in every KuCoin request; the helpers above return strings for that reason. postOnly is ignored with IOC/FOK, and a post-only order that would fill immediately is canceled rather than filled as a taker. KuCoin also applies price protection: an order that would execute too far through the book (beyond the symbol’s priceLimitRate) is canceled if it is a limit order, or partially executed if it is a market order — 102429 is the related error. Limits: 2,000 active orders per account, 200 per pair. Stop-loss and take-profit orders live on a different endpoint, POST /api/v1/stop-order (stopPrice plus a limit or market order that fires when triggered, stop=loss or stop=entry, at most 20 untriggered per pair); the docs list it under normal spot orders, but its metadata marks it as legacy, so keep it out of a first bot’s core loop. Testing: no sandbox — rehearse on /api/v1/hf/orders/test; a resting limit order far from the market is ideal practice material for the next recipe. Reference: Add Order · Add Stop Order

Canceling orders and listing open KuCoin orders

Docs say: GET /api/v1/hf/orders/active?symbol=… lists working orders for one symbol; DELETE /api/v1/hf/orders/{orderId}?symbol=… cancels one (or DELETE /api/v1/hf/orders/client-order/{clientOid}?symbol=… by your own id); DELETE /api/v1/hf/orders?symbol=… cancels everything on a symbol. Permission: listing needs General; canceling needs Spot — a read-only monitor key can see open orders but not pull them.

for o in signed_get("/api/v1/hf/orders/active", symbol="BTC-USDT"):
    signed_delete(f"/api/v1/hf/orders/{o['id']}", symbol="BTC-USDT")

Gotchas: symbol is required on all of these — there is no single call that lists open orders across every symbol; use GET /api/v1/hf/orders/active/symbols to learn which symbols have open orders, then loop (note: unlike the listing call, /active/symbols needs the Spot permission). The docs are explicit that a cancel endpoint “only sends cancellation requests”: the result must be confirmed by querying the order or by the private WebSocket stream, so in automation treat a cancel that returns before the order is gone as normal, and re-query state instead of assuming. Cancel-all across all symbols (DELETE /api/v1/hf/orders/cancelAll) costs weight 30 versus 1–2 for the calls above. If your bot can die with orders resting, look at POST /api/v1/hf/orders/dead-cancel-all — a timer KuCoin runs server-side that cancels your orders unless you keep renewing it. Testing: no sandbox — cancel the resting limit order from the previous recipe. Reference: Cancel Order By OrderId · Get Open Orders

Checking KuCoin order status and trade history

Docs say: GET /api/v1/hf/orders/{orderId}?symbol=… fetches one order (or /client-order/{clientOid}); GET /api/v1/hf/orders/done?symbol=… pages through completed orders; GET /api/v1/hf/fills?symbol=… returns your actual executions with fee fields. Permission: General, for all three.

o = signed_get(f"/api/v1/hf/orders/{order['orderId']}", symbol="BTC-USDT")
print(o["active"], o["dealSize"], o["dealFunds"], o["fee"], o["feeCurrency"])

fills = signed_get("/api/v1/hf/fills", symbol="BTC-USDT")
for f in fills["items"]:
    print(f["price"], f["size"], f["fee"], f["feeCurrency"], f["liquidity"])

Gotchas: orders tell you what you asked for; fills tell you what actually happened — prices, sizes, and the fee per execution (liquidity says whether you were taker or maker). Any profit-and-loss or fee accounting must come from fills. The history windows are short: filled orders are queryable 7 days back and canceled orders 2 days back on the spot endpoints, and fills for 7 days — store what you need. Pagination on the hf endpoints is cursor-style (lastId + limit, max 100), not page numbers. This family is also your safety net: when an order request times out or returns a 5xx, the execution status is unknown, so query by clientOid before any retry — blind retries double-buy. Testing: no sandbox — needs a key with real order history. Reference: Get Order By OrderId · Get Trade History

Getting a KuCoin deposit address (and deposit history)

A person carefully places a coin onto a weighing deck between two open compartments, with chutes carrying coins in from one side and out the other

Docs say: GET /api/v3/deposit-addresses?currency=USDT&chain=trx returns your existing deposit addresses for a currency on a chain; if there are none, POST /api/v3/deposit-address/create makes one. GET /api/v1/deposits lists deposits (PROCESSING, SUCCESS, FAILURE, …). The list of chains per currency — with the chainId value these calls expect — comes from the public GET /api/v3/currencies/{currency}. Permission: General on the endpoint pages — notably not Withdrawal (the permission overview mentions deposit addresses under Withdrawal; if a 400007 appears here, that is the reason).

addrs = signed_get("/api/v3/deposit-addresses", currency="USDT", chain="trx")
if not addrs:   # none on this chain yet — create one, then read it back
    signed_post("/api/v3/deposit-address/create", currency="USDT", chain="trx")
    addrs = signed_get("/api/v3/deposit-addresses", currency="USDT", chain="trx")
a = addrs[0]
print(a["address"], a.get("memo") or "", a["chainName"], a["to"])

Gotchas: always pass chain explicitly, using the chainId from the currencies endpoint (trx, eth, bsc, sol, …) — the create call defaults to ERC20, and funds sent on a mismatched chain are the classic irreversible mistake. Some chains need a memo/tag (needTag in the currencies data; the response’s memo field): send funds without it and they may not be credited. Note the to field: an address can be created for the Funding account (main, the default) or straight into the Trading account (trade) — pick trade if the deposit is for a bot and you want to skip a transfer. Deposit history is page-numbered (currentPage, pageSize), unlike the order endpoints. Testing: none — production only; addresses are harmless to fetch. Reference: Get Deposit Address (V3) · Add Deposit Address (V3) · Get Deposit History

Withdrawing funds with the KuCoin API

Docs say: POST /api/v3/withdrawals submits a withdrawal: currency, toAddress, amount, withdrawType (ADDRESS for an on-chain address; UID, MAIL, PHONE for internal transfers to another KuCoin user), plus chain and, where required, memo. The response contains only a withdrawalId — completion is confirmed by polling GET /api/v1/withdrawals, never by the response itself. Permission: Withdrawal, which KuCoin only allows on a key with IP restriction enabled.

A safety frame before any code: withdrawal is the one permission that lets a leaked key move funds out of your account. A trading bot never needs it — keep it off the bot’s key (this is the same rule as in our KuCoin trading bot guide). If you automate withdrawals at all, use a dedicated key, IP-restricted, against a whitelisted address, and start with the smallest amount the asset allows.

result = signed_post("/api/v3/withdrawals", currency="USDT", chain="trx",
                     toAddress="TYourWhitelistedAddress", amount="10",
                     withdrawType="ADDRESS")
print(result)   # {'withdrawalId': '...'} — now poll GET /api/v1/withdrawals

Gotchas: withdrawals come only from the Funding (main) account115007 tells you to move funds there first (see the next recipe). There is no sandbox for this: the first real run is on production with real assets, which is why the smallest-amount rule is not optional. Read the per-chain minimum and fee before choosing a chain — GET /api/v1/withdrawals/quotas?currency=USDT&chain=trx returns withdrawMinSize, withdrawMinFee and your remaining 24-hour quota; on 2026-08-18 USDT on TRC20 cost 1.99 USDT with a 4 USDT minimum, on ERC20 5.5 with a 30 minimum. If the web account has “withdraw to whitelisted addresses only” switched on, any other address fails with 260325; that is an account setting, not an API parameter. Withdrawals are also suspended for 24 hours after security changes such as resetting 2FA or the trading password — not a bug in your request. Cancel a pending request with DELETE /api/v1/withdrawals/{withdrawalId} while its status is still PROCESSING. Testing: none — production only, smallest amount first. Reference: Withdraw (V3) · Get Withdrawal Quotas

Moving funds between KuCoin accounts (Funding, Trading, Futures)

Docs say: POST /api/v3/accounts/universal-transfer — “Flex Transfer” — moves an asset between your own accounts. type=INTERNAL with fromAccountType/toAccountType set to MAIN (Funding), TRADE (spot Trading), CONTRACT (Futures) or MARGIN; clientOid is required here. Permission: FlexTransfers (InnerTransfer in the API’s vocabulary) — a separate toggle from trading, IP restriction required, master-account key only. Funds stay inside your account, so this is far less dangerous than withdrawal, but it is still fund movement: leave it off a pure trading key.

moved = signed_post("/api/v3/accounts/universal-transfer",
                    clientOid=str(uuid.uuid4()), type="INTERNAL",
                    currency="USDT", amount="50",
                    fromAccountType="MAIN", toAccountType="TRADE")
print(moved)   # {'orderId': '...'}

Gotchas: this endpoint answers the recurring “why does my bot see a zero balance” — the money is sitting in the Funding account while your code reads trade. MAIN → TRADE is the pair most people actually need; TRADE → MAIN is the step before any withdrawal. Transfers are free. If you learned KuCoin from an older tutorial, you may know POST /api/v2/accounts/inner-transfer with lowercase account names — that endpoint is under Abandoned Endpoints and the docs point to Flex Transfer instead. Check what is movable first with GET /api/v1/accounts/transferable?currency=USDT&type=MAIN. clientOid is mandatory on this endpoint (unlike orders). When a transfer request times out, reuse the same clientOid on the retry and confirm the outcome by re-reading balances (or the transferable check above) rather than assuming a duplicate would be rejected — the docs promise no such guarantee here. Testing: none — production only, but the money never leaves your account. Reference: Flex Transfer · Get Transfer Quotas

KuCoin WebSocket streams: live prices in Python

A pipe carries a continuous line of colored beads through a heart-shaped valve before pouring them into a bowl

Docs say: WebSocket access is a three-step handshake. First POST /api/v1/bullet-public (no auth) returns a token and the server list (instanceServers: endpoint, pingInterval, pingTimeout). Then connect to {endpoint}?token={token}&connectId={your id} and wait for a welcome message. Only then subscribe: {"id": "…", "type": "subscribe", "topic": "/market/ticker:BTC-USDT", "response": true} — you get an ack, then messages. Send {"type": "ping"} every pingInterval (18 seconds when checked) or the server drops you. Permission: none for public topics.

# pip install websocket-client
import json, threading, uuid, requests, websocket

tok = requests.post("https://api.kucoin.com/api/v1/bullet-public").json()["data"]
server = tok["instanceServers"][0]
url = f"{server['endpoint']}?token={tok['token']}&connectId={uuid.uuid4().hex}"

def on_message(ws, raw):
    msg = json.loads(raw)
    if msg["type"] == "welcome":                          # only now may you subscribe
        ws.send(json.dumps({"id": "1", "type": "subscribe",
                            "topic": "/market/ticker:BTC-USDT", "response": True}))
    elif msg["type"] == "message":
        d = msg["data"]
        print(d["price"], d["bestBid"], d["bestAsk"])

def keepalive(ws):
    if ws.keep_running:
        ws.send(json.dumps({"id": "ping", "type": "ping"}))   # KuCoin's own JSON ping
        threading.Timer(server["pingInterval"] / 1000, keepalive, [ws]).start()

ws = websocket.WebSocketApp(url, on_message=on_message, on_open=keepalive)
ws.run_forever()

Gotchas: the endpoint URL is dynamic — take it from the token response every time rather than hardcoding wss://ws-api-spot.kucoin.com, which the docs say may change. A token lasts 24 hours and so does a connection: production code must fetch a new token and reconnect, and resubscribe after reconnecting. Two failure modes are quiet: a wrong symbol in a topic (btc-usdt, BTCUSDT, a typo) is acknowledged with ack and simply never delivers data — verified live — while a wrong topic name gets {"type":"error","code":404,"data":"topic does not exist"}; and a public-token connection asked for a private topic (with "privateChannel": true) gets code 403 "login is required" — without that flag the request is silently acked, like a wrong symbol. Up to 100 symbols can share one topic (/market/ticker:BTC-USDT,ETH-USDT); candles are /market/candles:BTC-USDT_1hour, best-bid/ask at 10 ms is /spotMarket/level1:BTC-USDT. Private streams — your order events (/spotMarket/tradeOrdersV2), balance changes (/account/balance) — use the same handshake with a token from the signed POST /api/v1/bullet-private (signed_post("/api/v1/bullet-private") with the helper) and "privateChannel": true on subscribe. Testing: public topics are keyless — runs as-is; private topics need a General key. Reference: WebSocket introduction · Get Public Token · Get Private Token

KuCoin API rate limits and common errors

Three dispenser tanks refill under a wall clock while balls pass along a conveyor below, one gate arm pressing down on the line as a worker watches with a clipboard

KuCoin meters requests by weight against resource pools that refill every 30 seconds. At VIP 0 the Spot pool (orders, cancels, order queries) is 4,000 weight per 30 s per account, the Management pool (balances, deposits, withdrawals, transfers, key info) 2,000, and the Public pool (prices, candles, symbols) 2,000 per 30 s per IP. Every endpoint page states its pool and weight — an order costs 1, a balance read 5, cancel-all across symbols 30 — and every response reports where you stand in gw-ratelimit-limit, gw-ratelimit-remaining and gw-ratelimit-reset (a countdown in milliseconds). Exceed a pool and you get HTTP 429 with code 429000; wait out the reset value rather than retrying blind, because KuCoin also returns 429000 under server overload — without the headers — and there the right response is a backoff with growing intervals (1 s, 2 s, 4 s — the pacing the official pages suggest for rate-limited reconnections). Futures has its own pool; UTA another. On the WebSocket side: at most 800 connections, 100 client messages per 10 seconds per connection, 400 topics per connection. The errors this guide’s recipes mention most:

CodeMeaningUsual fix
400001a required auth header is missingsend all five KC-API-* headers plus Content-Type: application/json
400002timestamp invalidmilliseconds, within about ±5 s of GET /api/v1/timestamp
400003key does not exist or site mismatchtypo, deleted/expired key, or a regional-site key on the global API — not a signing problem
400004passphrase errorsend base64(HMAC-SHA256(secret, passphrase)), never the plain text
400005signature errormessage must be timestamp + METHOD + path?rawquery + exact body; check offline against the vectors above
400006IP not on the whitelistadd your IP to the key
400007access denied — permission missingadd the permission the endpoint needs (Spot / Withdrawal / FlexTransfers)
400008V1/V2 key no longer supportedcreate a V3 key; header KC-API-KEY-VERSION: 3
400100parameter error / insufficient balanceround to increments, clear minFunds, move funds MAIN → TRADE
429000rate limitwait gw-ratelimit-reset ms; back off exponentially if the headers are absent
900001symbol does not existhyphenated uppercase BTC-USDT (arrives with HTTP 200)
102426duplicate clientOidnew UUID per order
115007withdrawals only from the Funding accounttransfer TRADE → MAIN first

Running your KuCoin API code as a bot

The recipes above are the pieces; a bot is what runs them unattended — a loop of read → decide → order, with reconnect logic for the 24-hour WebSocket lifetime, careful retries keyed on clientOid, and a kill switch. That infrastructure, from key setup to the first automated order, is the subject of our step-by-step KuCoin trading bot guide.

Conclusion and next steps

The pattern repeats across every operation: confirm the endpoint is the current one (not one under Abandoned Endpoints), check what permission the key really needs, keep the request minimal and byte-exact with what you signed, and read the error table before assuming your code is wrong. KuCoin’s absence of a sandbox changes the practice routine rather than the risk: public endpoints and the public WebSocket cost nothing to run, the order-test endpoint checks your signing and parameters, and the first real order can be worth 0.1 USDT. Everything that moves funds runs on production only — which is exactly where the smallest-amount rule and the permission hygiene in this guide matter most. And whichever route you take, remember the risk no checklist removes: trading cryptocurrencies involves substantial risk of loss — KuCoin’s own SDK disclaimer says as much in so many words — and the orders and transfers your code submits are your own responsibility. If you don’t have a KuCoin account yet, our KuCoin review covers registration, fees and features in detail.

All endpoints, parameters, weights and error texts verified against the official KuCoin documentation on 2026-08-18, with live checks where the endpoint permits. Specifications change — the reference links in each section point at the current official source.

Frequently asked questions

What is the KuCoin API?

An interface that lets your own programs talk to KuCoin directly: read prices, check balances, place orders and move funds with code instead of the app. KuCoin documents several product lines under one site; this guide covers the Classic spot API plus the account operations (deposits, withdrawals, transfers between your own accounts) and the spot WebSocket streams a typical account needs.

Is the KuCoin API free to use?

There is no separate charge for using the API and keys are self-service. Orders placed through the API pay the same spot trading fees as orders placed on the website — 0.1% maker and 0.1% taker at the base level — and public market data can be read without an account.

Does KuCoin have a testnet or sandbox for the API?

Not any more. KuCoin delisted its sandbox environment on 2023-07-10, and the old sandbox hosts no longer resolve. What exists instead: public endpoints and the public WebSocket need no key at all; POST /api/v1/hf/orders/test validates a real order request (signature, parameters) without sending it to the matching engine; and every USDT pair accepts orders worth as little as 0.1 USDT, so a first live order can be tiny.

What is the KuCoin API passphrase?

A third credential you choose yourself when creating the key, alongside the key and secret KuCoin generate. Current (V3) keys never send it in plain text: the KC-API-PASSPHRASE header carries base64(HMAC-SHA256(secret, passphrase)). Sending the raw passphrase produces error 400004. All three values are shown once at creation and cannot be recovered afterwards.

Which endpoint places a spot order — /api/v1/orders or /api/v1/hf/orders?

POST /api/v1/hf/orders. The older POST /api/v1/orders family is listed under 'Abandoned Endpoints' in the official documentation, which says to switch to the hf (high-frequency) endpoints. Many tutorials and older libraries still show the abandoned one.

Does this guide cover KuCoin Futures?

No. KuCoin Futures is a separate API tree with its own base URL (api-futures.kucoin.com), its own symbols (such as XBTUSDTM), positions, leverage and a separate rate-limit pool. Mixing futures values into spot code is a common source of confusion, so futures is out of scope here.

Why does KuCoin say 'The API key does not exist or site mismatch' (400003)?

The key is unknown to the host you are calling: a typo, a deleted or expired key, or a key created on one of KuCoin's regional sites (EU, Turkey, Australia, Thailand) being used against the global API. It says nothing about your signature — KuCoin checks that the key exists before it checks the signature, so 400005 (Signature error) can only appear once the key itself is valid.

Which Python library should I use for the KuCoin API?

The official SDK is kucoin-universal-sdk (pip install kucoin-universal-sdk); the older kucoin-python-sdk repository is archived. The examples here use plain requests so they run anywhere and show exactly what any SDK does underneath.