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.