Skip to main content

Errors & response envelope

Every Crypto Payment Gateway response follows one of two predictable shapes. Read this page once and you know how to parse any /v1 endpoint.

Success envelope

A 2xx response is always a JSON object with at minimum success, message, and data:

{
"success": true,
"message": "OK",
"data": { ... endpoint-specific payload ... }
}

message is a human-readable string from the server's localised message catalogue — useful for debug logs, not for branching logic.

List endpoints add data_count

Endpoints that return a collection (GET /v1/balances, GET /v1/currencies, GET /v1/transactions) add a data_count field — the number of rows in data:

{
"success": true,
"message": "OK",
"data": [ ... rows ... ],
"data_count": 50
}

Cursor pagination (transactions only)

GET /v1/transactions is the only paginated endpoint. On top of data_count it adds two more fields:

{
"success": true,
"message": "OK",
"data": [ ... rows ... ],
"data_count": 50,
"next_cursor": 12345,
"has_more": true
}

To fetch the next page, pass next_cursor back in the cursor query parameter. When has_more is false, you're at the end and next_cursor is null.

GET /v1/balances and GET /v1/currencies are not paginated — they return the full set with data_count only, and never include next_cursor or has_more.

Error responses

A non-2xx response keeps the same envelope as a success — success is false and message carries the reason. There is no detail field:

{
"success": false,
"message": "Invalid signature"
}

The message value is a stable string from the server's message catalogue, so you can match on it for programmatic handling — though the HTTP status code is usually enough. When a request fails schema validation, message is a ; -joined list of the offending fields (e.g. amount: Input should be greater than 0; to_address: String should have at least 10 characters).

HTTP status code map

StatusMeaning
200Success.
400Business-rule rejection — invalid destination address, amount below the currency minimum, or insufficient balance.
401Authentication failed — missing/invalid signature, stale timestamp, or replayed signature.
403Authentication succeeded but the key isn't allowed to do this — missing permission or IP not whitelisted.
404Resource not found — wrong currency, network, or id, or the row belongs to another account.
422Request body or query failed schema validation — missing/empty field, wrong type, value out of range. message lists the offending fields.
429Rate limit exceeded — either the per-key limit or the global per-IP backstop. See Rate limits.
502The gateway could not complete an on-chain operation — broadcast rejected, no hot wallet, etc.
503A required backing service is temporarily unavailable, so the request fails closed. Retry with backoff.

Authentication error catalogue

These are the exact message strings the HMAC middleware emits. Every one of them is a 401 unless noted otherwise.

messageStatusCause
Missing authentication headers401One of X-CPG-Key, X-CPG-Timestamp, X-CPG-Signature is absent.
Invalid API key401X-CPG-Key doesn't match any key in the system.
API key has been revoked401The key exists but its status is no longer active.
Invalid signature401Recomputed HMAC didn't match. Almost always a canonical-string mistake.
Request timestamp is outside the allowed window401|server_now - X-CPG-Timestamp| exceeds 300 seconds.
Request signature has already been used401Same X-CPG-Signature was accepted within the last ~5 minutes (replay).
Source IP is not whitelisted for this key403Caller's IP is not in the key's ip_whitelist.
API key does not have permission for this action403The key is missing the required ApiPermission for this endpoint.
Rate limit exceeded429The key has made more than rate_limit_per_min requests this minute.
Authentication service temporarily unavailable503A required backing service is temporarily unreachable. Retry with exponential backoff.

See Rate limits & restrictions for the full lifecycle of the rate limit, nonce, and whitelist checks.

Idempotency

Two write endpoints accept a client-supplied idempotency key so retries after a network blip never double-spend.

POST /v1/deposit-addressesidempotency_key

Pass the same idempotency_key (any string up to 128 chars, unique per API key) on a retry, and the server returns the original allocation instead of allocating a new address.

{ "currency": "USDT", "network": "ETH", "idempotency_key": "alloc_2026_05_24_abc123" }

Reusing a key across a different (currency, network) is undefined — keys are scoped per API key, not per resource type, so make them globally unique within your app.

POST /v1/withdrawalsclient_reference

Same idea, different field name. Pass the same client_reference (up to 128 chars, unique per API key) on a retry, and you get back the original withdrawal row — never a second debit.

{
"currency": "USDT",
"network": "ETH",
"to_address": "0xrecipient...",
"amount": "50.00",
"client_reference": "order_2026_05_24_xyz789"
}

Without client_reference, a retried POST /v1/withdrawals after a network timeout could broadcast twice if your previous request actually succeeded but the response was lost. Always set it.