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.
High because duplicate payment execution from retries is a common real-world event that causes immediate financial and trust damage requiring manual 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:
// 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.
saas-api-design.api-security.idempotency-keyshighIdempotency-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?idempotencyKey parameter) consistently.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.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.