Idempotency
Make retries safe: how Idempotency-Key works, how to choose keys, and a retry loop that can't pay twice.
Networks fail. A request can reach us and the answer get lost on the way back. Without protection, retrying a payout could pay someone twice. Idempotency keys make retries safe.
The rule
Every request that moves money (today POST /api/v1/payouts) needs an Idempotency-Key header. Without one you get IDEMPOTENCY_KEY_REQUIRED and nothing happens.
| You send | We do |
|---|---|
| A new key | Process the request and store the response for 24 hours. |
| The same key and the same body (within 24 hours) | Return the stored response, with the header Idempotent-Replayed: true. No second payout. |
| The same key and a different body | Refuse with IDEMPOTENCY_KEY_REUSED (409). |
| The same key while the first is still running | Refuse with IDEMPOTENCY_IN_PROGRESS (409). Wait a second and retry. |
| A key whose first request failed validation or a limit | The key is released: fix the request and retry with the same key. |
Choosing keys
- 8 to 128 characters: letters, digits,
._:-. - One key per payout you intend to make, created before the first attempt and saved with your record (order, payroll line), so a retry after a crash reuses it.
- A UUID works, or something derived from your own id, such as
payroll-2026-11:line-42. Never reuse a key for a different payout. - Keys are separate per environment: the same key in test and live doesn't collide.
Retrying safely
retry.js
// Retry network errors and 5xx with backoff, reusing the same Idempotency-Key.
async function createPayout(body, idempotencyKey) {
for (let attempt = 0; attempt < 5; attempt++) {
try {
const res = await fetch("https://api.autopayke.com/api/v1/payouts", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.AUTOPAYKE_API_KEY}`,
"Idempotency-Key": idempotencyKey,
"Content-Type": "application/json",
},
body: JSON.stringify(body),
});
const json = await res.json();
if (json.ok) return json.data;
// Retry only what's worth retrying.
if (["IDEMPOTENCY_IN_PROGRESS", "RATE_LIMITED", "CAPABILITY_PAUSED", "METHOD_UNAVAILABLE"].includes(json.code) || res.status >= 500) {
await new Promise((r) => setTimeout(r, 2 ** attempt * 500));
continue;
}
throw Object.assign(new Error(json.error), { code: json.code, requestId: json.requestId });
} catch (err) {
if (err.code) throw err; // an API error we decided not to retry
await new Promise((r) => setTimeout(r, 2 ** attempt * 500)); // network error: retry
}
}
throw new Error("Gave up; check GET /api/v1/payouts?reference=… before trying again");
}After 24 hours a key can be used again as new. If you're unsure whether an old attempt went through, look it up by reference with GET /api/v1/payouts?reference=… before retrying.