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.code is 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.