# Crypto.com API: Exchange v1 keys, signing and limits

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

Crypto.com Exchange API v1, read 6 Nov 2026: API key setup, the exact HMAC signing string, rate limits that conflict, the UAT sandbox and tested Python.

**Short answer:** The documented **Crypto.com API** for trading is the Crypto.com Exchange API v1: REST at `api.crypto.com/exchange/v1`, WebSocket at `stream.crypto.com`, and keys created on the Exchange website, not in the App. Private calls carry an HMAC-SHA256 signature over the method, id, API key, sorted parameters and a millisecond nonce. As read on 6 November 2026, the developer documentation and the help center state different rate limits, and the UAT sandbox is invitation-only.

This guide covers the Crypto.com Exchange API v1 as read on official pages on 6 November 2026, with code run that day on public endpoints, a fake session and a local mock. No real order was placed, because that needs a real key.

This is technical education, not a recommendation of Crypto.com or any strategy, and automated trading can lose money. Availability depends on your jurisdiction and the platform'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.

## Crypto.com API: the App, the Exchange and which docs are current

The documented trading API belongs to the Exchange; the App's Agent Key is separate, and the v2.1 documentation redirects to Exchange v1.

**Crypto.com keys, from official pages read 6 November 2026**

| Product | Key created in | Documented |
| --- | --- | --- |
| Exchange API v1 | Exchange website, API Management | REST, WebSocket and FIX reference |
| App Agent Key | App: More, then Agent Key | Setup for OpenClaw, an AI agent tool: 30-day default expiry, weekly trading budget, no endpoint reference |

Despite the numbering, Exchange v1 replaced v2.1; per its introduction it trades spot, derivatives and margin. On 6 November 2026, old `/v2/public/get-ticker` calls still answered while `/v2/public/get-instruments` returned HTTP 404. The live list held 552 spot pairs, 417 perpetual swaps and 10 dated futures; derivatives carry a high risk of rapid loss, and availability varies by jurisdiction.

## How to create an API key on Crypto.com Exchange

Create it on the Exchange website under Manage Account, then API Management; a new key can only read until you enable trading.

1. Open the profile menu, then Manage Account and API Management, and create a key with a label and your 2FA code.
2. Copy the secret at once: the help center says it is shown only at creation.
3. Enable trading only if the software places orders, and leave withdrawal off.

The official pages disagree on IP restrictions. The developer reference calls a whitelist optional and names the menu User Center, then API; the help center article, updated 20 July 2026, requires one once trading or withdrawal is enabled. Plan for a fixed outbound IP address either way. Storage and rotation are covered in [API key security for trading bots](https://fincodelabs.com/en/blog/api-key-security-trading-bots).

## Crypto.com Exchange API signing: the exact string

Join method, id, API key, the parameter string and the nonce, HMAC-SHA256 the result with your secret, and send the hex digest as `sig`.

- **Parameter string:** keys sorted ascending, each followed by its value, no separators; lists in order; nested objects alike.
- **Numbers** in `params` must be strings, such as `"12.34"`.
- **Nonce:** UTC milliseconds, within 60 seconds of server time.

> **The docs' own inputs.** For `private/get-order-detail` with id 11, key `token`, `order_id` 53287421324 and nonce 1587846358253, the string to sign is `private/get-order-detail11tokenorder_id532874213241587846358253`, and with secret `secretKey` the signature starts `02ef0a52`.

**Official samples on the same inputs (JavaScript and Python run 6 November 2026, Java read)**

| Input | JavaScript | Java | Python |
| --- | --- | --- | --- |
| `exec_inst: ["POST_ONLY"]` (the docs' create-order example) | `exec_instPOST_ONLY` | `exec_instPOST_ONLY` | `TypeError` |
| Boolean `true` | `true` | `true` | `True` |
| Nested object | Keys and values | Keys and values | Python's printed dict |

The JavaScript sample run in Node, with Node's HMAC in place of crypto-js, and the client below both sign the create-order example as `6c17b745917702f0`, so follow JavaScript and Java. That example also pairs POST_ONLY with FILL_OR_KILL, which its own schema forbids. Other schemes are compared in API signature generation, and Fincode's [HMAC signature generator](https://fincodelabs.com/en/tools/hmac-signature-generator) shows a signed string byte by byte.

## Crypto.com API rate limits and error codes

The developer reference limits each method per API key, mostly per 100 milliseconds, while the help center states 10 calls per second; build for the lower figure.

**Developer reference limits, read 6 November 2026: per method and API key unless noted**

| Calls | Limit |
| --- | --- |
| `private/create-order`, `cancel-order`, `cancel-all-orders` | 15 per 100 ms each |
| `private/get-order-detail` | 30 per 100 ms |
| `private/get-trades`, `get-order-history` | 1 per second |
| Other private methods | 3 per 100 ms each |
| Public REST market data | 100 per second each, per IP |
| WebSocket User API, Market Data | 150 and 100 per second, scope not stated |

The help center, updated 20 July 2026, instead calls 10 calls per URL per second its only limit, per account for user APIs and per IP for market APIs. No response checked on 6 November 2026, public or private, carried a rate-limit header, so count client-side with a [token bucket rate limiter](https://fincodelabs.com/en/blog/token-bucket-rate-limiter-python).

**Errors to handle first, with live responses from 6 November 2026**

| HTTP | Code | Meaning | Seen live |
| ---: | ---: | --- | --- |
| 401 | 40101 | Key or signature wrong | UAT, made-up key: Authentication failure |
| 400 | 40102 | Nonce over 60 seconds off | UAT, stale nonce: Invalid nonce |
| 400 | 40004 | Argument missing or invalid | Production, unknown instrument |
| 429 | 42901 | Rate limit exceeded | Not provoked |

Live error bodies held only `code` and `message`, with numeric codes where examples show strings. The nonce check ran first: a made-up key with a 120-second-old nonce got 40102, so clock errors can hide key errors.

## Crypto.com UAT sandbox: who gets access and what it shows

The UAT sandbox at `uat-api.3ona.co` is by invitation for institutional accounts, according to the help center, yet its public endpoints answered without an account on 6 November 2026.

**Production and UAT public data, 6 November 2026**

| Check | Production | UAT |
| --- | --- | --- |
| Instruments | 979 | 1,284 |
| `product_type`, added 21 May 2026 | On 970 of 979 | On 228 of 1,284 |
| BTC_USDT ticker | Trading normally | 24-hour high, low and last trade all 80,064.00 on zero volume, while the best bid moved |

UAT prices therefore say nothing about fills. Without an invitation, test signing against a local mock and trading logic in a [paper trading](https://fincodelabs.com/en/blog/paper-trading-api) layer.

## A tested Python client for the Crypto.com Exchange API

The client signs like the JavaScript and Java samples, sends numbers as strings, rejects ambiguous input and separates authentication from network errors.

Run on 6 November 2026 with Python 3.11 and `requests` 2.31: public calls reached production without a key, and order calls went to a fake session returning the 401 body UAT sent for a made-up key.

```python
import hashlib, hmac, itertools, re, time, uuid
from decimal import Decimal
import requests

BASE = "https://api.crypto.com/exchange/v1"  # UAT: https://uat-api.3ona.co/exchange/v1
SESSION = requests.Session()
PLAIN = re.compile(r"[0-9]{1,20}(\.[0-9]{1,18})?")
ids, sent = itertools.count(1), set()

class ApiError(Exception): pass
class AuthError(ApiError): pass

def param_str(obj, level=0):
    if isinstance(obj, (dict, list)) and level >= 3:
        raise ValueError("nested deeper than 3 levels: the doc samples disagree")
    if isinstance(obj, dict):
        return "".join(k + param_str(obj[k], level + 1) for k in sorted(obj))
    if isinstance(obj, list):
        return "".join(param_str(v, level + 1) for v in obj)
    if isinstance(obj, bool):
        return "true" if obj else "false"
    if obj is None:
        return "null"
    if isinstance(obj, str):
        return obj
    raise TypeError(f"send numbers as strings, not {repr(obj)}")

def sign(method, req_id, key, params, nonce, secret):
    text = f"{method}{req_id}{key}{param_str(params)}{nonce}"
    return hmac.new(secret.encode(), text.encode(), hashlib.sha256).hexdigest()

def call(method, params=None, key="", secret=""):
    params = params or {}
    if method.startswith("private/"):
        if not (key and secret):                 # before anything is sent
            raise AuthError("API key and secret are required")
        body = {"id": next(ids), "method": method, "api_key": key,
                "params": params, "nonce": int(time.time() * 1000)}
        body["sig"] = sign(method, body["id"], key, params, body["nonce"], secret)
        r = SESSION.post(f"{BASE}/{method}", json=body, timeout=10)
    else:
        r = SESSION.get(f"{BASE}/{method}", params=params, timeout=10)
    try:
        data = r.json()
        code = int(data.get("code", -1))         # docs show "0", the live API sends 0
    except (ValueError, AttributeError, TypeError):
        raise ApiError(f"HTTP {r.status_code}: unexpected body") from None
    if r.status_code == 200 and code == 0:
        return data.get("result", {})
    auth = r.status_code == 401 or code in (40101, 40103, 40104)
    raise (AuthError if auth else ApiError)(f"HTTP {r.status_code} code {code}: {data.get('message', '')}")

def instrument(symbol):
    rows = [x for x in call("public/get-instruments").get("data", []) if x.get("symbol") == symbol]
    if len(rows) == 1:
        return rows[0]
    raise ApiError(f"{len(rows)} instruments named {symbol}")

def to_wire(value, tick):
    try:
        if PLAIN.fullmatch(value) and Decimal(value) > 0 and Decimal(value) % Decimal(tick) == 0:
            return value
    except (TypeError, ArithmeticError):     # non-strings, too many digits
        pass
    raise ValueError(f"rejected {repr(value)} for tick {tick}")

def limit_order(inst, side, qty, price, key, secret, client_oid=""):
    if not (key and secret):
        raise AuthError("API key and secret are required")
    client_oid = client_oid or str(uuid.uuid4())  # empty means new, never a duplicate
    if len(client_oid) > 36 or client_oid in sent:
        raise ValueError(f"client_oid {client_oid} is too long or already used")
    params = {"instrument_name": inst["symbol"], "side": side, "type": "LIMIT",
              "price": to_wire(price, inst["price_tick_size"]),
              "quantity": to_wire(qty, inst["qty_tick_size"]),
              "client_oid": client_oid, "time_in_force": "GOOD_TILL_CANCEL"}
    sent.add(client_oid)       # kept on failure: look the order up before resending
    return call("private/create-order", params, key, secret)

# --- demo
inst = instrument("BTC_USDT")
print("ticks:", inst["price_tick_size"], inst["qty_tick_size"])
for v in ("1e400", "1e-400", 0.1, "75000.005"):
    try:
        to_wire(v, inst["price_tick_size"])
    except ValueError as err:
        print(err)
try:
    call("public/get-book", {"instrument_name": "NOPE_USDT", "depth": "10"})
except ApiError as err:
    print("live:", err)
doc = {"instrument_name": "BTCUSD-PERP", "side": "SELL", "type": "LIMIT", "price": "50000.5",
       "quantity": "1", "client_oid": "c5f682ed-7108-4f1c-b755-972fcdca0f02",
       "exec_inst": ["POST_ONLY"], "time_in_force": "FILL_OR_KILL"}
print("doc example:", sign("private/create-order", "1", "token", doc, "1610905028000", "secretKey")[:16])

class FakeSession:
    posts, status_code = 0, 401
    def post(self, *args, **kwargs):
        FakeSession.posts += 1
        return self
    def json(self):
        return {"code": 40101, "message": "Authentication failure"}
SESSION = FakeSession()
for key, oid in (("", ""), ("k", ""), ("k", ""), ("k", "a1"), ("k", "a1")):
    try:
        limit_order(inst, "BUY", "0.00010", "50000.00", key, "s", oid)
    except (ApiError, ValueError) as err:
        print(f"key={repr(key)} oid={repr(oid)} posts={FakeSession.posts}:", err)
```

Output:

```
ticks: 0.01 0.00001
rejected '1e400' for tick 0.01
rejected '1e-400' for tick 0.01
rejected 0.1 for tick 0.01
rejected '75000.005' for tick 0.01
live: HTTP 400 code 40004: Invalid instrument_name
doc example: 6c17b745917702f0
key='' oid='' posts=0: API key and secret are required
key='k' oid='' posts=1: HTTP 401 code 40101: Authentication failure
key='k' oid='' posts=2: HTTP 401 code 40101: Authentication failure
key='k' oid='a1' posts=3: HTTP 401 code 40101: Authentication failure
key='k' oid='a1' posts=3: client_oid a1 is too long or already used
```

A local mock that verifies like the docs' Java sample accepted this client's orders, lists and booleans, and returned 40101 for a wrong secret and 40102 for a clock 61 seconds slow. A refused connection raised a `requests` error and kept its order ID reserved.

## Crypto.com WebSocket API: auth, heartbeat and order updates

Use `wss://stream.crypto.com/exchange/v1/user` for orders after `public/auth`, and `/exchange/v1/market` on the same host for public data without a key.

- **Wait a second.** Limits are pro-rated from the calendar second the connection opened; the docs advise a 1-second pause before the first request.
- **Heartbeats.** Per the docs, the server sends `public/heartbeat` every 30 seconds and expects `public/respond-heartbeat` with the same id within 5. In two tests on 6 November 2026, a market-data connection that never replied was closed with code 1000 about 63 seconds after the first heartbeat it ignored; do not rely on that slack.
- **Orders are asynchronous.** `private/create-order` confirms the request, `user.order` the order. Without `client_oid` the nonce becomes the ID, so set your own (36 characters at most); the REST and WebSocket pages do not say how long an ID stays reserved.
- **Cancel on disconnect.** `private/set-cancel-on-disconnect` takes scope CONNECTION, and its page says nothing more, so test it first.
- **Subscriptions.** Error 40107 caps them per session; the limits section that the change log of 11 December 2023 cites is not in the current docs.

See [WebSocket heartbeat and reconnect](https://fincodelabs.com/en/blog/websocket-heartbeat-reconnect), [order state machine](https://fincodelabs.com/en/blog/order-state-machine) and [client order ID](https://fincodelabs.com/en/blog/client-order-id). For integration work in general, see [Fincode's exchange API integration page](https://fincodelabs.com/en/services/exchange-api-integration); naming Crypto.com here does not mean Fincode integrates it or has built on it.

## Frequently asked questions

### Does the Crypto.com App have an API?

Its help pages describe an Agent Key for an AI agent tool, with no endpoint reference. The documented trading API is the Exchange API v1.

### Is there a Crypto.com API sandbox?

UAT at uat-api.3ona.co, by invitation for institutional accounts according to the help center. Its public endpoints answered without an account on 6 November 2026.

### Why does my Crypto.com API signature fail?

Common causes are JSON numbers instead of strings, unsorted keys, booleans written as True and the wrong secret. A stale nonce returns 40102 instead, and an unlisted IP 40103.

### Where is the Crypto.com Exchange API documentation?

At exchange-developer.crypto.com/exchange/v1. Old exchange-docs.crypto.com links and the Spot API v2.1 page lead there.

## Sources

- [Crypto.com Exchange API v1: Common API Reference (endpoints, rate limits, signing, error codes)](https://exchange-developer.crypto.com/exchange/v1/docs/api/rest-common-api-reference)
- [Crypto.com Exchange API v1: REST API introduction (API versions)](https://exchange-developer.crypto.com/exchange/v1/docs/api/rest-introduction)
- [Crypto.com Exchange API v1: Change Log (latest entry 21 May 2026)](https://exchange-developer.crypto.com/exchange/v1/docs/api/rest-change-log)
- [Crypto.com Exchange API v1: private/create-order](https://exchange-developer.crypto.com/exchange/v1/docs/api/rest/private-create-order-dma)
- [Crypto.com Exchange API v1: public/get-instruments](https://exchange-developer.crypto.com/exchange/v1/docs/api/rest/public-get-instruments)
- [Crypto.com Exchange API v1: public/get-tickers (ticker field definitions)](https://exchange-developer.crypto.com/exchange/v1/docs/api/rest/public-get-tickers)
- [Crypto.com Exchange API v1: public/heartbeat (WebSocket)](https://exchange-developer.crypto.com/exchange/v1/docs/api/websocket/ws-client-commands-heartbeat-request)
- [Crypto.com Exchange API v1: private/set-cancel-on-disconnect (WebSocket)](https://exchange-developer.crypto.com/exchange/v1/docs/api/websocket/ws-client-commands-set-cancel-on-disconnect-request)
- [Crypto.com Help Center: API (Exchange key creation, limits, UAT access; updated 20 July 2026)](https://help.crypto.com/en/articles/3511424-api)
- [Crypto.com Help Center: Agent Key, Getting Started (App; updated 17 March 2026)](https://help.crypto.com/en/articles/13843782-getting-started)
- [Crypto.com Help Center: Agent Key, API Key Management (App; updated 27 February 2026)](https://help.crypto.com/en/articles/13843786-api-key-management)
- [Crypto.com Spot API v2.1 documentation (now redirects to Exchange v1)](https://exchange-docs.crypto.com/spot/index.html)
- [Crypto.com Exchange public/get-instruments, live production response](https://api.crypto.com/exchange/v1/public/get-instruments)
- [Capital Markets Law No. 6362, consolidated text, Article 99/A (Turkish)](https://www.mevzuat.gov.tr/mevzuatmetin/1.5.6362.pdf)

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

---

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

Cite as: Fincode, "Crypto.com API: Exchange v1 keys, signing and limits", updated November 6, 2026, https://fincodelabs.com/en/blog/crypto-com-exchange-api
