Fast answer: a duplicate Stripe event is not automatically a duplicate charge

A duplicate Stripe webhook delivery is not automatically a duplicate charge — the event is a notification, and the business effect depends on what your n8n workflow does after receiving it. A useful diagnosis separates repeated delivery of the same Event, two different Events describing one underlying change, a repeated API mutation, and an actual duplicate payment record. Log processed Event IDs, cryptographically verify the Stripe-Signature header against the raw payload and endpoint secret, and use idempotency keys on outbound POST requests — but treat a real duplicate charge as a verification-and-reconciliation problem, never an automated fix.

Source: Stripe Docs (docs.stripe.com)
Source: n8n Docs (docs.n8n.io)
Documentation verified 6 August 2026

Four different incidents, four different responses

Diagnosing a “duplicate Stripe event” starts with naming which of four incidents actually occurred. For the general mechanics of why webhooks redeliver at all in n8n, see why your n8n webhook fires twice; this guide focuses specifically on the Stripe payment-safety layer on top of that.

  • Same Stripe Event delivered more than once. Stripe documents that webhook endpoints can receive the same event more than once; the guard is to log the Event ID and skip IDs that have already been processed.
  • Two distinct Event objects describing the same change. Stripe notes this can happen separately from redelivery; the dedupe key here is the ID of data.object combined with event.type, not the Event ID.
  • Your integration makes the same API mutation twice. This happens on the outbound side — a retried POST request to Stripe’s API — and is addressed with an idempotency key, not webhook-side deduplication.
  • An actual duplicate charge or payment record. This is a real business outcome that needs verification against Stripe’s records and a controlled process, not an automated workflow fix.

Confusing these categories is the most common design mistake: treating every duplicate webhook delivery as if it produced a duplicate charge leads to either unnecessary alarm or, worse, automated “corrections” against a payment that was never actually duplicated.

Evidence checklist before you diagnose anything

Gather these before deciding which incident you’re looking at:

  • The Stripe Event ID (evt_...) for every delivery received
  • event.type for each delivery
  • data.object.id — the ID of the underlying Stripe object, such as a charge, payment intent, or invoice
  • Delivery attempt history from the Event deliveries tab, including HTTP status codes returned
  • Which webhook endpoint or endpoints are registered to receive this event type — duplicate registrations across more than one active endpoint will multiply deliveries by design
  • n8n workflow execution IDs and timestamps for each processing attempt
  • The API request ID and idempotency key, if any, for outbound POST calls your workflow made
  • The final object state and payment records in Stripe itself, retrieved directly rather than assumed from a webhook payload

Decision matrix: dedupe key and response by incident

Safest deduplication key and response for each Stripe duplicate incident
Incident Safest dedupe key Response
Same Event delivered again Event ID (evt_...) Log processed Event IDs; skip processing if the ID is already logged
Two Events, same underlying change data.object.id + event.type Compare against previously processed combinations for that object, not just the Event ID
Repeated outbound API mutation Stripe idempotency key representing one intended operation Reuse the same key on every retry of that operation; never generate a fresh key per retry
Suspected real duplicate charge Not a dedupe-key problem Verify directly in Stripe records, then use a controlled business or support process — no automated reversal

A secure receiving pattern for the n8n workflow

This pattern narrows the general approach in preventing duplicate webhook executions in n8n to Stripe’s specific event and object model.

  1. Verify the request came from Stripe before acting on it. Stripe signs every webhook with a Stripe-Signature header, and documents both signature verification and IP allowlisting as the two protections to use together; a generic field check on the payload is not equivalent to cryptographic signature verification.
  2. Accept only the event types the integration needs. Configure the webhook endpoint to send a narrow, named list of event types rather than listening to everything.
  3. Normalize a stable comparison key for the incident type you’re guarding against — the Event ID for redelivery, or data.object.id plus event.type for duplicate Event objects.
  4. Make an atomic “first processing” decision. Check and record the key in a way that can’t race with a second, near-simultaneous delivery of the same event — a plain read-then-write check in a workflow branch is not atomic and can let two overlapping executions both decide they’re first.
  5. Acknowledge promptly. Return a successful status before running complex logic that could time out; Stripe recommends returning a 2xx response before slower operations such as updating an invoice as paid in another system. MetaFlowKit’s guide to responding to webhooks immediately in n8n covers the n8n response-mode choices.
  6. Make the side effect itself retry-safe. Design the downstream action so that processing the same event twice — because acknowledgment and durable recording aren’t the same instant — doesn’t double the business effect.

Stripe API idempotency for outbound requests from n8n

Webhook-side deduplication and API idempotency solve two different directions of the same problem. Webhook deduplication protects against Stripe sending your workflow the same notification twice. API idempotency protects against your workflow sending Stripe the same instruction twice — for example, retrying a charge or payment intent creation call after a timeout or connection error.

Stripe’s API accepts an idempotency key on POST requests. When you provide the same key on a retried request, Stripe returns the result of the original request instead of repeating the operation, including if the original request resulted in an error. A client generates this key; Stripe suggests a V4 UUID or another string with enough entropy to avoid collisions.

Current documented constraints worth designing around: idempotency keys can be up to 255 characters long, Stripe checks that the parameters of a retried request match the original request made with the same key, keys should not contain sensitive data such as email addresses, and Stripe may prune keys automatically after they are at least 24 hours old — after which a reused key starts a new request rather than returning the old result. Treat that 24-hour window as a retry-safety mechanism, not as a permanent record of what your integration has already done.

The design detail that trips people up: an idempotency key must represent one intended operation. Generating a fresh random key for every retry defeats the entire point, because Stripe then has no way to recognize the retry as the same operation. Reuse the same key across every retry attempt of a given intended mutation, and generate a new key only when you actually mean to perform a new, distinct operation.

Why concurrency limits, filters, and read-then-write flags fall short

Three patterns look like duplicate protection but don’t fully cover payment-safety needs on their own.

An n8n concurrency limit — including a limit of one production execution at a time — serializes execution at the instance level. It does not recognize that two separate executions represent the same Stripe event; it only controls how many run simultaneously. Two retried deliveries arriving minutes apart still each get their own execution and each pass through your logic independently.

A filter node that checks “have I seen this ID before” against data already visible to the workflow run is only as good as what that run can see. It doesn’t protect against two overlapping executions each reaching the filter within the same short window, before either has recorded the ID anywhere durable.

A simple read-then-write flag — read a status field, decide it’s unprocessed, then write “processed” — has a race condition built in. If two executions perform the read step before either performs the write step, both conclude they’re first. This is the core reason the receiving pattern above calls for an atomic first-processing decision rather than a sequential check.

For durable, atomic protection on high-value payment actions, an n8n Remove Duplicates node checking data within a single execution’s input is not a substitute for storage with an atomic unique constraint and a reconciliation process — that’s a production design recommendation, not a claim that Remove Duplicates is broken. See Remove Duplicates versus idempotency in n8n for how the node’s documented scope compares to true idempotency.

Verification procedure using test mode

Test the receiving pattern before production traffic reaches it, using Stripe’s test mode or synthetic identifiers rather than real payment data. This assignment did not run these checks in a live environment — treat the following as what to verify, not a report of results:

  • A duplicated delivery of the same test Event is recognized and skipped on the second attempt
  • Two Events sharing the same data.object.id and event.type are recognized as related
  • An intentionally retried outbound POST call with the same idempotency key returns the original result rather than creating a second object
  • The workflow still acknowledges promptly with a 2xx response even when downstream logic is deliberately slowed
  • Signature verification correctly rejects a request with a missing or incorrect Stripe-Signature header

Recovery and reconciliation when a real duplicate charge is suspected

If evidence points to an actual duplicate charge — two distinct successful payment records tied to what should be one transaction — the response is verification and a controlled business process, not an automated workflow action. Gather the evidence listed earlier (Event IDs, data.object.id values, execution IDs and timestamps, and the current object state pulled directly from Stripe) before concluding a duplicate actually occurred.

Do not configure a workflow to automatically refund, cancel, dispute, or otherwise alter a real payment in response to a suspected duplicate. Route confirmed duplicates through your existing support or finance process, where a person can confirm the evidence and take the appropriate action directly in Stripe. This keeps a diagnostic false positive from turning into an unintended refund.

If the concern runs the other way — events look missing rather than duplicated — remember that Stripe automatically retries failed webhook deliveries for a period of days in live mode before giving up. Rather than assuming an event was lost, use the List Events API with delivery-status filtering to find and manually process any events your endpoint never successfully received, instead of reconstructing them by hand from partial data.

Rollback and safety note

Frequently asked questions

Does Stripe guarantee webhook events arrive in order?

No. Stripe documents that it does not guarantee delivery order, so don’t design correctness around receiving events in a specific sequence — retrieve related objects directly from the API when order matters.

Is checking a generic field on the payload the same as verifying a Stripe signature?

No. Signature verification uses the Stripe-Signature header, an HMAC-SHA256 scheme, and your endpoint’s signing secret. A field check on the JSON body doesn’t confirm the request actually came from Stripe.

Should I generate a new idempotency key every time I retry a request?

No. Reuse the same key for every retry of one intended operation. A new key on each retry defeats Stripe’s ability to recognize the retry and can allow the operation to repeat.

Can an n8n concurrency limit of one prevent duplicate charges by itself?

Not completely. It serializes execution at the instance level but does not deduplicate events or requests — retries and distinct executions can still repeat a side effect.

What should I do first if I suspect a customer was charged twice?

Gather the evidence — Event IDs, object IDs, execution timestamps, and the current record in Stripe — then route it through your support or finance process rather than triggering an automated refund.

Sources and change log

Change log: Initial publication, 6 August 2026.