QuataPay
Developer

Error handling

How to retry, surface, and log errors without double-charging customers.

Browse developer docs
Back to developer docs

Retry only the right ones

Retry on 5xx and 429. Never retry 4xx— the payload itself needs fixing, and replaying it won't help.

Exponential backoff + jitter

async function callWithRetry(fn) {
  for (let attempt = 0; attempt < 5; attempt++) {
    try {
      return await fn();
    } catch (err) {
      if (!isRetryable(err) || attempt === 4) throw err;
      const base = 2 ** attempt * 500;            // 0.5s, 1s, 2s, 4s
      const jitter = Math.random() * 500;
      await sleep(base + jitter);
    }
  }
}

Honour Retry-After

On a 429 the response includes a Retry-After header (seconds). Sleep at least that long before retrying. Ignoring it can get your key throttled harder.

Idempotency saves you from doubles

Always send Idempotency-Key on POSTs that move money. Then a retry of a previously-succeeded request returns the cached success response instead of creating a second payment.

Surface customer-friendly messages

The error.message field is intended for end users. error.codeis the machine-readable enum for your code to branch on. Don't hand the customer raw stack traces or validation arrays.

Log the request id

Every response carries a unique X-Request-Id header. Log it on every error — when you open a support ticket, this is the single piece of info that lets us trace the call.