## TL;DR

Stripe may deliver the same event more than once, so key every handler on the event id: store processed event ids and skip repeats before doing any business logic. Make the business operations themselves idempotent too (upserts, not inserts; guarded state transitions). Test by replaying the same event twice and asserting a single effect.

## Steps

1. Create a processed-events store (table or cache) keyed by the Stripe event id.
   Expected: Duplicates have somewhere to be recognized.
2. At the top of each handler, check the event id; if seen, acknowledge and return.
   Expected: Repeat deliveries exit before touching business logic.
3. Write business effects idempotently: upsert records, guard transitions (only draft to finalized once).
   Expected: Even a missed dedup cannot corrupt state.
4. Handle the race: two deliveries arriving simultaneously need an atomic check-and-set.
   Expected: Concurrent duplicates are safe.
5. Expire old event ids after your retry window plus margin (e.g. 30 days).
   Expected: The store stays bounded.

## When to use

- Invoice webhooks sometimes process twice
- You see duplicate fulfillment or accounting entries
- You need at-least-once delivery safety

## When not to use

- Events fail signature verification (different problem)
- The handler logic itself is wrong on first delivery
- You need exactly-once delivery (not offered; idempotency is the answer)

## Compatibility

Stripe webhooks at-least-once delivery; any datastore for event ids. Applies to all event types.

## Variant phrasings

### ### Stripe webhook duplicate event handling

### ### idempotent webhook handler Stripe

### ### same invoice webhook delivered twice

## Root cause

Stripe retries deliveries until your endpoint acknowledges, and network ambiguity means a successful processing can still look like a failure to Stripe, triggering a redelivery. At-least-once is the contract; idempotency is your half of it.

## Edge cases

- Different event types for the same invoice (created vs finalized) are not duplicates; dedup by event id, not invoice id
- Database transactions should wrap the dedup check and the effect together
- The 30-day event id expiry must exceed Stripe's maximum retry window

## Provenance

Resolved from the public thread: https://vectle.com/posts/pst_38X0Q6z9IaDoNbLHSqbpBA
