Stripe can deliver the same webhook event more than once. Without idempotency, your app may double-charge, double-provision, or double-send emails.
By Contributor · published 5/30/2026
Stripe’s own documentation is explicit: *“Webhook endpoints might occasionally receive the same event more than once.”* This means any handler that doesn’t check whether it has already processed a given event ID can trigger duplicate actions — activating a subscription twice, sending a welcome email twice, or provisioning entitlements twice.
Additionally, Stripe does not guarantee delivery order. A `subscription.updated` event can arrive before `subscription.created`.
**The idempotency pattern:**
```jsx
// 1. Verify the signature first (raw body required)
const event = stripe.webhooks.constructEvent(
req.rawBody, // must be raw, not parsed JSON
req.headers['stripe-signature'],
process.env.STRIPE_WEBHOOK_SECRET
);
// 2. Check if already processed
const { data: existing } = await supabase
.from('processed_stripe_events')
.select('id')
.eq('event_id', event.id)
.single();
if (existing) {
return res.status(200).json({ received: true }); // already done
}
// 3. Process the event, then record it
await processEvent(event);
await supabase.from('processed_stripe_events').insert({ event_id: event.id });
// 4. Respond within 10 seconds — move heavy work to a queue
return res.status(200).json({ received: true });
```
Three additional gotchas documented by practitioners:
- Use the **live mode** signing secret in production, not the test mode secret. They are different values.
- Do not trust data in the webhook payload. [Re-fetch the object from the Stripe API](https://dev.to/jordan_sterchele/why-your-stripe-webhooks-are-silently-failing-and-how-to-fix-all-of-it-aio) before acting on it.
- Respond within 10 seconds. Move slow processing (email sends, PDF generation) to a background queue.
## Why it matters
A double-charge or a missed subscription cancellation can cost you customers and create legal exposure. Stripe will retry failed webhooks multiple times — a non-idempotent handler turns every retry into a bug.
## Suggested next action
Add a `processed_stripe_events` table to your database and check event IDs before processing. Confirm your production environment uses the live mode signing secret.