WebSocket recovery
Code example
{"op":"SUBSCRIBE","channel":"MARKET","stream":"BOOK", "marketId":"BTC-3M-...","outcome":"UP","afterSequence":"42"}{"op":"SUBSCRIBE","channel":"MARKET","stream":"LIFECYCLE", "marketId":"BTC-3M-...","afterSequence":"7"}{"op":"SUBSCRIBE","channel":"USER","afterSequence":"101"}Without afterSequence, the server sends a first snapshot. With it, retained events replay from the next sequence. A gap, future cursor, retention loss, or backpressure returns RESYNC_REQUIRED; reload the REST snapshot before reconnecting. BOOK deltas bind previousBookHash to the new bookHash.
Heartbeat
POST /heartbeats renews every 5 seconds with a 15-second TTL. On an unknown renew result, HEARTBEAT_EXPIRED, or HEARTBEAT_CANCEL_IN_PROGRESS, stop order submission immediately. Poll until DISARMED, confirm account snapshot openOrders is empty, then re-arm with a new timestamp, nonce, and HMAC. Any remaining order or recovery timeout must fail closed. WebSocket ping/pong does not renew it.
Code example
// Stop submitting orders as soon as renew is unknown or returns// HEARTBEAT_EXPIRED / HEARTBEAT_CANCEL_IN_PROGRESS.for (;;) { const heartbeat = await resoGet("/api/trading/heartbeats"); if (heartbeat.status === "ARMED" && heartbeat.expiresAtMs > heartbeat.serverTimeMs) break; if (heartbeat.status === "EXPIRED" || heartbeat.status === "CANCELING") { await delay(heartbeat.heartbeatIntervalMs); continue; } if (heartbeat.status !== "DISARMED") throw new Error("heartbeat recovery unconfirmed"); const snapshot = await resoGet("/api/trading/account/snapshot"); if (snapshot.openOrders.length !== 0) throw new Error("open orders remain"); await resoPost("/api/trading/heartbeats"); // New timestamp, nonce and HMAC. break;}Errors and safe retries
Trading V2 /api/trading/* errors use {error:{code,message,requestId,retryable,retryAfterMs}}. Existing product and Funding routes use the OpenAPI-documented {error:"CODE_OR_MESSAGE"}; do not parse them as the same envelope. Honor Retry-After. Never retry authentication, signature, nonce, parameter, or balance failures unchanged.
| HTTP | Meaning | Retry unchanged |
|---|---|---|
| 400 / 422 | Request, signature, or balance error | No |
| 401 / 403 | Authentication or permission error | No |
| 409 | State or idempotency conflict; query first | Same key and bytes only |
| 413 | Request body exceeds 256 KiB; reduce it | No |
| 423 | Feature safely paused | No |
| 429 | Rate limited | Honor Retry-After |
| 500 / 503 | Server or Provider unavailable; query unknown creates first | Same key and bytes only |
Manual Provider orders return a stable category in order.failureCode with HTTP 422: ADI_SIGNATURE_REQUIRED, ADI_SUBMIT_FAILED, LIVE_SUBMIT_FAILED, LIVE_API_KEY_MISMATCH, LIVE_CREDENTIAL_REFRESH_FAILED, LIVE_DEPOSIT_WALLET_REQUIRED, LIVE_MIN_ORDER_SIZE, LIVE_INSUFFICIENT_BALANCE, LIVE_ALLOWANCE_REQUIRED, or GEOBLOCKED_REGION. Clients must tolerate future values and treat unknown values like LIVE_SUBMIT_FAILED.
On an unknown create result, query account/orders first and retry only with the identical idempotency key and exact signed payload. A changed payload requires a new key and signature. Limits: 15 batch orders, 100 cancel IDs, 32 market scopes, 256KiB HTTP JSON, and 16KiB WebSocket client messages.