FIX-02 · STRIPE · SUPABASE EDGE FUNCTIONS

Stripe webhook not firing with Supabase Edge Functions: the checklist

Seven checks, in order, from "is Stripe even sending events" to "is the signing secret from the wrong endpoint". Most webhook failures fall to one of the first five.

Updated July 2026 · 6 min read · free self-serve checks inside

"The webhook isn't firing" is the most common self-diagnosis we hear from founders on the Lovable/Bolt + Supabase + Stripe stack — and it's usually slightly wrong in a useful way. Stripe almost always is firing. The event is leaving Stripe and dying somewhere on the way into your Supabase Edge Function. This checklist walks that path in order, so you find the break instead of guessing at it.

1. Confirm Stripe is generating events at all

In the Stripe Dashboard, open the Developers area (bottom-left — Stripe calls it Workbench) and select the Events tab. Trigger the thing you're debugging; a test-mode checkout works fine. You should see events appear — checkout.session.completed is the big one for payments. If events show up here, Stripe's side is alive and your problem is delivery. If nothing appears, checkout itself isn't completing, which is a different bug.

2. Confirm a real endpoint is registered — not a CLI listener

Switch to the Webhooks tab. You need an endpoint whose URL points at your deployed Supabase function. Two impostors show up constantly:

  • An empty list. The webhook handler exists in your code but was never registered with Stripe. Code alone does nothing — Stripe needs the URL.
  • A `stripe listen` CLI session. The Stripe CLI forwards events to your laptop while it's running, and stops when the terminal closes. If checkout "worked yesterday" on your machine and died in production, this is often why.

3. Check the URL character-for-character

Supabase Edge Function URLs look like https://YOUR-PROJECT-REF.functions.supabase.co/stripe-webhook (or the older /functions/v1/ form). Compare the URL registered in Stripe against the URL shown in Supabase → Edge Functions. The classic misses: pointing at a previous Supabase project, a typo'd function name, or a function that was renamed after the endpoint was created.

4. Read the response code on recent deliveries

Click the endpoint in Stripe and look at the delivery attempts. The code tells you which layer rejected it:

  • 401 — Supabase rejected the call before your code ran. Edge Functions require a login token (JWT) by default; Stripe doesn't send one. Fix in step 5.
  • 400 — your code ran and rejected the signature. Almost always a wrong whsec_... secret. Fix in step 6.
  • 404 — the URL is wrong or the function isn't deployed. Back to step 3.
  • 5xx or timeout — the function crashed or hung. Supabase → Edge Functions → your function → Logs has the actual error.

5. Turn off JWT verification for this one function

The webhook doesn't need a Supabase login — Stripe authenticates itself with a cryptographic signature instead, which your code checks in step 6. Tell Supabase to let the call through:

supabase/config.toml
[functions.stripe-webhook]
verify_jwt = false

Redeploy the function afterwards. If you deploy from the CLI, supabase functions deploy stripe-webhook --no-verify-jwt does the same job, and there's a per-function toggle in the Supabase dashboard too.

6. Match the signing secret to this exact endpoint

Every webhook endpoint has its own signing secret. Click the endpoint in Stripe, reveal the whsec_... value, and make sure that exact value is what your function reads — usually a Supabase secret named STRIPE_WEBHOOK_SECRET. The three classic mismatches: a secret from a deleted endpoint, a secret from the test-mode endpoint used in live mode, or the temporary secret that stripe listen printed in a terminal months ago.

One code-level gotcha while you're in there: signature verification needs the raw request body. If the function parses the JSON first and verifies afterwards, verification fails even with the right secret.

7. Check which events the endpoint subscribes to

On the endpoint's page, read the "Listening to" section. Endpoints only receive the event types selected when they were created. For payments you want at least checkout.session.completed; for subscriptions add customer.subscription.updated, customer.subscription.deleted and invoice.payment_failed. A perfectly healthy endpoint stays silent about events it was never asked for.

After the fix: replay what was missed

Stripe keeps every undelivered event. Once deliveries go green, use Resend on the failed events from the endpoint's page — customers who paid during the outage get processed as if nothing happened.

And if you've walked all seven and deliveries are green but your app still doesn't react, the break is on the far side: the database write. That's the "200 but nothing changed" failure — covered here — and it's the point where most founders call us.

STILL BROKEN? THAT'S THE HARD VERSION

Diagnosed free. Fixed for a flat $250. Proven live.

01 · FREE HEALTH CHECK

Describe the symptom in two minutes. You get a plain-English root cause within hours — yours to keep even if you fix it yourself. No call, no strings.

02 · THE $250 FLAT FIX

One broken flow, fixed within 48 hours of access — proven with a recorded live test transaction you watch, and covered by a 30-day warranty. Miss the deadline, pay nothing.