# Crypto payment API errors, rate limits and safe retries

> Errors are plain JSON with conventional status codes. The skill is knowing which ones are an answer and which are an outage, and retrying the second kind without ever paying twice.

Updated 2026-10-10 · Payaries · https://www.payaries.com/guides/crypto-payment-api-errors-and-retries

## The error shape

There is no envelope. A validation failure is `{ "error": "Validation failed", "details": { ... } }`; everything else is `{ "error": "Human-readable message" }`, often with a machine-readable `code` and context. Match on the status code first and on `code` where present, never on the text.

## Status codes and what to do

| Status | Meaning | Retry? |
|---|---|---|
| `400` | Validation failed, or structurally invalid: unsupported or disabled asset, bad address, missing `Idempotency-Key`, or a `successUrl`/`cancelUrl` host that isn't a redirect domain | No. Fix the request |
| `401` | Bad or missing auth, stale timestamp, **replayed signature**, revoked session, or a 2FA code required or already used | Only after re-signing, and only once. Don't loop |
| `402` | `FEE_WALLET_INSUFFICIENT`: the asset requires a funded fee wallet | No. Top up, then retry |
| `403` | Account rejected, onboarding incomplete, IP not whitelisted, blocked destination, or a dashboard-only action attempted with a key | No. It needs a person or a setting change |
| `404` | Not found, or not yours | No |
| `409` | Conflict: insufficient balance, an invalid state change, or an `Idempotency-Key` reused with different parameters | No. Read the state |
| `429` | Rate limited, or an IP locked after repeated auth failures | Yes, with backoff. Honour the limit headers |
| `502` | An internal service can't be reached | Yes. Safe to retry |
| `503` | A security dependency is unavailable and the gateway **fails closed** | Yes, with a fresh signature and the same `Idempotency-Key` |

## Error codes worth handling

| `code` | Where | Meaning and action |
|---|---|---|
| `ONBOARDING_REQUIRED` | `POST /v1/invoices` | Business profile not submitted. Surface to a person; read `onboardingStatus` |
| `ACCOUNT_REJECTED` | Invoices, withdrawals, fee-wallet top-ups | The application was rejected. Final; contact support |
| `REDIRECT_URL_NOT_ALLOWED` | `POST /v1/invoices` | Carries `field` (`successUrl` or `cancelUrl`). Register the host as a redirect domain in the dashboard, or fix the URL ([hosted checkout](https://www.payaries.com/guides/hosted-crypto-checkout)) |
| `FEE_WALLET_INSUFFICIENT` | Invoices, withdrawals | Carries `requiredUsd`. Top up the fee wallet |
| `BELOW_MINIMUM_INVOICE` | `POST /v1/invoices` | Carries `chainId` and `minimumUsd`. Raise the amount or pick another chain |
| `BELOW_MINIMUM_WITHDRAWAL` | Withdrawals, quote | Carries `minimumUsd` |
| `AMOUNT_TOO_SMALL_FOR_NETWORK_FEE` | Withdrawals, quote | Carries `networkFee` and `minimumAmount`. Withdraw at least that |
| `NETWORK_FEE_UNAVAILABLE` | Withdrawals, quote | `503`: the fee couldn't be priced. Retry; nothing was charged |
| `two_factor_required` | Dashboard-session withdrawals | 2FA isn't enabled on the account |
| `TOKEN_NOT_ACTIVE_YET` | Custom tokens | Carries `usableFrom`. Retry after that time |
| `TOKEN_DISABLED` | Custom tokens | No new invoices in this token; existing balances can still be withdrawn |

## Rate limits

| Limiter | Applies to | Limit |
|---|---|---|
| General | Every request | 120 per minute per IP |
| Strict | Writes: creating invoices and withdrawals, settings changes | 20 per minute per IP |
| Signup | `POST /v1/merchants/signup` | 5 per hour per IP |
| Login | `POST /v1/auth/login` | 10 per 15 minutes per IP, and 5 failures per 15 minutes per account |
| Auth failures | Any request with a bad key or signature | 30 failures in 10 minutes locks that IP for 10 minutes |

A `429` includes standard rate-limit headers. Limits are shared across gateway replicas.

## A retry loop that can't double-pay

Retry only `429`, `502` and `503`, with jittered exponential backoff. The request function signs afresh on every call; a withdrawal's `Idempotency-Key` is generated once, stored with your payout record, and reused on every attempt, so a network-level retry can never pay twice.

**Node.js**

```
async function withRetry(makeRequest, { attempts = 5 } = {}) {
  for (let i = 0; i < attempts; i++) {
    const res = await makeRequest();          // makeRequest signs afresh each time: new timestamp, new signature
    if (res.status < 400) return res;
    const retryable = res.status === 429 || res.status === 502 || res.status === 503;
    if (!retryable || i === attempts - 1) return res;   // 4xx is an answer, not an outage: do not retry it
    await new Promise((r) => setTimeout(r, Math.min(30000, 500 * 2 ** i) * (0.5 + Math.random() / 2)));
  }
}

// A withdrawal keeps the SAME Idempotency-Key on every attempt:
const key = 'wd_2026_10_10_0001';   // generated once, stored with your payout record
await withRetry(() => call('POST', '/v1/withdrawals', body, { 'Idempotency-Key': key }));
```

- A timeout is not a failure. The request may have succeeded; retry with the same idempotency key, or read the resource.
- Don't retry a `4xx`. On invoice creation a `4xx` is a real refusal, not a transient fault.
- A gas shortfall on a withdrawal is not an API error: the request succeeds as `processing`. Watch `withdrawal.status_changed` or poll `GET /v1/withdrawals/:id`.

## Questions

**Which Payaries API errors are safe to retry?**
429 (with backoff), 502, and 503, which means the gateway is failing closed on a security check. Re-sign every attempt, and keep the same Idempotency-Key on a withdrawal. Other 4xx responses are answers about the request and should not be retried.

**What are the Payaries API rate limits?**
120 requests a minute per IP overall and 20 a minute for writes such as creating invoices and withdrawals. Thirty failed API-key authentications from one IP within ten minutes lock that IP for ten minutes.

**How do I stop a retry from creating a second withdrawal?**
Send an Idempotency-Key header and reuse the same value on every retry of that withdrawal. The same key with the same parameters returns the original withdrawal; the same key with different parameters is a 409.

## Try it

Create a test key and run a real invoice: [Payaries — www.payaries.com](https://www.payaries.com/signup). Payaries is a multi-chain crypto payment gateway — https://www.payaries.com

## Keep reading

- https://www.payaries.com/guides/sign-crypto-payment-api-requests — How to sign crypto payment API requests (HMAC-SHA256)
- https://www.payaries.com/guides/crypto-withdrawals-api — Withdrawing crypto through the API: quote, idempotency and approval
- https://www.payaries.com/guides/crypto-payment-api-quickstart — Crypto payment API quickstart: from signup to your first payout
- https://www.payaries.com/crypto-payment-api — A crypto payment API that behaves like the rest of your stack
- https://www.payaries.com/guides/hosted-crypto-checkout — Hosted crypto checkout: send the customer, get them back
- https://www.payaries.com/developers
- https://www.payaries.com/ai-agents — Build a crypto payment integration with an AI agent, safely
