Errors
Errors use the same envelope as success responses, with success: false. An application error carries
a stable code:
{
"success": false,
"message": "Client fee mode requires a positive order amount",
"code": "EXPECTED_AMOUNT_REQUIRED",
"correlationId": "8f3c…"
}
A request-validation failure carries details instead of a code:
{
"success": false,
"message": "Validation failed",
"details": [{ "field": "amount", "message": "amount must be a non-negative decimal string" }],
"correlationId": "8f3c…"
}
messageis always present and human-readable.code(when set) is a stable string code — branch on this, not the message text.detailsfor validation errors is an array of{ field, message }.correlationIdechoes yourx-correlation-idrequest header when it is a UUID (any other value is replaced by a server-generated one); it is also returned in thex-correlation-idresponse header on every response — include it when contacting support.
HTTP status mapping
| Situation | Status |
|---|---|
| Validation failure | 422 |
| Bad request | 400 |
| Missing/invalid API key, expired token | 401 |
| Forbidden / IP not allowed | 403 |
| Resource not found | 404 |
| Duplicate / conflict | 409 |
| Rate limit exceeded | 429 |
| Application error | the error's own status |
| Server error | 500 (message hidden) |
| Upstream timeout / unavailable (gateway) | 504 / 502 |
Error codes
Codes you may encounter on the integration surface:
| Code | Status | Meaning |
|---|---|---|
| — | 401 | Missing/malformed/revoked key, or env mismatch (test key on live, etc.) — "message": "Invalid or missing API key", no code |
IP_NOT_ALLOWED | 403 | Request IP isn't in the key's allowlist (message echoes the IP seen) |
EXPECTED_AMOUNT_REQUIRED | 422 | amount required in client fee mode |
USD_AMOUNT_REQUIRED | 422 | pricingCurrency: "USD" sent without a positive amount |
RATE_UNAVAILABLE | 503 | No usable USD→USDT rate for a USD-priced order — retry shortly |
MAX_ACTIVE_ADDRESSES | 429 | Active-address cap reached |
SETTLEMENT_ACCOUNT_NOT_FOUND | 404 | settlementAccountId isn't yours |
NO_SETTLEMENT_ACCOUNT | 422 | reusable requested with no active settlement wallet |
INVALID_WEBHOOK_URL | 422 | Webhook URL failed the SSRF guard (private/loopback) |
| — | 422 | Unsupported webhook event name — "Validation failed" with details[].field = "eventTypes.<index>", no code |
NO_WEBHOOK_URL | 422 | Tried to test a key webhook with no URL set |
NOT_USD_ORDER / ORDER_NOT_ACTIVE / ORDER_EXPIRED | 422 | Checkout re-quote (POST /v1/checkout/:token/requote) on an order that is not USD-priced / no longer active / expired |
PAYMENT_IN_PROGRESS | 409 | Checkout re-quote after a payment was already detected |
LEDGER_UNREACHABLE / WALLET_UNREACHABLE | 503 | A ZaroPay internal service was unreachable while creating the charge — retry |
FEE_QUOTE_FAILED / FEE_QUOTE_INVALID / WALLET_DERIVE_FAILED | 502 | Internal fee-quote / address-derivation failure while creating the charge — retry |
ADDRESS_EXISTS | 409 | Address derivation collision (not expected in practice) — retry |
Rate-limit responses
A 429 may be a plain { "success": false, "message": "Rate limit exceeded" } with a Retry-After
header (seconds). Honor Retry-After and retry with backoff.
Handling tips
- Retry
429(afterRetry-After) and5xx/502/504(with exponential backoff). Do not retry4xxvalidation/auth errors — fix the request. - Log
correlationIdon failures so support can trace a specific request.