If a customer pays and your app stays locked, start with the endpoint in Stripe: in Workbench, open it under Webhooks, then the Event deliveries tab, which shows the status code your endpoint answered. In a Lovable or Bolt app, that endpoint is usually a Supabase edge function, which runs on Deno, and four things break it: Supabase refuses the request before your code runs (a 401), the code checks the signature in the synchronous way Deno does not support, the body is parsed before the check, or the secret the function reads is missing or belongs to another endpoint.
What the status in Stripe tells you
| Status in Stripe | What happened | What to do |
|---|---|---|
| 401 | Supabase refused the request before your code ran: the function expects a JWT, and Stripe sends none | Turn off JWT verification for this function |
| 400 | Your function ran and rejected the signature | Read the message in the function's logs |
| 404 | Nothing answers at that URL: the function has another name, or it was removed | Copy the function's URL into the endpoint again |
| 500, or a timeout | Your code failed, or took too long, after the check | Read the function's logs |
| 200, and the app is still locked | Your function accepted the event and did not unlock the user | Read the function's logs, and check it handles this event type |
The function's logs are in Lovable under More → Cloud → Edge functions, then View logs; in Bolt under the database icon → Server Functions → View Logs; and with your own Supabase project, on the function's page in the Supabase dashboard.
401: Supabase expects a JWT that Stripe never sends
By default, Supabase checks every request to an edge function for a valid JWT in the Authorization header and refuses one without it before your code runs, with {"code": 401, "message": "Missing authorization header"}. Stripe proves its requests with a signature instead, and Supabase's own guide names Stripe webhooks as the case for turning the check off:
# supabase/config.toml: the name in brackets is the function's folder
[functions.stripe-webhook]
verify_jwt = false
The same switch is supabase functions deploy stripe-webhook --no-verify-jwt on the command line, or the JWT verification toggle on the function's page in the Supabase dashboard. In Lovable or Bolt, ask in the chat to turn off JWT verification for the webhook function; Bolt's troubleshooting guide gives exactly this fix. With the check off, anyone can call the function, so the Stripe signature check is its only lock: keep it.
400: the signature check fails
The message in the function's logs names the cause:
- SubtleCryptoProvider cannot be used in a synchronous context. The code calls
stripe.webhooks.constructEvent(). In Deno, stripe-node computes the signature with Web Crypto, which has no synchronous API. Useawait stripe.webhooks.constructEventAsync(). - No signatures found matching the expected signature for payload. The body was parsed before the check, or the secret is not this endpoint's. Read the body with
await req.text(), neverawait req.json(), pass that text on, then check the secret, below. - No webhook secret value was provided. The variable the code reads is empty where the function runs.
Every Stripe signature message, with its causes, is on our page about Stripe's signature errors.
Where the signing secret lives
The function reads the secret by name, for example Deno.env.get("STRIPE_WEBHOOK_SECRET"), so a secret stored under another name is as good as none.
| Your app | Where its functions' secrets are |
|---|---|
| Lovable, built-in backend | More → Cloud → Secrets |
| Lovable or Bolt with your own Supabase project | Supabase dashboard, Edge Functions → Secrets |
| Bolt Database | The database icon → Secrets |
| Supabase from the command line | supabase secrets set --env-file .env, with a .env file that git ignores |
Functions read a new secret right away, without a redeploy. The value must be the whsec_ of the endpoint that sends the events, in the same mode: a test-mode endpoint and a live one have different secrets, and so do stripe listen and each Dashboard endpoint.
A webhook function that passes
# supabase/config.toml
[functions.stripe-webhook]
verify_jwt = false
// supabase/functions/stripe-webhook/index.ts
import Stripe from "npm:stripe@23";
const stripe = new Stripe(Deno.env.get("STRIPE_SECRET_KEY")!);
Deno.serve(async (req) => {
const body = await req.text(); // the exact body Stripe signed
const signature = req.headers.get("stripe-signature") ?? "";
let event;
try {
event = await stripe.webhooks.constructEventAsync(body, signature, Deno.env.get("STRIPE_WEBHOOK_SECRET")!);
} catch (err) {
console.warn(`Stripe webhook rejected: ${(err as Error).message}`); // never log the secret or the header
return new Response("invalid signature", { status: 400 });
}
// Skip event.id if you have handled it already, then update the user.
return new Response("ok", { status: 200 });
});
Lovable: built-in payments and your own Stripe account
With built-in payments, Lovable registers two endpoints for each environment on the Stripe account: your app's, whose URL ends in ?env=sandbox or ?env=live, and a monitoring endpoint of its own. Leave both alone. Lovable's documentation is blunt about it: delete or disable your app's endpoint, and purchases still succeed while subscriptions never activate. An endpoint you add yourself with the same URL delivers events your app cannot verify, and removing it can fix delivery.
With your own Stripe account, Lovable sets up no webhook by default: the app asks Stripe whether the signed-in user has an active subscription, matched by email address. A customer who paid with another address than the one they sign in with stays locked out. If you asked Lovable for a webhook, you create the endpoint in Stripe and give Lovable its signing secret yourself.
Bolt
Bolt's Stripe integration writes the checkout and webhook functions, registers the webhook endpoint on your Stripe account and verifies the signatures, so there is nothing to set up in the Stripe dashboard. When deliveries fail anyway, the function's logs are under the database icon → Server Functions → View Logs, a secret the code expects but nobody created shows up as "Missing secrets", and a 401 is the JWT check above: ask Bolt to turn it off for the webhook function.
How PayHook helps
PayHook, the webhook inspector we are building, gives your project its own endpoint for Stripe events, checks each signature as it arrives, and follows each subscription's events to say whether access should be on and why, with a fix prompt you can paste into the chat of Lovable or Bolt. PayHook is in closed beta; the home page has the details.
Sources
- Supabase documentation, Function configuration, Edge Function 401 error response, Securing Edge Functions and Secrets, and Supabase's Stripe webhook example, checked on 9 October 2026.
- Lovable documentation, Connect your own Stripe account, Add payments to your app, Edge functions and Secrets, checked on 9 October 2026.
- Bolt documentation, Stripe for payments, Integrations issues and Secrets settings, checked on 9 October 2026.
- Stripe documentation, Manage webhook endpoints, checked on 9 October 2026; the synchronous and asynchronous checks: PayHook's own run of stripe-node 23.0.0 in its Web Crypto build under Node and under Bun 1.3.4, 9 October 2026.