Demo, all content is generated
Question

Stripe payment succeeds but my Bolt app never marks the order as paid

Solved · 428 views · asked by kwame_o · edited

Checkout works, I see the payment in Stripe (test mode). But the order in my Supabase table stays pending. Bolt made a webhook edge function stripe-webhook. In Stripe → Webhooks I see the events with a red 400:

Webhook signature verification failed. No signatures found matching the expected signature for payload.
What I’ve tried

Copied the webhook secret again from Stripe into Supabase secrets. Asked Bolt to fix the webhook, it changed constructEvent to constructEventAsync, still 400.

Comment
Does the function read the body with req.json() or req.text()? tobiasw · edited

3 answers

Marked as helpful by the asker
tobiasw · edited

constructEventAsync is correct for Deno, keep that. The usual remaining causes:

  1. Body was parsed before verifying. You must verify the raw text:
const body = await req.text();
const sig = req.headers.get("stripe-signature")!;
const event = await stripe.webhooks.constructEventAsync(
  body, sig, Deno.env.get("STRIPE_WEBHOOK_SECRET")!
);

If there's a await req.json() anywhere before this, that's the bug.

  1. Wrong secret. Each webhook endpoint has its own whsec_.... Test mode and live mode are different endpoints too. The one from stripe listen on your laptop is yet another.

  2. JWT check on the function. Stripe doesn't send a Supabase JWT. Deploy with --no-verify-jwt (or verify_jwt = false in config.toml) or Stripe gets a 401 before your code runs. You're seeing 400 so it's probably 1 or 2.

Comment
It was 1. There was a req.json() at the top to log the event type. Moved logging after verify, events are 200 now and orders flip to paid. kwame_o · edited
nils_tw · edited

Late, but add idempotency while you're there: Stripe can send the same event twice. Store event.id and skip if you've already processed it, otherwise you might send two confirmation emails.

Comment
chidi_eze · edited

For testing without clicking through checkout every time: stripe listen --forward-to <your function url> plus stripe trigger checkout.session.completed.

Comment