Blog · · 3 min read

How do you prevent duplicate orders when an agent retries checkout?

Separate request idempotency, payment state and fulfillment deduplication, then test uncertain responses in a payment sandbox.

A checkout request times out. The shopper's assistant cannot tell whether the order exists, so it tries again. Unless the merchant has designed for that uncertainty, one purchase intent can become two orders.

Preventing this requires a contract between the caller and the merchant, plus durable handling inside the merchant's system. A payment provider's idempotency feature covers its documented requests. It does not automatically deduplicate your inventory reservation, order email or shipment.

Give one purchase attempt a durable identity

Create a merchant-owned identifier for the purchase intent before starting the payment request. Associate it with the intended cart, customer context and operation state. Decide when changed details mean a new intent rather than a retry.

Keep an existing intent available for lookup after a timeout. The caller should have a documented way to ask "what happened to this attempt?" without making another purchase. Return a status that distinguishes an in-progress operation from a completed or rejected one.

These are application design recommendations. Your payment provider's contract determines which requests can be safely repeated and for how long. Do not assume an arbitrary HTTP client automatically supplies the same identifier on a retry.

Use the provider's idempotency contract accurately

Stripe documents using idempotency keys when creating or updating objects. Its current page distinguishes API v1 behavior from API v2 behavior. For API v1, it stores the first status and body after endpoint execution begins and returns that saved response for the same key, including a 500 response. API v2 has different retry semantics. Stripe idempotent requests.

For an integration using API v1, repeated attempts for the same logical operation should reuse the intended key under Stripe's rules. Generating a fresh key after an uncertain timeout defeats the purpose of deduplicating that operation.

Keep provider keys and request state on the server. The agent should not need your secret API credential. Document the merchant-facing operation rather than exposing an internal payment request as a browser workaround.

Deduplicate fulfillment separately

A payment can finish even if the browser never reaches your success page. Stripe explicitly warns against relying only on the checkout landing page for fulfillment. Its guide uses webhooks to cover payment completion and a redirect to improve the immediate customer experience. Stripe fulfillment guide.

Make both paths converge on the same merchant order. Guard the transition to fulfillment with durable storage so concurrent calls cannot each ship the item. Track the outcome of each fulfillment step, especially if the shipping provider accepts work before your process crashes.

Stripe also documents duplicate webhook deliveries and events arriving without a guaranteed order. Recording processed event identifiers helps with repeated delivery; some cases require considering the underlying object and event type as well. Stripe webhook guidance.

Do not translate a duplicate event into a second business action. A webhook receipt and an order shipment are different records with different purposes.

Walk through the uncertain cases

Use test payments and disposable order data. Start with one cart and one intended order. Save the merchant attempt ID, provider session ID and resulting order ID in the test record, without storing sensitive payment details.

Run these cases deliberately:

  1. Submit the same purchase intent twice in close succession.
  2. Let the server receive the payment response, then simulate losing the client response.
  3. Deliver the same webhook twice.
  4. Trigger the return page and webhook handler at roughly the same time.
  5. Change the cart after an attempt has started and verify the published policy for that change.

For each case, inspect order count, payment state and fulfillment count independently. Seeing one successful browser page is not enough. Check whether an email or shipment was duplicated even when the order table contains one row.

Tell the caller when to stop

An agent needs a useful answer while the merchant is still reconciling payment state. Give it a lookup identifier and an explicit instruction to check status. Do not label an unresolved outcome "failed" if that might encourage another purchase.

If the provider reports a final rejection, distinguish that from missing evidence. Your support team should be able to find the purchase intent without asking the customer to start checkout again. Keep the customer-visible status consistent with the merchant's durable record.

Store the sandbox results with the checkout integration's release notes. They should identify the tested provider API version and the merchant code revision, so a later change can repeat the same cases against a known baseline.

Release when the recorded cases produce one intended order and the expected fulfillment work, including after a lost response. Keep the lookup route documented for the client that will handle the next timeout.

Sources

Check your site

See what an agent finds on your site, and which of these problems you have.