Usually because the webhook points to the wrong URL, fails its signature check, or is blocked by database rules when it writes.

In live mode, Stripe retries a failed webhook for up to three days. Signature checks need the raw request body, so parsing it first makes every event fail.

Source: Stripe docs, Webhooks, read 3 Nov 2026

The usual causes

  • The endpoint is registered in a sandbox or test mode only, or still points to a preview URL.
  • The signing secret in your environment is from a different endpoint or mode, so every event fails verification.
  • The handler parses the body as JSON before verifying it. Stripe signs the raw body, so the check fails.
  • The handler is a Supabase Edge Function with JWT verification left on, the default. Stripe sends no Supabase token, so every call gets a 401. Set verify_jwt = false for that function, or deploy it with --no-verify-jwt, and rely on the Stripe signature check instead.
  • In an Edge Function, the code reads the body with req.json() or calls constructEvent. Read it with await req.text() and verify it with constructEventAsync and Stripe.createSubtleCryptoProvider(), as in the Supabase example.
  • The handler writes with the anon key, and row-level security blocks the write.
  • The table is new and has no grants. Since 30 October 2026, Supabase no longer grants new tables to the API roles, so the write fails with a 42501 permission denied error, even with the service_role key.
  • The function times out or returns an error, and nothing logs it.
  • The app listens for the wrong event, for example checkout.session.completed but not invoice.paid for renewals.

How a reliable setup looks

  • One endpoint per environment, each with its own signing secret.
  • Signature verified on the raw body before anything else.
  • Writes done server-side with a key that never reaches the browser.
  • Each event ID stored, so a retried event is not processed twice.
  • A 2xx response only after the event is safely stored, with slow work moved to a queue so Stripe does not time out.
  • A way to compare Stripe and your database, and fix any that disagree.

Where to look first

In the Stripe dashboard, open Workbench, then Webhooks, pick your endpoint and open Event deliveries. Click a failed delivery to see its HTTP status code. A 400 usually means a signature problem, a 401 or 403 an auth problem, a 404 a wrong URL, and a 500 an error in your handler.

In a sandbox, Stripe retries a failed event only three times over a few hours, so a broken test endpoint goes quiet fast. You can resend an event from the dashboard for up to 15 days.

Sources

First published on badii.studio. I review and fix apps built with Lovable, Bolt and other AI tools before launch. If yours has a bug like this, book a Tech Check.