# Idempotency

Safely retry any POST request

# Idempotency

Every `POST` endpoint in the Southbill 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`:

```json
{
  "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:

```ts
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.SOUTHBILL_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("Southbill API unavailable");
}
```

Never retry a `4xx` other than `429` — fix the request first.

