Errors
Error envelope shape and the full catalogue of error codes returned by the payzum API.
The interactive API reference at /api/docs lets you try every endpoint in the browser and see live error responses alongside the schema documentation.
All error responses share a single JSON envelope shape. Your error-handling code only needs to inspect one structure regardless of which endpoint returned the error.
Error envelope
{
"statusCode": 401,
"code": "API_KEY_MISSING",
"message": "The x-api-key header is required for this endpoint."
}| Field | Type | Description |
|-------|------|-------------|
| statusCode | number | Mirrors the HTTP status code. |
| code | string | Machine-readable constant — use this for programmatic handling. |
| message | string | Human-readable description, suitable for logging. |
Always branch on code, not on message — the message text may change across releases.
Error code reference
This is the canonical catalogue of every code value the /v1/* merchant API can return. If you see a code that is not in this table, treat it as INTERNAL_ERROR and report it to support.
| HTTP | code | Retryable? | When it occurs |
|------|--------|------------|----------------|
| 400 | INVALID_REQUEST | No | The request body failed JSON parsing or Zod schema validation. The message field contains the first validation error. |
| 400 | CURRENCY_NOT_SUPPORTED | No | The pay_currency value is unknown or the underlying chain is not enabled for this deployment. |
| 400 | AMOUNT_BELOW_MINIMUM | No | The invoice value is below the network minimum. See Minimum amounts. |
| 400 | RANGE_TOO_LARGE | No | Accounting export: the requested date range spans more than 366 days. Narrow the range. |
| 401 | API_KEY_MISSING | No | The x-api-key header was not sent on an endpoint that requires authentication. |
| 401 | API_KEY_MALFORMED | No | The header was present but the value fails format validation — issued keys are 64-character hex strings, and the gateway rejects any value shorter than 32 characters. |
| 401 | API_KEY_NOT_FOUND | No | The key format is valid but no merchant record matches the SHA-256 hash of the supplied key. |
| 403 | MERCHANT_SUSPENDED | No | The merchant account exists and the key is valid, but the account has been suspended by an admin. Existing open invoices continue to accept funds and fire IPNs; new invoices cannot be created. |
| 404 | PAYMENT_NOT_FOUND | No | No invoice matching the supplied invoice ID or merchant order_id was found for this merchant. |
| 404 | REPORT_NOT_FOUND | No | The requested monthly report does not exist. |
| 422 | NO_ELIGIBLE_CURRENCIES | No | The invoice was created with pay_currency: "all" but the merchant's accepted-tokens allowlist leaves no eligible currency for the buyer to choose. |
| 429 | RATE_LIMIT_EXCEEDED | Yes — after Retry-After | The per-merchant request quota of 60 requests per 60 seconds was exceeded. The response includes Retry-After: 60. |
| 429 | QUOTA_EXCEEDED | Yes — but not immediately | The merchant has reached the maximum number of simultaneously open invoices. Retry only after existing invoices have been closed or expired — immediate retries will hit the same wall. |
| 500 | INTERNAL_ERROR | Yes — with backoff | An unhandled server-side failure — encryption error, missing master key, or unexpected exception. Retry with exponential backoff; if the error persists, contact support. |
| 503 | RATE_PROVIDER_DOWN | Yes — with backoff | The upstream market rate provider is unreachable. The estimate and invoice-creation endpoints require a live price feed. Retry after a short delay. |
| 503 | EXPORT_HISTORY_UNAVAILABLE | Yes — with backoff | Accounting export: the historical data store is temporarily unavailable. Retry after a short delay. |
Other surfaces
A few endpoints deliberately use a different error shape from the canonical envelope above. This is a per-surface design decision, not a bug — each surface matches the conventions its callers expect:
- Public buyer status endpoint —
GET /v1/invoices/:id/statusreturns the canonical envelope plus a legacyerrorfield containing a slug:invalid_invoice_id,rate_limited, ornot_found. Existing widget/checkout clients key off the slug; new integrations should usecode. - Payout APIs —
/v1/mass-payout,/v1/evm-mass-payout, and/v1/nc-payoutuse a result envelope instead of the HTTP-error envelope. UTXO and EVM mass payouts return{ "ok": false, "error": { "kind": ... } }with snake_case kinds (for exampleinvalid_csv, oridempotency_conflictwith HTTP 409); nc-payout returns{ "ok": false, "error": { "code": ... } }with SCREAMING_SNAKE codes. Each payout page documents its own catalogue — see Payouts overview. - CoinPayments adapter — the compatibility adapter mirrors the CoinPayments convention: application errors are returned as
{ "error": "...", "result": ... }with HTTP200. See CoinPayments adapter.
Handling errors in code
const res = await fetch(`${PAYZUM_BASE}/v1/payment`, {
method: "POST",
headers: { "x-api-key": apiKey, "Content-Type": "application/json" },
body: JSON.stringify(payload),
});
if (!res.ok) {
const err = await res.json() as { statusCode: number; code: string; message: string };
switch (err.code) {
case "RATE_LIMIT_EXCEEDED":
// honour Retry-After header
await sleep(Number(res.headers.get("Retry-After") ?? 60) * 1000);
break;
case "CURRENCY_NOT_SUPPORTED":
// show user a currency picker
break;
default:
throw new Error(`Payzum error ${err.code}: ${err.message}`);
}
}MERCHANT_SUSPENDED (403) will not resolve on its own — creating new invoices is blocked until an admin lifts the suspension. Handle it by surfacing a clear message to the merchant rather than retrying.