User Charged Twice After Retry
When a payment request times out or the response fails to reach the client, the client retries the request. If the payment API is called without an idempotency key, the provider has no way to recognise the retry as a duplicate and processes it as a second, independent charge — billing the user twice for the same order.
CriticalIntermediateAPI testingManual testingExploratory testing
// UNDERSTAND
// Symptoms
- Two separate charge records appear in the payment provider's dashboard for a single order
- The user receives two payment confirmation emails or two card statement entries
- The order appears once in the application but the card shows two charges of the same amount
- Customer support receives a double-charge complaint after a slow or failed checkout experience
// Root Cause
- The payment API call is retried without an idempotency key, so the provider treats each request as a new transaction and charges the card twice
- A network timeout causes the client to assume the first attempt failed and submit a second request, even though the provider had already processed and completed the original charge
- The retry logic does not check whether the order already has a successful payment record before submitting a new charge attempt
// Where It Appears
- Online checkout flows with automatic retry-on-timeout behavior
- Subscription billing systems that retry failed renewal charges
- Mobile payment flows where network interruption during checkout is common
- Backend payment processing jobs that retry on any non-success response
// REPRODUCE & TEST
// How to Reproduce
- 01Authenticate and create a test order; note the order ID (e.g. 789)
- 02Send POST /api/payments with the order ID and a test card token; record the charge ID from the response
- 03Without changing any field, send the identical POST /api/payments request again immediately — this simulates a retry where no idempotency key was included
- 04Record the charge ID from the second response — a different charge ID confirms a second, independent charge was created
- 05Check the payment provider's test dashboard for charges against the test card for order 789
- 06Confirm two separate charges of the same amount appear — the double-billing bug is present
// Test Data Needed
- A test account with a valid test card (e.g. Stripe test card 4242 4242 4242 4242)
- A way to send the same API request twice (Postman, curl, or any HTTP client)
- Access to the payment provider's test dashboard or API to verify the charge count
// Manual Testing Ideas
- Use DevTools Slow 3G throttle during payment and allow the client to retry; check for a second charge in the provider dashboard
- Send the same payment request twice using Postman without an idempotency key and verify the charge count
- Check whether the application reads the current order payment status before initiating a new charge on retry
- Review the payment provider's test logs for missing or reused idempotency keys
- Test the subscription renewal path: interrupt a renewal mid-request and confirm only one charge appears on retry
// API Testing Ideas
- Authenticate and create a test order; capture the order ID (e.g. 789)
- Send POST /api/payments with the order ID and test card token; record the charge ID from the response
- Send the identical POST /api/payments request again immediately, using the same order ID and card token but without an idempotency key header
- Record the charge ID from the second response — a different ID from step 2 confirms a second charge was created
- Query the payment provider's test API for all charges against the test card for order 789
- Assert the charge count is 1 — two charges confirm the duplicate-billing bug
// Automation Idea
Using a payment sandbox, send two identical POST /api/payments requests for the same order ID in rapid succession, both without an idempotency key. Query the payment provider API for charges on that order and assert the count is 1. Then repeat the test with an Idempotency-Key header on both requests and assert the provider returns the same charge ID for both, confirming deduplication works when the key is present.
// Expected Result
Retrying a payment request for the same order does not create a second charge. The application detects the duplicate and returns the result of the original transaction.
// Actual Result (Example)
Two POST /api/payments requests for order 789 each return 200 OK with a unique charge ID, and the payment provider's test dashboard shows two separate charges of $49.99 on the same test card.
// REPORT IT
Example Bug Report
- Title
- Double charge created when payment request is retried after a network timeout
- Severity
- Critical
- Environment
- Staging environment Chrome 124 Stripe test card 4242 4242 4242 4242 Order 789
- Steps to Reproduce
- 01Add a product to the cart and proceed to checkout with order 789
- 02At the payment step, open DevTools and set the network to Slow 3G
- 03Click the Pay button and wait for the timeout error to appear on screen
- 04Restore the network to normal speed in DevTools
- 05Click the Retry payment button
- 06Check the Stripe test dashboard for charges on the test card for order 789
- Expected Result
- Only one charge of $49.99 appears in the Stripe test dashboard for order 789.
- Actual Result
- Two charges of $49.99 appear in the Stripe test dashboard for order 789 — the user's card has been billed twice.
- Impact
- Customers are billed twice for a single order, requiring manual refund processing, incurring chargeback risk, and causing serious loss of trust in the payment experience.