As a team of blockchain engineers, we often receive requests to integrate trading bots with KuCoin. At first glance, the API looks standard—REST and WebSocket. But practice shows that developers stumble on v2 authentication (passphrase in base64), dynamic WebSocket URL, and rate limits. Once, a client lost 2 ETH due to incorrect timestamp—orders were delayed and entered the stale zone. Let's break down how to build a fault-tolerant bot that saves money and nerves. Switching from market to limit orders can save up to 30% in fees—in one case, over $300 monthly. And proper WebSocket subscription reduces latency by 80%, indirectly saving an additional 0.5% in slippage.
How KuCoin Authentication Works and Why It's Non-Standard
KuCoin uses HMAC-SHA256 signatures. Key nuance: the passphrase is also signed and base64-encoded. Below is a working class for generating headers.
import hmac
import hashlib
import base64
import time
import json
import httpx
class KuCoinClient:
BASE_URL = "https://api.kucoin.com"
FUTURES_URL = "https://api-futures.kucoin.com"
def __init__(self, api_key: str, api_secret: str, passphrase: str):
self.api_key = api_key
self.api_secret = api_secret
# KuCoin v2 signature: passphrase is also signed
self.passphrase = base64.b64encode(
hmac.new(api_secret.encode(), passphrase.encode(), hashlib.sha256).digest()
).decode()
def _sign(self, timestamp: str, method: str, endpoint: str, body: str = "") -> str:
str_to_sign = timestamp + method.upper() + endpoint + body
return base64.b64encode(
hmac.new(self.api_secret.encode(), str_to_sign.encode(), hashlib.sha256).digest()
).decode()
def _headers(self, method: str, endpoint: str, body: str = "") -> dict:
timestamp = str(int(time.time() * 1000))
return {
"KC-API-KEY": self.api_key,
"KC-API-SIGN": self._sign(timestamp, method, endpoint, body),
"KC-API-TIMESTAMP": timestamp,
"KC-API-PASSPHRASE": self.passphrase,
"KC-API-KEY-VERSION": "2",
"Content-Type": "application/json"
}
According to KuCoin documentation, the timestamp must differ from the server's by no more than 5 seconds—otherwise 401000. In our projects, we synchronize clocks via NTP and read server time from the KC-API-TIMESTAMP response header on each request.
Step-by-step API key creation guide
- Log in to your KuCoin account, go to
Settings→API. - Click Create API Key.
- Select version 2.
- Set a passphrase (minimum 8 characters).
- Save the API key, secret, and passphrase—they are shown only once.
- Use the class above in your code, passing the keys.
What's Faster: REST or WebSocket?
| Criterion | REST API | WebSocket API |
|---|---|---|
| Price latency | 200–500 ms (polling) | 10–50 ms (push) |
| Server load | High (polling) | Low (subscription) |
| Required connections | One-off HTTP | Persistent WS |
| Rate limits | 30 requests/s | 100 connections per account |
For high-frequency strategies, WebSocket is 5–10 times faster than REST. However, WebSocket connection stability requires implementing reconnection and keepalive logic.
How to Place Orders via REST
async def place_order(
self,
symbol: str, # 'BTC-USDT'
side: str, # 'buy' or 'sell'
order_type: str, # 'limit' or 'market'
size: str = None,
price: str = None,
funds: str = None # for market buy by quote
) -> dict:
endpoint = "/api/v1/orders"
payload = {
"clientOid": str(int(time.time() * 1000)), # unique client ID
"symbol": symbol,
"side": side,
"type": order_type
}
if order_type == "limit":
payload["size"] = size
payload["price"] = price
elif side == "buy" and funds:
payload["funds"] = funds # buy for $X USDT
else:
payload["size"] = size
body = json.dumps(payload)
async with httpx.AsyncClient() as client:
response = await client.post(
f"{self.BASE_URL}{endpoint}",
content=body,
headers=self._headers("POST", endpoint, body)
)
result = response.json()
if result.get("code") != "200000":
raise KuCoinError(f"Order error: {result.get('msg')}")
return result["data"]
async def get_accounts(self, currency: str = None) -> list:
endpoint = "/api/v1/accounts"
if currency:
endpoint += f"?currency={currency}"
async with httpx.AsyncClient() as client:
response = await client.get(
f"{self.BASE_URL}{endpoint}",
headers=self._headers("GET", endpoint)
)
return response.json().get("data", [])
Note: clientOid must be unique for each order. We generate it based on a timestamp and random string to avoid collisions on resubmission. This is critical for strategies where we replace orders when the price changes.
Why Does WebSocket Require a Dynamic URL?
KuCoin does not publish a static WebSocket URL—it must be requested. This improves security: the token expires after 24 hours. Our client fetches the endpoint, connects, and maintains keepalive.
async def get_ws_endpoint(self, private: bool = False) -> dict:
endpoint = "/api/v1/bullet-private" if private else "/api/v1/bullet-public"
method = "POST" if private else "POST"
headers = self._headers(method, endpoint) if private else {"Content-Type": "application/json"}
async with httpx.AsyncClient() as client:
response = await client.post(
f"{self.BASE_URL}{endpoint}",
headers=headers
)
data = response.json()["data"]
server = data["instanceServers"][0]
token = data["token"]
ws_url = f"{server['endpoint']}?token={token}&connectId={int(time.time()*1000)}"
ping_interval = server["pingInterval"] / 1000 # in seconds
return {"url": ws_url, "ping_interval": ping_interval}
async def subscribe_ticker(self, symbols: list[str]):
ws_data = await self.get_ws_endpoint(private=False)
async with websockets.connect(ws_data["url"]) as ws:
await ws.send(json.dumps({
"id": str(int(time.time() * 1000)),
"type": "subscribe",
"topic": f"/market/ticker:{','.join(symbols)}",
"privateChannel": False,
"response": True
}))
async def keepalive():
while True:
await asyncio.sleep(ws_data["ping_interval"])
await ws.send(json.dumps({"id": "ping", "type": "ping"}))
asyncio.create_task(keepalive())
async for message in ws:
data = json.loads(message)
if data["type"] == "message" and "data" in data:
await self.on_ticker(data["data"])
A typical WebSocket issue: connection drop without notification. During prolonged inactivity, KuCoin may close the socket without sending a close frame. Our error handler automatically reconnects with exponential backoff (1s, 2s, 4s... up to 30s). This covers 99.9% of cases.
What Errors Are Most Common?
| Error | Cause | Solution |
|---|---|---|
code: "400003" |
Invalid signature | Check HMAC algorithm, passphrase, timestamp |
code: "401000" |
Expired timestamp | Difference from server must be ≤5 seconds |
code: "429000" |
Rate limit exceeded | Introduce delays, use rate-limit headers |
KuCoin API returns code: "200000" on success (not HTTP status). Always check this field.
KuCoin Futures API Specifics
KuCoin provides a separate domain for the futures API: https://api-futures.kucoin.com. Authentication is identical to the spot version, but endpoints differ. For algorithmic trading, our crypto bot uses advanced order types. Setting correct leverage and marginType (isolated or cross) is crucial. The funding rate updates every 8 hours—available via /api/v1/funding-rate/{symbol}/current. Monitoring the funding rate helps avoid unwanted debits when holding a position through the settlement period.
What's Included in a Turnkey Solution
- Bot architecture design (strategy, risk management, spot/futures choice)
- Client implementation with rate limit handling, reconnections, and full error logging
- Testing on the sandbox environment (full scenario coverage: orders, cancellations, partial fills)
- Deployment on your server or in the cloud (Docker, systemd, monitoring via Grafana)
- API and operations documentation: how to restart, how to change strategies
- 30-day post-launch support: bug fixes, consultations
We guarantee stability: our team has 7+ years of experience in crypto-trading. To launch a bot in production, contact us—we'll prepare the architecture in 2 days and help you avoid common integration mistakes. Get a consultation for your project—we'll discuss strategy, risks, and technical details.







