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

StatusMeaningRetry?
200 / 201Success
400Bad request — invalid parametersNo — fix the request first
401Unauthorized — invalid or missing API keyNo — check your key
403Forbidden — key lacks the required scopeNo — create a key with the right scope
404Resource not foundNo
409Idempotency key conflictNo — use a new key
422Gateway declined the chargeNo — inform the user
429Rate limit exceededYes — after Retry-After seconds
500 / 502 / 503Server errorYes — with exponential backoff

Error codes

Error codeStatusDescription
unauthorized401API key is missing, invalid, or revoked
forbidden403API key lacks the required scope
invalid_request400One or more required fields are missing or invalid
invalid_amount400Amount is not a positive integer
invalid_currency400Currency is not a valid ISO 4217 code
gateway_declined422Payment gateway declined the charge
gateway_error502Gateway returned an unexpected error
not_found404The requested resource does not exist
rate_limited429Too many requests — see Retry-After header
idempotency_conflict409Same 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)
Key reuse with different bodies Using the same 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:

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.