# Idempotency keys for payment and mutation endpoints

- **Pattern:** `ab-002198` (`saas-api-design.api-security.idempotency-keys`)
- **Severity:** high
- **Lifecycle:** active
- **Last modified:** 2026-04-18
- **Canonical URL:** https://auditbuffet.com/patterns/ab-002198
- **License:** CC-BY-4.0 — attribute to AuditBuffet Pattern Catalog (https://auditbuffet.com/patterns/ab-002198)

## Why it matters

Without idempotency protection, a network timeout followed by a client retry on a payment endpoint triggers a duplicate charge (CWE-362, race condition via repeated mutation). A double-click on a checkout button produces two identical Stripe `createSubscription` calls. The business consequence is immediate: duplicate charges require manual refunds, generate chargebacks, and erode customer trust. This is not a theoretical risk — network retries are the default behavior of most HTTP clients and mobile SDKs, making duplicate invocation the expected path under degraded network conditions.

## Severity rationale

High because duplicate payment execution from retries is a common real-world event that causes immediate financial and trust damage requiring manual remediation.

## Remediation

Pass idempotency keys to every Stripe API call in your payment route handlers, and store key-to-result mappings in Redis or your database for your own mutation endpoints:

```ts
// For Stripe calls — always pass idempotencyKey
const idempotencyKey = request.headers.get('Idempotency-Key') ?? crypto.randomUUID()

const subscription = await stripe.subscriptions.create(
  { customer: customerId, items: [{ price: priceId }] },
  { idempotencyKey }
)
```

For your own endpoints, check whether the key was already processed before executing business logic, and return the cached response on a hit. The client generates and stores the key before the first attempt, then retries with the same key on failure. Document this contract in your API reference so integrators know to supply the header.

## Detection

- **ID:** `idempotency-keys`
- **Severity:** `high`
- **What to look for:** Enumerate all API routes that perform irreversible or expensive operations: payment processing, subscription management, sending emails, sending notifications, creating orders, or other mutations where duplicate execution would cause problems. Check whether these endpoints support or require an `Idempotency-Key` header (or equivalent). Also check: if using Stripe, are `idempotencyKey` options passed to Stripe API calls? Are idempotency keys stored and checked to prevent replay?
- **Pass criteria:** At least 100% of payment endpoints and critical mutation endpoints either (a) implement idempotency key checking with a persistent store to deduplicate duplicate requests, or (b) use a payment provider's built-in idempotency (e.g., Stripe's `idempotencyKey` parameter) consistently.
- **Fail criteria:** Payment endpoints or email-sending endpoints have no idempotency protection — a network retry or double-click could trigger duplicate charges, duplicate emails, or duplicate resource creation.
- **Skip (N/A) when:** No payment processing and no expensive/irreversible mutation endpoints exist. Signal: no Stripe, PayPal, or other payment library detected; no email-sending library detected; no endpoint logic that creates orders or triggers financial operations.
- **Detail on fail:** Example: `POST /api/billing/subscribe has no idempotency key`. Identify the unprotected endpoints (e.g., "POST /api/billing/subscribe has no idempotency key; Stripe createSubscription call lacks idempotencyKey parameter"). Max 500 chars.
- **Remediation:** Add idempotency protection in `app/api/` route handlers to payment and critical mutation endpoints. For Stripe API calls, always pass an `idempotencyKey`: `stripe.subscriptions.create(params, { idempotencyKey: clientSuppliedKey || crypto.randomUUID() })`. For your own endpoints, accept an `Idempotency-Key` header, store it in Redis or your database with the request result, and on duplicate requests return the cached response. The client should generate and store the key before making the request, and retry with the same key on network failures. Stripe's own documentation has a clear idempotency implementation guide.

## External references

- cwe CWE-362
- iso-25010 functional-suitability.functional-correctness

Taxons: data-integrity, cost-efficiency

HTML version: https://auditbuffet.com/patterns/ab-002198
