New stores: 14 days of Protector freeFirst 25 stores.Uptime, daily scans, Slack and Telegram alerts. No card.See what’s included

Technical

Duplicate orders from webhook retries: the idempotency bug that double-charges customers

Richard K.

Richard K. · October 2, 2026 · 9 min read

Duplicate orders from webhook retries: the idempotency bug that double-charges customers

A customer emails your support inbox with a bank statement showing two identical charges for one hoodie. Your order list shows two orders, same items, same minute, same customer. Nothing looks obviously broken. Your site was up, checkout worked, payment processed. And yet you now owe someone a refund and an apology, and you have no idea how many other customers this happened to quietly, without emailing you at all.

This is one of the least understood failure modes in ecommerce, and it has nothing to do with your site crashing. It happens because something in your stack, usually a webhook, did its job too well.

What a webhook retry actually is

When a customer pays, your store and your payment gateway (Stripe, PayPal, Shopify Payments, Authorize.net) don't just complete the transaction and move on silently. The gateway sends a webhook, a small HTTP notification, to your store or app telling it "payment succeeded, go create the order." Apps that sync orders to a fulfillment tool, accounting system, or marketing platform work the same way: event happens, webhook fires, receiving system reacts.

Webhooks are sent over the open internet, and the internet is unreliable in small, routine ways. A server might be slow to respond. A network blip might delay the response. The receiving app might be mid-deploy and take an extra two seconds to answer. Whatever the cause, if the sender doesn't get a fast, clean "200 OK" back, it assumes the delivery failed.

And then it does exactly what it's designed to do: it tries again. Most payment and ecommerce platforms retry webhooks automatically, often multiple times, with increasing delays. Shopify's webhook documentation describes this retry behavior explicitly, as do Stripe's and most other major processors. Retries are a feature, not a bug. They exist because losing an order notification silently would be worse than sending it twice.

Where it turns into a double charge

The retry itself isn't the problem. The problem is what happens on your end when the same webhook arrives twice.

Imagine a checkout app that receives a "payment.succeeded" webhook and, in response, calls your store's API to create an order and simultaneously triggers a charge capture on a held payment. If that webhook arrives a second time, because the first response was slow and the gateway assumed it failed, a naive implementation processes it again: a second order, and in worse cases, a second charge capture against the same customer.

This is especially common with:

Apps that hold a payment authorization and capture it only after some internal step completes (inventory check, fraud screen, manual approval). If the capture step doesn't check whether it already captured that authorization, a retried webhook captures it twice.

Custom integrations built in-house or by a freelance developer, where "handle the webhook" was implemented as "run this function every time a POST request arrives," with no memory of which events have already been seen.

Multi-app stacks where more than one app listens for the same order-created event. If two apps both try to reconcile the same order against the gateway, you can get duplicate fulfillment actions even without a true double charge.

The common thread is a missing concept called idempotency: the property that doing the same operation multiple times produces the same result as doing it once. Payment gateways design their APIs to be idempotency-friendly (Stripe, for instance, supports idempotency keys on charge requests), but that only helps if the app calling the API actually uses them correctly.

Why this is a monitoring problem, not just a dev problem

Most store owners never see this bug directly. It surfaces as a support ticket, a chargeback, or a customer leaving a one-star review about being billed twice. By the time you hear about it, the charge has already happened, and you're doing damage control instead of prevention.

This is similar in spirit to other silent order-flow failures worth understanding, like the order that never arrived in your admin, where a webhook failure drops an order instead of duplicating it. Duplicate orders and missing orders are two sides of the same unreliable-delivery coin: one is the system failing to notice a success, the other is the system failing to notice a repeat.

Stores running subscription billing face a related risk, since a mishandled retry on a recurring charge event can double-bill a customer on their renewal date instead of just their initial purchase, a problem covered in more depth in subscription billing failures. And if you're running a flash sale or traffic spike, webhook volume spikes along with order volume, which is exactly when timeout-driven retries become more likely, a dynamic explored in when your apps throttle you.

A retry is the system being honest about uncertainty. The bug is pretending that uncertainty doesn't exist.

What to ask your apps and gateway

You don't need to read your app's source code to protect yourself. You need to ask the right questions, in writing, and get specific answers.

Ask your payment gateway: "Do you support idempotency keys on charge and capture requests, and do your webhook payloads include a unique event ID I can use to deduplicate?" Stripe and most major processors will say yes and point you to documentation. If a provider can't answer this clearly, treat that as a warning sign.

Ask every app that touches orders or payments: "If I receive the same webhook twice, what happens?" A competent vendor will describe event ID tracking, a deduplication window, or a stated retry-safe design. A vague "that shouldn't happen" is not an answer, it's an admission they haven't tested it.

Ask about your own custom integrations: if a developer built something in-house that listens for Shopify, WooCommerce, or BigCommerce order or payment webhooks, confirm it logs the event ID of every webhook it processes and skips any ID it has already seen. This is a small amount of code. Its absence is a large amount of risk.

Finally, build in a way to notice if it happens anyway. Order volume and revenue patterns that don't match expectations, two orders placed seconds apart with identical contents, or a spike in refund requests are all signals worth watching. Continuous order-flow monitoring, the kind Cassian™'s Order Pulse feature is built for, won't prevent a bad idempotency implementation from firing twice, but it will flag the resulting anomaly (a sudden cluster of near-identical orders, or an unusual gap followed by a burst) faster than waiting for a customer email. The Cassian Score™ also rolls order-flow health into a single number, so a pattern like this doesn't get lost among the dozens of other metrics you're half-watching.

Frequently asked questions

Why did my customer get charged twice for one order?
A customer gets charged twice for one order when a payment gateway or app sends a webhook notification, doesn't receive a fast confirmation back, and automatically retries it, and the system receiving that webhook processes the retry as a brand new event instead of recognizing it as a repeat. This is a documented, standard behavior of webhook-based integrations on Shopify, WooCommerce, BigCommerce, and most payment gateways. The fix is on the receiving end: it needs to track unique event IDs and skip ones it has already handled, a property called idempotency.
How do I stop duplicate orders from webhook retries on Shopify, WooCommerce, or BigCommerce?
Stopping duplicate orders from webhook retries requires the app or integration receiving the webhook to check a unique event ID against a log of previously processed events before taking any action. Ask every app in your stack that touches orders or payments whether it does this, and ask your payment gateway whether it supports idempotency keys on charge and capture requests. If you use a custom-built integration, confirm with your developer that it deduplicates by event ID rather than reacting to every incoming request as new.
Is a webhook retry itself a bug I should report to my app provider?
No, a webhook retry on its own is normal, expected behavior built into most payment gateways and ecommerce platforms specifically to prevent lost order or payment events. The actual bug, if one exists, is in how the receiving app or integration handles a retried webhook, specifically whether it recognizes the event as a duplicate before creating a second order or capturing a second charge. Report the duplicate order or charge outcome to your app provider, and ask them directly how their system handles repeated webhook deliveries.

The quiet fix that matters most

Nobody budgets time for idempotency handling because it's invisible when it works and only becomes visible as a refund request when it doesn't. But it's a small, well-understood engineering problem with a well-understood solution: track event IDs, skip repeats, done.

The real work for a store owner isn't writing that code yourself, it's confirming someone already has, for every app standing between a customer's card and your order list. Ask the question once, get it in writing, and move on to the next thing that can quietly go wrong, like the order that never arrived at all.

Keep reading