We kept running into the same issue with n8n webhooks:
Stripe webhook fires → workflow runs → charge executes.
Then Stripe retries the webhook → workflow runs again → charge executes again.
Customer gets charged twice.
This happens because most webhook systems use at-least-once delivery.
If the provider thinks the webhook failed (timeout, network hiccup, 5xx), it retries the same event.
From n8n’s perspective this looks like a new trigger, so the workflow executes again.
The pattern we ended up using is adding idempotency before the side effect (Stripe / email / DB write).
What is the error message (if any)? No error message — the workflow simply executes twice because the webhook event is retried.
Please share your workflow
Example pattern:
Webhook → HTTP Request (gate) → IF → Stripe charge
The idea is to send a stable idempotency key before the risky action.
For Stripe webhooks this can be:
{{ $json.id }}
Share the output returned by the last node
Example response when a duplicate event is detected:
Easiest fix here is just passing {{ $json.id }} as an Idempotency-Key header on your Stripe API call, Stripe itself will reject the duplicate charge without needing any external service. n8n also has a built-in Remove Duplicates node you can throw before the charge node if you want to short-circuit the whole workflow earlier.
You don’t really need an external gate service for this, n8n has a Remove Duplicates node that does exactly this natively — just set it to “Remove Items Processed in Previous Executions” and use {{ $json.id }} as the value. Also if you’re making charges via HTTP Request you can just pass the Stripe event ID as an Idempotency-Key header and Stripe itself will reject the duplicate.
You don’t really need an external HTTP gate for this, n8n has a Remove Duplicates node with a “Remove Items Processed in Previous Executions” mode — just set the dedupe value to {{ $json.id }} from the Stripe event and it handles it natively. You can also pass the event ID as an Idempotency-Key header on your Stripe API call as a second safety net.
If you’re only protecting the Stripe call, using Stripe’s Idempotency-Key and the Remove Duplicates node is probably the simplest solution.
The reason we put a gate step before actions is when the same workflow does more than one side effect (for example charge + email + DB write) and we want one consistent idempotency check before any of them runs.
But yeah - for Stripe specifically, their idempotency header is definitely the first thing to use.