Getting started with CCXT
Figures on this page are as of 2026-09. Fees, limits and margin tiers change — check the venue's own docs before acting on a number.
CCXT gives you one interface across many exchanges. The usual pitch is that it saves you from writing an adapter per venue, which is true but undersells it.
The more valuable thing is the unified error hierarchy, because that is what lets you write correct handling once instead of learning each venue’s error codes before you can be safe.
What it removes
The per-venue work that disappears is mostly request signing, and it is not trivial. Each major venue does it differently, and none of the differences are cosmetic:
| Venue | Pre-hash string |
|---|---|
| Binance | Query string concatenated with the HTTP body |
| Bybit | timestamp + api_key + recv_window + queryString | body |
| OKX | timestamp + method + requestPath + body |
Plus header names that share no convention — X-MBX-APIKEY, versus Bybit’s four
X-BAPI-* headers, versus OKX’s four OK-ACCESS-* headers including a
passphrase that only OKX has.
Getting all of that right per venue is where integration time goes. CCXT is where it stops being your problem.
The error hierarchy is the point
The top-level split:
BaseError
├── ExchangeError — the exchange understood you and refused
└── OperationFailed — the outcome is unknown
ExchangeError means the request was evaluated and rejected. Fix the request;
retrying unchanged will fail again.
OperationFailed — covering NetworkError, RequestTimeout,
RateLimitExceeded, ExchangeNotAvailable — means you do not know whether it
was processed.
That distinction is worth more than the unified method names, because it is the one that decides whether a retry is safe. Catch these two rather than enumerating leaf classes, and new subclasses are handled sensibly:
try:
order = exchange.create_order(...)
except ccxt.ExchangeError:
raise # understood and refused — do not retry
except ccxt.OperationFailed:
reconcile(symbol, attempt_time) # outcome unknown — go and find out
Never blind-retry an order after OperationFailed. A timeout on
createOrder may mean the request never arrived, or that it filled and the
response was lost. Query open orders, recent fills and the position before
sending anything.
And do not assume a client order ID saves you. On Binance spot,
newClientOrderId is documented as “a unique id among open orders”, with a
reused ID accepted “only when the previous one is filled” — so it blocks the
duplicate while the original rests and permits it once filled, which is exactly
the case you needed protection for.
What it does not remove
CCXT normalises the interface, not the venues. These remain yours:
Permission models. Binance flags on the key with withdrawals gated behind IP restriction and trading permission expiring at 90 days without a whitelist; Alpaca’s Access Controls; IBKR’s application-level Read-Only API; Hyperliquid’s agent wallets, which are not keys at all.
Rate limit semantics. Binance meters weight, not count. Bybit splits
account-level (10006) from IP-level (10018). Hyperliquid earns request
allowance from cumulative volume. RateLimitExceeded unifies the exception; it
does not unify what to do about it.
Environment separation. Binance, Bybit, Hyperliquid and Alpaca use separate
hostnames for test environments. OKX shares a hostname and switches with
x-simulated-trading: 1 — a header CCXT must be told about, and one that
silently means production when absent.
Symbol filters and minimums. Per-venue, per-symbol, and they change.
Account type quirks. Bybit’s Unified Trading Account changes what several
error codes mean — 10003 is “too many sessions” on classic and “your api key
has expired” on UTA spot. A unified exception type does not tell you which
underlying condition you hit.
Practical notes
Check exchange.has before assuming a method works. Coverage varies per
venue and the unified interface does not imply universal support.
Read exchange.rateLimit and think about the built-in limiter. It is a
useful safety net, but per-venue limits are richer than a single delay —
Binance’s weighting in particular is not captured by a uniform interval.
Keep the venue’s own error visible. When something fails, you will want the
native code — -2015, 10005, 50113 — not just the CCXT class. The unified
type tells you what kind of problem it is; the native code tells you which one.
Verify against the source when it matters. The exception hierarchy lives in
ts/src/base/errors.ts. It is source code, not an API contract, and is worth
re-checking on a major version bump.
Is it worth it for one exchange?
Often yes, for the error hierarchy and the signing alone. The unified interface matters most across venues; the classification of failures matters even on one.
The case against is that an abstraction tends to expose the intersection of venue features rather than the union, so if you depend on something venue- specific you will end up reaching past it anyway. That is a normal and fine outcome — use it for the common path and drop down where you need to.
FAQ
Does CCXT handle API key permissions for me?
No. Permission models are venue-specific and stay your responsibility — Binance uses flags on the key with withdrawals gated behind IP restriction, Alpaca uses Access Controls, IBKR uses an application setting, and Hyperliquid has no keys at all. CCXT unifies how you call the API, not what your credential is allowed to do.
Should I use the built-in rate limiter?
It is a reasonable safety net and better than nothing. But it cannot express
per-endpoint weighting the way Binance meters usage, or the account-versus-IP
split Bybit reports through 10006 and 10018. Treat it as a floor, not as
compliance with the venue’s actual limits.
Which exception should I catch?
ExchangeError and OperationFailed — the two top-level branches — rather than
individual leaf classes. The first means the request was refused and should be
fixed; the second means the outcome is unknown and you should reconcile. Catching
at that level also means new subclasses are handled sensibly.
Does CCXT make retries safe?
No, and nothing does automatically. It gives you the information needed to
decide: ExchangeError is safe to not retry, OperationFailed means you must
find out what happened first. The reconciliation step is yours to write.