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, timeouts and 429, always with the same Idempotency-Key. Never retry other 4xx responses — the request 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 the Idempotency-Key header when you create a payment. Then a retry of a request that already went through returns the first response (marked Idempotency-Replayed: true) instead of creating a second payment. Reusing a key with a different body is a 409.

Surface customer-friendly messages

The error field is a sentence meant for your logs, and code (VALIDATION_ERROR or HTTP_<status>) plus the HTTP status are what your code branches on. Write your own friendly message for the customer; don't hand them raw errors or validation details.

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.