Stopping duplicate Stripe charges in n8n (webhook retries) — simple idempotency pattern

Stopping duplicate Stripe charges in n8n (webhook retries) - simple idempotency pattern

Describe the problem/error/question

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:

decision = BLOCK
policy_hits = [“idempotency.duplicate”]

Information on your n8n setup

  • n8n version: latest
  • Database (default: SQLite): SQLite
  • n8n EXECUTIONS_PROCESS setting (default: own, main): default
  • Running n8n via (Docker, npm, n8n cloud, desktop app): n8n Cloud
  • Operating system: Linux

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.

Yeah, totally fair point.

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.