# Idempotency keys are used on all state-changing charge requests

- **Pattern:** `ab-001158` (`ecommerce-payment-security.payment-integration.idempotency-keys-all-charges`)
- **Severity:** high
- **Lifecycle:** active
- **Last modified:** 2026-04-18
- **Canonical URL:** https://auditbuffet.com/patterns/ab-001158
- **License:** CC-BY-4.0 — attribute to AuditBuffet Pattern Catalog (https://auditbuffet.com/patterns/ab-001158)

## Why it matters

Without idempotency keys, a network timeout on a charge request leaves your app in an ambiguous state: the payment may or may not have succeeded, and a retry creates a second charge for the same order. This is a real operational failure — customers who retry a checkout after a timeout get double-charged, support costs spike, and refund rates increase. CWE-362 (Race Condition) captures the concurrent-retry scenario that idempotency keys prevent. Beyond customer impact, double charges on high-volume stores compound into significant financial reconciliation problems. An idempotency key ensures that Stripe treats any retry of the same logical transaction as a no-op, returning the original result instead of creating a new charge.

## Severity rationale

High because missing idempotency keys on charge calls make double-charging customers a near-certainty under any network instability or client retry logic.

## Remediation

Generate a unique idempotency key per logical transaction and pass it to every state-changing Stripe call. Derive it from your order ID for natural retry deduplication.

```ts
// app/api/create-payment-intent/route.ts
import { v4 as uuidv4 } from 'uuid'

// Stable key: reusing orderId means retries hit the same PaymentIntent
const idempotencyKey = `pi-${orderId}`

const paymentIntent = await stripe.paymentIntents.create(
  {
    amount: amountInCents,
    currency: 'usd',
    customer: stripeCustomerId,
  },
  { idempotencyKey }
)
```

Apply the same pattern to `stripe.refunds.create()` and any other state-changing call. Store the idempotency key alongside the order record so that application-level retries always reuse the same key.

## Detection

- **ID:** `idempotency-keys-all-charges`
- **Severity:** `high`
- **What to look for:** Count every backend call that creates charges, payment intents, refunds, or subscription records. For each call, check whether an idempotency key is provided. For Stripe, look for the `idempotencyKey` option in `stripe.paymentIntents.create()`, `stripe.charges.create()`, and `stripe.refunds.create()` calls. For other providers, look for equivalent idempotency header parameters. Check that the idempotency key is unique per logical transaction (typically a UUID generated per request or derived from a stable order identifier). Report the ratio: "X of Y state-changing payment API calls include idempotency keys."
- **Pass criteria:** 100% of state-changing payment API calls include a unique idempotency key passed to the provider. No more than 0 calls should lack an idempotency key. Keys are generated freshly per transaction (not reused across different transactions).
- **Fail criteria:** Any charge, payment intent creation, or refund call is made without an idempotency key. This exposes the app to double-charging if the client retries due to a network error.
- **Skip (N/A) when:** The project has no server-side payment charge logic (e.g., only uses PaymentIntents created by Stripe Checkout client-side, where Stripe handles idempotency).
- **Detail on fail:** Name the endpoint and the missing parameter. Example: `"POST /api/create-charge calls stripe.paymentIntents.create() without idempotencyKey — network retries could create duplicate charges"`
- **Remediation:** Generate a unique idempotency key per transaction and pass it with every charge call:

  ```ts
  import { v4 as uuidv4 } from 'uuid'

  // Generate per-order, store with order record so retries reuse same key
  const idempotencyKey = `order-${orderId}-${uuidv4()}`

  const paymentIntent = await stripe.paymentIntents.create(
    {
      amount: amountInCents,
      currency: 'usd',
      customer: stripeCustomerId,
    },
    { idempotencyKey }
  )
  ```

  Alternatively, derive the key from a stable order ID so retries naturally reuse the same key: `idempotencyKey: \`pi-order-${orderId}\``.

## External references

- cwe CWE-362
- iso-25010 reliability.fault-tolerance

Taxons: data-integrity, error-resilience

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