— Contents 18 sections
- 01 How the KuCoin API works
- 02 Before you start: keys, passphrase, no sandbox, and one auth helper
- 03 Getting the current price from the KuCoin API
- 04 Fetching KuCoin candlestick data (klines)
- 05 Reading a KuCoin symbol’s trading rules (increments and minimums)
- 06 Checking your KuCoin balance in Python
- 07 Placing a market order with the KuCoin API
- 08 Placing a limit order with the KuCoin API (and where stop orders fit)
- 09 Canceling orders and listing open KuCoin orders
- 10 Checking KuCoin order status and trade history
- 11 Getting a KuCoin deposit address (and deposit history)
- 12 Withdrawing funds with the KuCoin API
- 13 Moving funds between KuCoin accounts (Funding, Trading, Futures)
- 14 KuCoin WebSocket streams: live prices in Python
- 15 KuCoin API rate limits and common errors
- 16 Running your KuCoin API code as a bot
- 17 Conclusion and next steps
- 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
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:
| Section | Host and path style | What it is | In this guide |
|---|---|---|---|
| Spot Trading | api.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 Info | api.kucoin.com | balances, key info, deposits, withdrawals, transfers between your own accounts, fee rates | Yes |
| Spot / Margin WebSocket | dynamic host from a token call, currently wss://ws-api-spot.kucoin.com | live tickers, candles, order book, your order and balance events | Yes (spot topics) |
| Margin Trading | api.kucoin.com (own section) | borrowing to trade spot with leverage — same host, separate endpoints | No |
| Futures Trading | api-futures.kucoin.com (symbols like XBTUSDTM) | perpetuals with positions and leverage; own base URL, own rate-limit pool, own WebSocket token | No — separate tree |
| Earn, VIP Lending, Copy Trading, Convert, Affiliate, Broker | various (api-broker.kucoin.com for brokers) | product-specific APIs | No |
| UTA REST / WebSocket (“Pro API”) | api.kucoin.com + /api/ua/v1/…, wss://x-push-… (public) | the Unified Trading Account API — VIP 2+, beta | No |
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.comanddocs.kucoin.com/futuresnow 301-redirect todocs-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
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-keyreturns the key’spermissionlist,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_plainwith the secrettest_api_secretand expectsjie/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
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 uppercase — BTC-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
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
balance — holds 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
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)
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) account —
115007 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
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
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:
| Code | Meaning | Usual fix |
|---|---|---|
400001 | a required auth header is missing | send all five KC-API-* headers plus Content-Type: application/json |
400002 | timestamp invalid | milliseconds, within about ±5 s of GET /api/v1/timestamp |
400003 | key does not exist or site mismatch | typo, deleted/expired key, or a regional-site key on the global API — not a signing problem |
400004 | passphrase error | send base64(HMAC-SHA256(secret, passphrase)), never the plain text |
400005 | signature error | message must be timestamp + METHOD + path?rawquery + exact body; check offline against the vectors above |
400006 | IP not on the whitelist | add your IP to the key |
400007 | access denied — permission missing | add the permission the endpoint needs (Spot / Withdrawal / FlexTransfers) |
400008 | V1/V2 key no longer supported | create a V3 key; header KC-API-KEY-VERSION: 3 |
400100 | parameter error / insufficient balance | round to increments, clear minFunds, move funds MAIN → TRADE |
429000 | rate limit | wait gw-ratelimit-reset ms; back off exponentially if the headers are absent |
900001 | symbol does not exist | hyphenated uppercase BTC-USDT (arrives with HTTP 200) |
102426 | duplicate clientOid | new UUID per order |
115007 | withdrawals only from the Funding account | transfer 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.