Error handling
Build resilient integrations with correct retry logic, idempotency, and error classification.
Error response format
All errors return a JSON body with an error code and an optional human-readable details string:
{
"error": "invalid_amount",
"details": "Amount must be a positive integer in minor units (e.g. 4999 for $49.99)"
}
HTTP status codes
| Status | Meaning | Retry? |
|---|---|---|
200 / 201 | Success | — |
400 | Bad request — invalid parameters | No — fix the request first |
401 | Unauthorized — invalid or missing API key | No — check your key |
403 | Forbidden — key lacks the required scope | No — create a key with the right scope |
404 | Resource not found | No |
409 | Idempotency key conflict | No — use a new key |
422 | Gateway declined the charge | No — inform the user |
429 | Rate limit exceeded | Yes — after Retry-After seconds |
500 / 502 / 503 | Server error | Yes — with exponential backoff |
Error codes
| Error code | Status | Description |
|---|---|---|
unauthorized | 401 | API key is missing, invalid, or revoked |
forbidden | 403 | API key lacks the required scope |
invalid_request | 400 | One or more required fields are missing or invalid |
invalid_amount | 400 | Amount is not a positive integer |
invalid_currency | 400 | Currency is not a valid ISO 4217 code |
gateway_declined | 422 | Payment gateway declined the charge |
gateway_error | 502 | Gateway returned an unexpected error |
not_found | 404 | The requested resource does not exist |
rate_limited | 429 | Too many requests — see Retry-After header |
idempotency_conflict | 409 | Same Idempotency-Key used with different request body |
Idempotency
All mutating endpoints (POST, PUT) accept an Idempotency-Key header. Sending the same key within 24 hours returns the original response without re-executing the operation.
Always use idempotency keys for payment operations to safely retry on network failures:
// Node.js — generate a stable key from your order ID
const idempotencyKey = `order-${orderId}-charge`;
const res = await fetch('https://api.uat.payments-central.com/api/v1/transactions', {
method: 'POST',
headers: {
'Authorization': `Bearer ${apiKey}`,
'Idempotency-Key': idempotencyKey,
'Content-Type': 'application/json',
},
body: JSON.stringify({ amount: 4999, currency: 'USD', gateway: 'stripe' }),
});
// Safe to retry — if the network failed after the server processed the request,
// retrying with the same key returns the original transaction (no double charge)
Idempotency-Key with a different request body returns 409 Conflict.
Use a key derived from the operation (e.g. order-{id}-charge) rather than a random UUID per attempt.
Retry strategy
Only retry on server errors (5xx) and rate limits (429). Never retry 4xx errors without fixing the request first.
async function callWithRetry(fn: () => Promise, maxAttempts = 4): Promise {
let delay = 500; // ms
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
const res = await fn();
if (res.ok) return res;
// Don't retry client errors
if (res.status >= 400 && res.status < 500 && res.status !== 429) {
return res;
}
if (attempt === maxAttempts) return res;
// Respect Retry-After header if present
const retryAfter = res.headers.get('Retry-After');
const waitMs = retryAfter ? parseInt(retryAfter) * 1000 : delay;
await new Promise(r => setTimeout(r, waitMs));
delay *= 2; // exponential backoff
}
throw new Error('Unreachable');
}
Gateway declines
A 422 gateway_declined means the payment gateway (Stripe, PayPal, etc.) rejected the charge. The transaction will be in failed status. This is a terminal state — do not retry the same charge.
Common reasons for declines:
- Insufficient funds
- Card expired
- Issuer fraud block
- Card reported stolen
In your UI, prompt the user to try a different payment method rather than retrying the same card.
Rate limits
The API enforces per-merchant rate limits. When exceeded, requests return 429 Too Many Requests with a Retry-After header indicating seconds to wait.
Default limits: 1,000 requests/minute per API key. Contact support for higher limits.