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:

  1. 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.
  2. If the key was already used with the same request body → the original response is returned verbatim. The endpoint is not re-executed.
  3. 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.