Authentication
The public API supports two credential models. Pick the one that matches your workload — interactive flows use bearer JWTs minted by the dashboard, programmatic flows use scoped API keys.
Bearer JWT
A signed session JWT, valid for 60 minutes, refreshable for 30 days. Pass it as Authorization: Bearer <token> on every request. Tokens are bound to the issuing IP range; a request from a materially different network triggers a soft re-authentication.
curl -H "Authorization: Bearer $TOKEN" \ https://api.cooud.exchange/v1/wallet/balances
API keys
Provision an API key pair from Settings → API keys → New key. The secret is shown exactly once; we store an HMAC and never recover it. Each key is scoped (read, trade, withdraw) and rotates on a 30-day cadence by default.
Requests are signed by HMAC-SHA256 over timestamp + method + path + body. Skew tolerance is 30 seconds.
X-Cooud-Key: $KEY_ID X-Cooud-Timestamp: 1747555200 X-Cooud-Signature: 6fb9c1...e2a4
Idempotency
Every mutating endpoint (POST, PATCH, DELETE) requires an Idempotency-Key header. Use a UUID v4 per logical operation — never reuse a key across different operations.
Retrying with the same key always returns the original response (status code, body, and stored headers) for 24 hours. After that window the key is garbage-collected and may be reused.
POST /v1/orders
Idempotency-Key: 0c64d6e7-3b1e-4f6f-9e3b-8a3b3f04e5b2
{
"symbol": "ETH/USDT",
"side": "sell",
"type": "limit",
"price": "3120.50",
"size": "0.5"
}If a retry arrives while the first request is still being processed, we return 409 idempotency_in_flight. Back off and retry a few hundred milliseconds later.
Rate limits
Limits are applied per credential and per IP. Counters are exposed on every response via standard headers.
X-RateLimit-Limit: 1200 X-RateLimit-Remaining: 1187 X-RateLimit-Reset: 1747555260
The default budget is 1200 requests per minute for read endpoints, 300 per minute for writes, and 60 per minute for withdrawal submissions. WebSocket connections are capped at 8 per credential with a 256-channel subscription ceiling per connection.
When the budget is exhausted we respond with 429 rate_limited and a Retry-After header. Persistent bursts above the soft cap may trip a longer cool-off (15 minutes) at the edge.
REST endpoints
All paths are prefixed with /v1 and served from https://api.cooud.exchange. Bodies are JSON; numeric values are sent as strings to preserve precision.
Auth
POST /v1/auth/login— issue a session JWT.POST /v1/auth/refresh— refresh a session JWT.POST /v1/auth/logout— revoke the active session.POST /v1/auth/2fa/enroll— start TOTP enrolment.
Wallet
GET /v1/wallet/balances— total, free, locked per asset.GET /v1/wallet/addresses— deposit addresses by chain.POST /v1/wallet/withdrawals— submit a withdrawal.GET /v1/wallet/withdrawals/:id— fetch a withdrawal.
Trading
POST /v1/orders— place an order.DELETE /v1/orders/:id— cancel an order.GET /v1/orders— list open + recent orders.GET /v1/fills— your trade history.
Markets
GET /v1/markets— list symbols and tick/step sizes.GET /v1/markets/:symbol/ticker— 24-hour ticker.GET /v1/markets/:symbol/orderbook— L2 snapshot.GET /v1/markets/:symbol/candles— OHLCV history.
An example: place a limit order and read the canonical response.
POST /v1/orders
{
"symbol": "BTC/USDT",
"side": "buy",
"type": "limit",
"price": "61500.00",
"size": "0.0005",
"tif": "GTC"
}
201 Created
{
"id": "ord_01HXY9G7M5...",
"status": "open",
"filled": "0",
"remaining": "0.0005",
"created_at":"2026-05-18T12:04:11.842Z"
}WebSocket channels
Streaming data is served from wss://stream.cooud.exchange/v1. Subscribe to channels after the connection upgrade; a single connection multiplexes many channels.
Public channels
orderbook.<symbol>— L2 deltas, snapshot every 60s.trades.<symbol>— public trades as they print.ticker.<symbol>— 24-hour rolling ticker.candles.<symbol>.<interval>— live OHLCV.
User stream (authenticated)
orders— your order updates (open, fill, cancel).balances— credit/debit events as they settle.withdrawals— state transitions for your withdrawals.
{
"op": "subscribe",
"args": [
"orderbook.BTC/USDT",
"trades.BTC/USDT"
]
}Heartbeats are sent every 15 seconds. If no message arrives for 45 seconds the client should reconnect; we do not require a ping from you, but we close idle sockets after 5 minutes.
Errors
Errors share a single envelope. The code is stable; the message is human-readable and may change between versions; the request_id identifies the call in our logs.
{
"error": {
"code": "insufficient_balance",
"message": "Account does not have enough USDT to place this order.",
"request_id": "req_01HXY9GA2T..."
}
}Status codes follow HTTP semantics. The common ones:
400 invalid_request— body or parameters did not validate.401 unauthenticated— missing or invalid credentials.403 forbidden— credential lacks the required scope.404 not_found— resource does not exist.409 idempotency_in_flight— duplicate retry while pending.422 business_rule_violation— request violates a platform rule (e.g. tick size, withdrawal velocity).429 rate_limited— budget exhausted, seeRetry-After.500 internal_error— bug in us; therequest_idis your ticket.
Versioning
We version under /v1, /v2, … — never via headers. Breaking changes always land on a new major. Additive changes (new fields, new optional parameters, new endpoints) land in place; integrations should ignore unknown fields gracefully.
Deprecations are announced at least 90 days before removal. The deprecation window is broadcast via the Deprecation and Sunset response headers, and in the changelog at /docs.

