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 endpointGET /v1/invoices/:id/status returns the canonical envelope plus a legacy error field containing a slug: invalid_invoice_id, rate_limited, or not_found. Existing widget/checkout clients key off the slug; new integrations should use code.
  • Payout APIs/v1/mass-payout, /v1/evm-mass-payout, and /v1/nc-payout use a result envelope instead of the HTTP-error envelope. UTXO and EVM mass payouts return { "ok": false, "error": { "kind": ... } } with snake_case kinds (for example invalid_csv, or idempotency_conflict with 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 HTTP 200. 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.