Idempotency
Safely retry any POST request
Idempotency
Every POST endpoint in the spuke API accepts an Idempotency-Key header. Use it to safely retry a request after a network error without creating duplicate resources.
How it works
The uniqueness scope is (merchant_id, method, path, Idempotency-Key). When the API receives a POST with an Idempotency-Key:
- If the key has never been used on this endpoint → the request is processed normally, and its response (status + body) is stored for 24 hours.
- If the key was already used with the same request body → the original response is returned verbatim. The endpoint is not re-executed.
- If the key was already used with a different request body → the API returns
409 Conflict:
{
"error": {
"type": "idempotency_error",
"code": "idempotency_key_reused",
"message": "Idempotency-Key reused with a different request body"
}
}
Keys expire after 24 hours. After that, the same key can be reused for a new request.
Choosing a good key
- Deterministic per business action:
order_12345,sub_2026-07-18_001,refund_ch_abc_partial_1. - Do not use timestamps or random UUIDs generated per retry — the whole point is that a retry uses the same key.
- 1–255 characters, ASCII.
Endpoints that support it
| Method | Path |
|---|---|
POST |
/v1/checkout/sessions |
POST |
/v1/checkout/sessions/{id}/expire |
POST |
/v1/subscriptions |
POST |
/v1/subscriptions/{id}/cancel |
POST |
/v1/products |
POST |
/v1/products/{id} |
POST |
/v1/products/{id}/prices |
POST |
/v1/products/{id}/default_price |
GET and DELETE requests ignore the header — they are already idempotent by definition.
Retries
Combine Idempotency-Key with exponential backoff for 5xx and 429 responses:
async function postWithRetry(url: string, body: unknown, key: string) {
for (let attempt = 0; attempt < 5; attempt++) {
const res = await fetch(url, {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.SPUKE_SECRET_KEY}`,
"Content-Type": "application/json",
"Idempotency-Key": key,
},
body: JSON.stringify(body),
});
if (res.status < 500 && res.status !== 429) return res;
const wait = res.headers.get("Retry-After");
await new Promise(r => setTimeout(r, (wait ? +wait : 2 ** attempt) * 1000));
}
throw new Error("spuke API unavailable");
}
Never retry a 4xx other than 429 — fix the request first.