Stripe webhook · symptom

Duplicate events / missing idempotency

what it looks like
Same checkout.session.completed processed twice: two welcome emails, credits granted twice

What it means

Stripe delivers at least once. Retries after a timeout or non-2xx, and occasionally plain duplicates, mean the same event.id can arrive more than once. Handlers that increment, insert or send emails without checking will repeat the side effect.

Common causes

  • No record of which event.id values were already processed
  • Incrementing credits or inserting rows instead of writing current state
  • A slow handler that Stripe retried

How to fix it

  1. Store event.id with a unique constraint before doing side effects; skip if it already exists
  2. Prefer writing the current state (status = X) over increments or toggles
  3. Return 200 for duplicates so Stripe stops retrying
prisma/schema.prisma + route.ts
model StripeEvent {
  id        String   @id // evt_...
  createdAt DateTime @default(now())
}

// in the handler, after constructEvent:
try {
  await db.stripeEvent.create({ data: { id: event.id } });
} catch {
  return Response.json({ received: true, duplicate: true });
}
verify the fix
stripe listen --forward-to localhost:3000/api/stripe/webhook
# put the whsec_... it prints in .env.local, then restart npm run dev
stripe trigger checkout.session.completed
stripe events resend evt_...   # the evt_ id stripe listen printed
stripe events resend evt_...   # same id again

pass if All three deliveries return [200], the database changed once, and there's one stored row for that event id.

works with stripe-node 23.0.0 · Next.js 16.4.3 App Routerlast verified CLI steps: Stripe CLI 1.55.2, Oct 27, 2026

prevent it · stripe-webhooks.mdc (free Cursor Rules Pack): Records each event.id under a unique constraint and returns 200 for repeats without doing the work again.

Prompt for Cursor or Claude Code

Make my Stripe webhook idempotent. Add a StripeEvent table keyed by event.id (unique), insert it right after signature verification and return 200 early if it already exists. Change any increments or one-off side effects so a repeated event is harmless. Add a test that sends the same event twice.

Not quite your error? Paste yours into the free VibeBoot Doctor or browse the Stripe webhook fixer.

next: skip the next one

Skip the debugging. The VibeBoot SaaS Starter ships with this already handled: a signature-verified webhook (raw body + constructEvent) that re-fetches the subscription from Stripe before saving it, with Vitest tests. Plus auth and a database on Next.js 16.4.3. $49, one-time.

14-day money-back guarantee · lifetime updates

$ fixed it? get the next one first

New fixes for what AI agents break, by email.

When we add a fix or a Cursor rule for a new Next.js, Auth.js, Prisma or Stripe break, it goes out in the VibeBoot newsletter. Free, occasional, unsubscribe in one click.

Subscribe free