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.