Integrations10 min readPublished September 2026

Your automation ran twice and the customer was charged twice

The platform is usually not the one repeating itself. Duplicates come from the source system, which documents that it may send the same event more than once, and from the replay button that re-runs every step on purpose.

The shape of the complaint

Two invoices with consecutive numbers and identical amounts. Two shipping labels for one order. The same customer thanked twice by the same automated email, four seconds apart. Sometimes a second charge.

The reflex is to look for a loop, and occasionally there is one. More often the automation ran exactly once per event and the event arrived twice. Or it ran once, and then somebody made it run again. Those are different faults with different fixes, and the logs tell them apart in about a minute if you know what you are looking at.

Is the platform quietly retrying behind your back?

Mostly no, and this is worth establishing first, because it is where people spend the afternoon.

Zapier's automatic retry is narrower than its name suggests:

When you enable autoreplay, Zapier will automatically replay any Zap run with an errored status.

That replays the errored run. The documentation is explicit that repeating everything is a separate option, which we come back to below. Note the "when you enable": autoreplay is a paid-plan feature, absent on Free, so on a Free account the retry you are reasoning about does not exist at all. Zapier also excludes the deliberately stopped ones:

It will not replay safely halted Zap runs.

Make behaves similarly when incomplete executions are switched on: the unfinished run is stored in the Incomplete executions tab, and when Make retries it, the scenario runs again starting with the module that caused the error - resumption at the failure point instead of a replay from the trigger. Worth knowing where that automation stops: Make retries a limited set of error types on its own, and other errors wait for a person. In n8n, the data handed to an error workflow carries execution.retryOf, which is only present when the execution is a retry of a failed execution - so a retry is identifiable as one.

The uncomfortable conclusion for the debugging session: if you were hoping to find a platform bug that duplicated your work, you will usually not find one. The duplicate came from one of the two places below.

Where does the second event actually come from?

From the system that sent it. The major sources document this openly, and it is the single most ignored sentence in commerce integrations.

Stripe:

Webhook endpoints might occasionally receive the same event more than once. You can guard against duplicated event receipts by logging the event IDs you've processed, and then not processing already-logged events.

The same page goes further, which matters because it breaks the naive fix:

In some cases, two separate Event objects are generated and sent.

So deduplicating on the event ID alone is necessary and sometimes insufficient: two genuinely different event objects can describe the same underlying thing. Stripe's own advice covers it:

To identify these duplicates, use the ID of the object in data.object along with the event.type.

Shopify says the same in one line of guidance:

Verify HMAC signatures and ignore duplicate deliveries using X-Shopify-Webhook-Id.

  • Webhooks, Shopify Developer Documentation

Read those together and the picture is clear. Both vendors warn that the same event can arrive more than once, and neither treats that as a defect to be fixed on their side. They are telling integrators that handling it is the integrator's job. A no-code scenario that starts with "webhook received, create invoice" has silently accepted a job it is not doing.

This is why the fix is not "make the sender stop". The sender is behaving as documented. Anything you build against a webhook, in any tool, has to survive receiving the same message twice.

Which button really does repeat everything?

The one that says so. Zapier draws the distinction explicitly:

You can replay all steps in a completed Zap run. Unlike replaying errors, when you replay all steps, the Zap will replay every single step in the Zap, including the trigger and all previous steps, regardless of their run status.

"Regardless of their run status" is the whole sentence. Steps that already succeeded run again: the charge that went through, the email that was sent, the row that was written. And the replay is recorded as its own run:

When you replay an entire Zap run, it will reuse your run's trigger data to generate a new run.

Which is exactly how this happens in practice. Something breaks on step 6. A person fixes step 6, wants the order finished, and replays the whole run to be safe. Steps 1 through 5 are perfectly happy to do their work a second time.

Why is a partial failure the dangerous shape?

Because the steps before the failure already changed the world, and nothing about re-running the automation un-changes them.

A run that fails at step 1 is harmless. A run that fails at step 6 has already taken payment, written to the CRM and sent a confirmation. Every option from that point is bad in a different way: leave it and the order is half-processed, repeat it and the customer pays twice, fix it by hand and nobody can reconstruct what happened next month.

This is the argument for putting the check in before you need it, because at the moment you need it you are choosing between two damages under time pressure.

What does the check look like without the jargon?

The word is idempotency, and the mechanic behind it is a bouncer with a guest list.

  1. Every incoming event has an identity. Stripe gives an event ID, Shopify gives X-Shopify-Webhook-Id, most others give something similar. If a source truly gives you nothing, build a key from fields that cannot repeat: order number plus event type plus timestamp.
  2. Keep a list of identities you have already finished. A sheet, an Airtable table, a Make data store, an n8n static data entry - anything that persists between runs and that you can search quickly.
  3. First step of the automation: look it up. Present in the list means stop, with a successful status. An error status here would invite the platform to retry the very thing you are trying to prevent.
  4. Last step: write it to the list. After the side effects - with one exception for money, below.

That is the whole pattern. It is three modules in any of these tools, and nothing in a webhook trigger puts it there for you.

Where exactly do you put the write?

After the irreversible part, and this ordering is the one detail that decides whether the pattern helps or hurts.

Write the key at the start and a run that dies halfway leaves the event marked as done: the retry is blocked, the order is half-finished, and now the automation cannot recover even in principle. Write it at the end and a run that dies halfway leaves the event unmarked: the retry repeats the completed steps, which is the problem you started with.

Neither is free, so pick by damage. When repeating a step is embarrassing but survivable - a duplicate row, a second Slack message - write the key at the end and let a rare double happen. When repeating a step touches money or a customer's inbox, split it: mark the event as in progress before the payment step, mark it done after, and let the in-progress state stop a second run while a human looks at it.

The half-finished run still needs a person. What the pattern is meant to buy is that it needs one person once, instead of a customer noticing.

One detail decides whether any of this works: checking the list and writing to it has to be a single operation. Two deliveries arriving within the same second will both read "not present", both pass the check, and both charge. Use a store that can refuse the second write - a unique constraint on the key column, so the duplicate fails instead of proceeding - rather than a lookup followed by an append.

Where money is involved, do not rely on your own list alone. Payment providers offer the check on their side, which is the only one that sits with whoever moves the money. Stripe documents it as "The API supports idempotency for safely retrying requests without accidentally performing the same operation twice. When creating or updating an object, use an idempotency key" (Idempotent requests, Stripe API Reference, read 18 August 2026).

How do you test it deliberately?

By causing the thing you are afraid of, on purpose, in a copy.

  • Send the same event twice. Re-post the identical webhook body to the scenario and confirm the second one exits at the lookup step with a success status.
  • Break it in the middle on purpose. Point step 6 at a bad credential, let it fail after the charge step, then replay and watch what step 3 does.
  • Replay all steps once, knowingly, on test data, to see exactly which actions repeat. This is the fastest way to discover which of your steps are irreversible, and the list is usually longer than expected.

Do this on a copy with its hands cut off - test credentials, no live email, no real payment. A clone pointed at production is a second machine wired to the same systems, which is its own way of processing everything twice.

What if it already happened?

Then the automation work and the cleanup work are separate, and only one of them is urgent.

The cleanup is manual by nature: find the duplicates, decide which side is authoritative, refund or void, tell the customers who were emailed twice. No amount of scenario editing performs this retroactively.

What is worth doing immediately is stopping the flow before fixing it. Turning the automation off costs an afternoon of manual orders; leaving it on while you debug costs another duplicate every time the event arrives. The order matters, because a scenario that is failing loudly is also a scenario that is likely to get switched off by the platform, and a switched-off automation is silent rather than fixed.

When this is a job to hand over

Adding a lookup and a write to a scenario you understand is an hour of work, and if the flow is small and the actions are reversible, do it yourself.

It stops being a small job when duplicates have already reached customers and somebody has to reconstruct which records are real, when the flow touches payment and the in-progress state has to be designed rather than bolted on, or when the same event fans out to several downstream systems that now disagree with each other. Those are scoped, finite jobs with a known shape and a known price, which is what Fix S and M are for.

Sources

  • Webhooks - Stripe Docs. Endpoints may receive the same event more than once; guard by logging processed event IDs; in some cases two separate Event objects are sent for the same occurrence.
  • Webhooks - Shopify Developer Documentation. Verify HMAC signatures and ignore duplicate deliveries using X-Shopify-Webhook-Id.
  • Replay Zap runs - Zapier Help Center. Autoreplay covers runs with an errored status and skips safely halted runs; replaying all steps repeats every step regardless of run status and creates a new run from the original trigger data.
  • Incomplete executions - Make Help Center, read 18 August 2026. A stored incomplete execution keeps the blueprint and the data being processed at the point of failure, and a retry restarts from the module that errored with the original input.
  • Error handling - n8n Docs. execution.retryOf is present only when an execution is a retry of a failed execution.

Integration dropping data between systems? Fix S — $300, 2 business days, fixed price.

Get my quote in 24h

Written by the Fixmation team.