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
| Status | Meaning |
|---|---|
200 | Success. |
400 | Business-rule rejection — invalid destination address, amount below the currency minimum, or insufficient balance. |
401 | Authentication failed — missing/invalid signature, stale timestamp, or replayed signature. |
403 | Authentication succeeded but the key isn't allowed to do this — missing permission or IP not whitelisted. |
404 | Resource not found — wrong currency, network, or id, or the row belongs to another account. |
422 | Request body or query failed schema validation — missing/empty field, wrong type, value out of range. message lists the offending fields. |
429 | Rate limit exceeded — either the per-key limit or the global per-IP backstop. See Rate limits. |
502 | The gateway could not complete an on-chain operation — broadcast rejected, no hot wallet, etc. |
503 | A 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.
message | Status | Cause |
|---|---|---|
Missing authentication headers | 401 | One of X-CPG-Key, X-CPG-Timestamp, X-CPG-Signature is absent. |
Invalid API key | 401 | X-CPG-Key doesn't match any key in the system. |
API key has been revoked | 401 | The key exists but its status is no longer active. |
Invalid signature | 401 | Recomputed HMAC didn't match. Almost always a canonical-string mistake. |
Request timestamp is outside the allowed window | 401 | |server_now - X-CPG-Timestamp| exceeds 300 seconds. |
Request signature has already been used | 401 | Same X-CPG-Signature was accepted within the last ~5 minutes (replay). |
Source IP is not whitelisted for this key | 403 | Caller's IP is not in the key's ip_whitelist. |
API key does not have permission for this action | 403 | The key is missing the required ApiPermission for this endpoint. |
Rate limit exceeded | 429 | The key has made more than rate_limit_per_min requests this minute. |
Authentication service temporarily unavailable | 503 | A 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.