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-addresses — idempotency_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/withdrawals — client_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.