# KuCoin sandbox in 2026: status and Python API testing

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

Is there a KuCoin sandbox? Status checked 5 November 2026, and how to test the KuCoin API in Python: signing, order test, rate limits and WebSocket drops.

**Short answer:** There is no working **KuCoin sandbox** as of 5 November 2026: KuCoin announced in June 2023 that it would suspend it on 10 July 2023 and return it “in a new form later”, and no relaunch was found. To test the KuCoin API in Python, use public endpoints, a local mock that checks signatures, and the Classic order test endpoint, which validates an order without matching it.

Ranking library docs still describe a sandbox at `sandbox.kucoin.com`, and KuCoin's archived demo code lists `openapi-sandbox.kucoin.com`; neither resolves. Facts below come from KuCoin's pages, repositories and PyPI, and from keyless tests, all on 5 November 2026 between 19:04 and 20:41 UTC.

This is a technical guide, not a recommendation of KuCoin or any strategy; automated trading can lose money, and availability depends on your jurisdiction and KuCoin's terms. For readers in Turkey: Article 99/A of Capital Markets Law No. 6362 treats activity by foreign crypto-asset platforms directed at Turkish residents as unauthorized. For architecture in general, see [crypto exchange API integration](https://fincodelabs.com/en/blog/crypto-exchange-api-integration).

## Is there a KuCoin sandbox or testnet in 2026?

No: the last official word is a 2023 suspension notice.

**KuCoin sandbox checks, 5 November 2026**

| Check | Result |
| --- | --- |
| KuCoin announcement, 29 June 2023 | Sandbox web and API services to be suspended on 10 July 2023 and to return “in a new form later” |
| Relaunch notice | None found; other sandbox notices date from 2019 to March 2023 |
| API documentation | No sandbox, testnet or demo mode in the docs index, introduction, authentication or rate limit pages, or the change log back to 1 December 2024 |
| DNS at 19:05 and 19:59 UTC | `openapi-sandbox.kucoin.com`, `sandbox.kucoin.com`, `api-sandbox-futures.kucoin.com`, `sandbox-docs.kucoin.com`: non-existent domain on the local resolver, 1.1.1.1 and 8.8.8.8 |
| Official SDK 1.3.2 | Production endpoints only |
| Archived official code | `kucoin-api-demo` still lists the sandbox URL; `kucoin-python-sdk` is unmaintained |

Code aimed at the old host fails at DNS lookup, before any key check.

## How to test the KuCoin API without a sandbox

Test in layers, from no key to real money.

**Testing layers for KuCoin API code**

| Layer | Needs | Shows |
| --- | --- | --- |
| Public REST and WebSocket | Nothing | Hosts, symbol rules, weights, message formats |
| Local mock | Synthetic keys | Signing, error handling, 429 waits, duplicate IDs, bad numbers |
| Order test endpoint | Real key with trade permission | That KuCoin accepts signature, permission and parameters |
| Smallest live order | Real key and funds | Everything, at real risk |

Spot's `POST /api/v1/hf/orders/test` (page modified 3 August 2026, weight 1) takes a real order's parameters and checks the signature without sending it to matching. Futures has `POST /api/v1/orders/test` (weight 2) on `api-futures.kucoin.com`, where the official SDK sends it; on `api.kucoin.com` that path is an abandoned spot endpoint. Futures carry high risk, because leverage magnifies losses. The index lists such endpoints for Classic accounts only, not UTA REST V2; both need a real key, so neither ran here. A live order is real and your own decision; for simulated fills, see the [paper trading API guide](https://fincodelabs.com/en/blog/paper-trading-api).

## KuCoin API Python: official SDK or plain requests

KuCoin's documentation points to one Python package, `kucoin-universal-sdk`; plain `requests` works too if you own the signing.

- **The SDK.** Its README calls it KuCoin's official SDK (1.3.2, released 15 September 2026, MIT license, marked Alpha on PyPI). On 5 November 2026 a keyless UTA order book call through it worked, and its signer matched the code below. By default it retries up to 3 times, including a GET or DELETE the server received (not a POST), and reconnects WebSockets every 5 seconds without limit.
- **UTA V2.** UTA REST V1 is now under Abandoned Endpoints; V2 paths start with `/api/ua/v2/`.

**Base URLs, introduction page modified 29 August 2026**

| Account | REST | Public WebSocket | Private WebSocket |
| --- | --- | --- | --- |
| UTA | `https://api.kucoin.com` | `wss://x-push-spot.kucoin.com`, `wss://x-push-futures.kucoin.com` | `wss://wsapi-push.kucoin.com`; orders `wss://wsapi.kucoin.com` |
| Classic | `https://api.kucoin.com`; futures `https://api-futures.kucoin.com` | `wss://ws-api-spot.kucoin.com`, `wss://ws-api-futures.kucoin.com`, with a token | Returned with a private token |

For a multi-exchange library, see the [CCXT Python guide](https://fincodelabs.com/en/blog/ccxt-python-trading-bot).

## KuCoin API signature in Python: passphrase, key version, encoding

Sign timestamp + method + path with query + body with HMAC-SHA256 and Base64, sign the passphrase the same way, and sign the query before URL encoding.

- **Headers.** The authentication page (modified 23 August 2026) requires six, all set in the code below; samples and SDK send key version 3.
- **A conflict.** The text says to sign the un-encoded URL, with a passphrase containing @ and # as its example, but the page's Python sample signs `urlencode(query_params)`. The SDK signs the raw form. Both agree when nothing needs encoding, so tests with `BTC-USDT` pass either way.

This client follows the text and the SDK. It ran on 5 November 2026 with Python 3.11, requests 2.31, synthetic keys and BTC-USDT steps from the public instrument endpoint.

```python
import base64, hashlib, hmac, json, threading, time, uuid
from decimal import Decimal, InvalidOperation
from http.server import BaseHTTPRequestHandler, HTTPServer
from urllib.parse import unquote, urlencode
import requests

class KuCoinError(Exception):
    pass

def sign(secret, msg):
    return base64.b64encode(hmac.new(secret.encode(), msg.encode(), hashlib.sha256).digest()).decode()

def wire(value, step):                  # finite, positive, multiple of step
    try:
        d, s = Decimal(str(value)), Decimal(step)
        ok = d.is_finite() and d > 0 and d == d.quantize(s) and d % s == 0
    except InvalidOperation:
        ok = False
    if not ok:
        raise ValueError(f"refused {repr(value)} (step {step})")
    return format(d, "f")

class KuCoinREST:
    def __init__(self, key, secret, passphrase, base="https://api.kucoin.com"):
        if not (key and secret and passphrase):          # before any network call
            raise ValueError("key, secret and passphrase are all required")
        self.key, self.secret, self.base, self.used = key, secret, base, set()
        self.passphrase = sign(secret, passphrase)

    def raw(self, path, params):        # the signed form: not URL-encoded
        return path + ("?" + "&".join(f"{k}={v}" for k, v in params.items()) if params else "")

    def call(self, method, path, params=None, body=None):
        data = json.dumps(body, separators=(",", ":")) if body else ""
        ts = str(int(time.time() * 1000))
        headers = {"KC-API-KEY": self.key, "KC-API-TIMESTAMP": ts, "KC-API-KEY-VERSION": "3",
                   "KC-API-SIGN": sign(self.secret, ts + method + self.raw(path, params) + data),
                   "KC-API-PASSPHRASE": self.passphrase, "Content-Type": "application/json"}
        url = self.base + path + ("?" + urlencode(params) if params else "")
        try:
            r = requests.request(method, url, headers=headers, data=data or None, timeout=10)
        except requests.RequestException as e:
            raise KuCoinError(f"no answer ({type(e).__name__}): outcome unknown") from None
        try:
            js = r.json()
        except ValueError:
            js = None
        js = js if isinstance(js, dict) else {}
        code = str(js.get("code", ""))
        if r.status_code == 429 or code == "429000":
            raise KuCoinError(f"rate limited: wait {r.headers.get('gw-ratelimit-reset', '?')} ms")
        if r.status_code == 200 and code == "200000":
            return js.get("data")
        raise KuCoinError(f"HTTP {r.status_code}, code {code or 'none'} {js.get('msg', '')}".strip())

    def order_test(self, symbol, side, price, size, tick, step, client_oid=""):
        oid = client_oid or uuid.uuid4().hex     # empty: new ID, not a duplicate
        body = {"clientOid": oid, "side": side, "symbol": symbol, "type": "limit",
                "price": wire(price, tick), "size": wire(size, step)}
        if oid in self.used:
            raise ValueError(f"clientOid {oid} already used")
        self.used.add(oid)
        return self.call("POST", "/api/v1/hf/orders/test", body=body)

class EncodedSigning(KuCoinREST):
    def raw(self, path, params):
        return path + ("?" + urlencode(params) if params else "")

SECRET, PASS = "synthetic-secret", "synthetic-pass"

class Mock(BaseHTTPRequestHandler):
    def do_POST(self):
        body = self.rfile.read(int(self.headers.get("Content-Length") or 0)).decode()
        prehash = self.headers["KC-API-TIMESTAMP"] + self.command + unquote(self.path) + body
        if self.path == "/limited":
            status, reply = 429, b"<html>Too Many Requests</html>"
        elif (self.headers["KC-API-SIGN"] == sign(SECRET, prehash)
              and self.headers["KC-API-PASSPHRASE"] == sign(SECRET, PASS)):
            status, reply = 200, b'{"code":"200000","data":{"orderId":"mock-1"}}'
        else:
            status, reply = 401, b'{"code":"400005","msg":"Signature error"}'
        self.send_response(status)
        self.send_header("gw-ratelimit-reset", "812")
        self.send_header("Content-Length", str(len(reply)))
        self.end_headers()
        self.wfile.write(reply)
    do_GET = do_POST
    log_message = lambda *args: None

srv = HTTPServer(("127.0.0.1", 0), Mock)
threading.Thread(target=srv.serve_forever, daemon=True).start()
local = f"http://127.0.0.1:{srv.server_port}"
kc = KuCoinREST("synthetic-key", SECRET, PASS, local)
enc = EncodedSigning("synthetic-key", SECRET, PASS, local)
odd = {"passphrase": "abc@#11"}
buy = lambda price, size, oid="": kc.order_test(
    "BTC-USDT", "buy", price, size, "0.1", "0.00000001", oid)
tests = [
    ("raw, odd value", lambda: kc.call("GET", "/x", odd)),
    ("encoded, odd value", lambda: enc.call("GET", "/x", odd)),
    ("order test", lambda: buy("1000.1", "0.0001")),
    ("ID reused", lambda: [buy("1000.1", "0.0001", "a1") for _ in range(2)]),
    ("float 1e400", lambda: buy(float("1e400"), "1")),
    ("size 1e-9999999", lambda: buy("1000.1", "1e-9999999")),
    ("429, HTML body", lambda: kc.call("GET", "/limited")),
    ("closed port", lambda: KuCoinREST("k", SECRET, PASS, "http://127.0.0.1:9").call("GET", "/x")),
]
for label, fn in tests:
    try:
        print(label, "->", fn())
    except (KuCoinError, ValueError) as e:
        print(label, "->", type(e).__name__ + ":", e)
srv.shutdown()
```

Output:

```
raw, odd value -> {'orderId': 'mock-1'}
encoded, odd value -> KuCoinError: HTTP 401, code 400005 Signature error
order test -> {'orderId': 'mock-1'}
ID reused -> ValueError: clientOid a1 already used
float 1e400 -> ValueError: refused inf (step 0.1)
size 1e-9999999 -> ValueError: refused '1e-9999999' (step 0.00000001)
429, HTML body -> KuCoinError: rate limited: wait 812 ms
closed port -> KuCoinError: no answer (ConnectionError): outcome unknown
```

The mock applies the documented rule; KuCoin's verdict needs a real key. After a timeout on a real order, look it up by `clientOid` before resending. For errors such as `400003`, see invalid API key, IP or permissions; for key setup, the [exchange API key guide](https://fincodelabs.com/en/blog/exchange-api-key-how-to).

## KuCoin API rate limit: pools, weights and the window

Private endpoints share a per-account pool and public ones a per-IP pool; the public pool measured on 5 November 2026 reset every 30 seconds, as the tables say, not the text.

**Rate limit pages, both modified 11 October 2026**

| Item | Text | Table |
| --- | --- | --- |
| Window | 1 second; HTTP 429, code `429000`, once it is used up | Public 2,000/30s; Classic spot 4,000/30s at VIP0; UTA trading 300/s at VIP0 |
| VIP5 example | 700 per second | UTA trading 900/s |

At 19:09 and 20:02 UTC, keyless public calls carried `gw-ratelimit-limit: 2000`, with `gw-ratelimit-reset` counting down from 30,000 ms; quota fell by 5, 20, 20 and 50 for UTA order book depths 20, 21, 100 and 101, as documented (public since 2 November 2026). Private windows need a key and were not measured; read the three headers on every response. A `429000` without them means server overload, outside your quota. KuCoin's HTTP status page gives 403 for a breached limit, so test the code too.

> **Hypothetical example.** Polling the UTA spot order book every second is 30 calls per window: 600 of the 2,000 public weight at depth 100 (weight 20), but 1,500 at depth 101 (weight 50), leaving 500 for everything else on that IP.

## KuCoin WebSocket disconnects: ping, auth and reconnect

KuCoin drops connections on purpose, so ping on its interval, check the auth reply and reconnect after a pause.

- **Planned drops.** Load balancing may close UTA or Classic connections, at most once or twice a week by KuCoin's estimate (pages modified 11 October 2026); Classic tokens and connections last 24 hours.
- **Ping.** UTA welcome messages and the Classic token response gave `pingInterval` 18000 ms (`pingTimeout` 10000 on private and Classic connections). Pinging more than once a second drops the connection.
- **Auth.** On `wsapi-push.kucoin.com`, send `op: auth` with an HMAC of timestamp + `POST/api/websocket/users/verify` as `kc-api-sign` (step 3 of the page mislabels it `kc-api-passphrase`) plus the signed passphrase. A synthetic key at 19:10 and 20:03 UTC got `"result":false,"message":"auth failed"`, and the socket stayed open for at least 10 more seconds, so check the reply.
- **Format drift.** All UTA frames arrived as binary frames holding JSON text; the subscribe acknowledgment had a boolean `true` where the docs show a string, and the pong carried `ts` in nanoseconds where they show `timestamp`.
- **Reconnect.** Wait 3 to 5 seconds, as the rate limit page asks, then resubscribe; each IP may open 300 new spot or private connections per 5 minutes.

Heartbeat code is in the [WebSocket reconnect guide](https://fincodelabs.com/en/blog/websocket-heartbeat-reconnect). For integration work in general, see [exchange API integration](https://fincodelabs.com/en/services/exchange-api-integration); naming KuCoin here does not mean Fincode integrates it or has built on it.

## Frequently asked questions

### Does KuCoin have a testnet or sandbox in 2026?

Not as of 5 November 2026: KuCoin announced its suspension from 10 July 2023, and the current API documentation names no replacement.

### What was the KuCoin sandbox URL?

KuCoin's archived demo repository lists https://openapi-sandbox.kucoin.com. It and sandbox.kucoin.com did not resolve on 5 November 2026.

### Can I get a KuCoin testnet API key?

No. Keys created on kucoin.com are production keys, so keep withdrawal permission off, use the order test endpoint first and treat any live order as real.

### Why does my KuCoin signature fail only on some requests?

Check whether the query was URL-encoded before signing: KuCoin's text and SDK sign the un-encoded form, its Python sample the encoded one, and only values with characters such as @ or # tell them apart.

## Sources

- [KuCoin: KuCoin Will Delist the Sandbox Mode (announcement, 29 June 2023)](https://www.kucoin.com/announcement/kucoin-will-delist-the-sandbox-mode-0629)
- [KuCoin API: Introduction, UTA and Classic base URLs (modified 29 August 2026)](https://www.kucoin.com/docs-new/introduction-uta)
- [KuCoin API: Authentication (modified 23 August 2026)](https://www.kucoin.com/docs-new/authentication)
- [KuCoin API: Rate Limit Rule (UTA), modified 11 October 2026](https://www.kucoin.com/docs-new/rate-limit-rule-uta)
- [KuCoin API: Rate Limit Rule (Classic), modified 11 October 2026](https://www.kucoin.com/docs-new/rate-limit-rule-classic)
- [KuCoin API: Change Log (read 5 November 2026)](https://www.kucoin.com/docs-new/change-log)
- [KuCoin API: Add Order Test, Classic spot (modified 3 August 2026)](https://www.kucoin.com/docs-new/rest/spot-trading/orders/add-order-test)
- [KuCoin API: UTA WebSocket V2 introduction (modified 11 October 2026)](https://www.kucoin.com/docs-new/websocket-api/introduction)
- [KuCoin API: Classic WebSocket base info (modified 28 January 2026)](https://www.kucoin.com/docs-new/websocket-api/base-info/introduction)
- [KuCoin API: Spot error codes](https://www.kucoin.com/docs-new/error-code/spot)
- [GitHub: Kucoin/kucoin-universal-sdk (README, version 1.3.2)](https://github.com/Kucoin/kucoin-universal-sdk)
- [PyPI: kucoin-universal-sdk release history](https://pypi.org/project/kucoin-universal-sdk/)
- [GitHub: Kucoin/kucoin-python-sdk (archived, deprecation notice)](https://github.com/Kucoin/kucoin-python-sdk)
- [GitHub: Kucoin/kucoin-api-demo (archived; sandbox base URL in README)](https://github.com/Kucoin/kucoin-api-demo)
- [Capital Markets Law No. 6362, Article 99/A (Turkish, consolidated text)](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/kucoin-sandbox-api-python

Cite as: Fincode, "KuCoin sandbox in 2026: status and Python API testing", updated November 6, 2026, https://fincodelabs.com/en/blog/kucoin-sandbox-api-python
