Stripe webhook · symptom

Can't map the event to a user (missing metadata / customer id)

what it looks like
Webhook returns 200 but the user is still on Free / updateMany count: 0 / Cannot read properties of undefined (reading 'userId')

What it means

The webhook ran but couldn't tell which of your users the event belongs to. Subscription events carry a customer id, not your user id, so you need a stored stripeCustomerId or metadata set when the Checkout Session was created.

Common causes

  • Checkout Session created without client_reference_id or metadata.userId
  • stripeCustomerId never saved on the user
  • A new Customer created per checkout (customer_email) instead of reusing one
  • Reading metadata from the subscription when you only set it on the session

How to fix it

  1. Create Checkout with client_reference_id: user.id (and subscription_data.metadata.userId if you need it on the subscription)
  2. On checkout.session.completed save session.customer as the user's stripeCustomerId
  3. Look up later subscription events by stripeCustomerId; pass customer for returning users
checkout route
await stripe.checkout.sessions.create({
  mode: "subscription",
  line_items: [{ price: process.env.STRIPE_PRICE_ID!, quantity: 1 }],
  client_reference_id: user.id,
  subscription_data: { metadata: { userId: user.id } },
  ...(user.stripeCustomerId ? { customer: user.stripeCustomerId } : { customer_email: user.email }),
  success_url: `${origin}/billing?success=1`,
  cancel_url: `${origin}/billing`,
});
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 --add checkout_session:client_reference_id=<test-user-id>
# the fixture is a one-time payment; also run one subscription checkout in test mode

pass if That test user's row gets the Stripe customer id, and a later subscription event finds the same user.

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.mdc (free Cursor Rules Pack): Sets client_reference_id (or metadata.userId) on every Checkout Session.

Prompt for Cursor or Claude Code

My Stripe webhook returns 200 but never updates the right user. Trace how a Checkout Session links to my user: add client_reference_id (and subscription_data.metadata.userId), save session.customer as stripeCustomerId on checkout.session.completed, and look subscription events up by stripeCustomerId. Log a warning when no user matches.

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