Who sees this
You take payments with Stripe and your webhook runs in a Supabase Edge Function. This is common in apps built with Lovable or Bolt, and in apps where an AI tool copied a Node.js webhook example into Deno. The same error appears on Cloudflare Workers, Vercel Edge and Bun.
What you notice is usually not the error itself. You notice that a customer paid but their plan or order didn't update, or that every delivery in Stripe's Event deliveries tab shows a 400 or 401.
Why this happens
- In Node.js, Stripe's library signs with Node's own crypto module, which can work synchronously. That's why
constructEventworks in Next.js and Express examples. - In Deno and other edge runtimes, the library uses the Web Crypto API instead. Stripe's source code throws exactly this message whenever that provider is used from a synchronous function: "SubtleCryptoProvider cannot be used in a synchronous context."
- Newer versions of the library add a second line to the message: "Use
await constructEventAsync(...)instead ofconstructEvent(...)".
The trap: many webhook handlers wrap the check in try/catch and answer "Invalid signature" for any error. So the logs say the signature is wrong, and people re-paste the signing secret again and again. The secret is fine. The code never gets as far as checking it. Read the full error in your function logs before you change the secret.
How to fix it, step by step
1. Find the real error
In Supabase, open Edge Functions, select your webhook function and open Logs. Look for "SubtleCryptoProvider cannot be used in a synchronous context". If your handler hides the error, log err.message (never the secret) and resend one event from Stripe.
2. Switch to the async check
Replace the synchronous call with the async one. This is the setup in Supabase's own Stripe webhook example:
import Stripe from 'npm:stripe@^22'
const stripe = new Stripe(Deno.env.get('STRIPE_SECRET_KEY')!)
// Needed to use the Web Crypto API in Deno
const cryptoProvider = Stripe.createSubtleCryptoProvider()
Deno.serve(async (req) => {
const signature = req.headers.get('Stripe-Signature') ?? ''
// Use the raw text body. Don't call req.json() before verifying.
const body = await req.text()
let event: Stripe.Event
try {
event = await stripe.webhooks.constructEventAsync(
body,
signature,
Deno.env.get('STRIPE_WEBHOOK_SECRET')!,
undefined,
cryptoProvider
)
} catch (err) {
console.error('Stripe webhook check failed:', (err as Error).message)
return new Response((err as Error).message, { status: 400 })
}
// Handle event.type here, then answer 200
return Response.json({ received: true })
})Three details matter:
await.constructEventAsyncreturns a promise. Withoutawait,eventis a promise,event.typeis undefined and nothing gets handled.- Raw body. Use
await req.text(). Parsing JSON first changes the body, and the signature check then fails with "No signatures found matching the expected signature". - Old imports. Older examples import Stripe from
esm.sh(for examplestripe@14?target=deno). They have the same rule: use the async check. You don't need to change the import to fix this error.
3. Keep the function open to Stripe
Stripe can't send a Supabase login token. If the function still checks for one, Stripe gets a 401 before your code runs. Set this in supabase/config.toml, for the webhook function only:
[functions.stripe-webhook]
verify_jwt = falseUse your function's real name. The Stripe signature check from step 2 is what turns away requests that didn't come from Stripe. More detail: Stripe webhook 401 "Authentication Required".
4. Redeploy and resend
Deploy the function again (supabase functions deploy stripe-webhook, or publish from your AI tool). Then in Stripe, open Workbench → Webhooks, select the endpoint, open Event deliveries, and resend one failed event. It should show 200.
5. Catch up on missed events
While the webhook was failing, paid customers may not have been upgraded. In live mode Stripe retries a failed delivery for up to three days. Anything older won't come back by itself. Check the failed deliveries in the Event deliveries tab, resend the ones that matter, and check those customers in your app.
Fix prompt for your AI builder
Paste this into Lovable, Bolt, Cursor, Replit or whichever tool you use. Replace the bracketed part first.
My Stripe webhook runs in a Supabase Edge Function [function name] and fails with "SubtleCryptoProvider cannot be used in a synchronous context". Fix only this function:
1. Replace stripe.webhooks.constructEvent(...) with await stripe.webhooks.constructEventAsync(body, signature, webhookSecret, undefined, Stripe.createSubtleCryptoProvider()).
2. Read the body with await req.text() and verify it before any JSON parsing.
3. Read the signing secret from a server-side secret (Deno.env.get), never from code that runs in the browser.
4. Log err.message on failure (never the secret) and return 400. Return 200 only after the event is handled.
5. Make sure supabase/config.toml has verify_jwt = false for this function only. Leave every other function as it is.
Don't change any other file.How to confirm it's fixed
- The resent event in Stripe's Event deliveries tab shows 200.
- The function logs no longer show the SubtleCryptoProvider error.
- The customer's plan or order in your app has updated.
- A request without a valid signature still gets a 400. That means the check is working.
How to find out sooner next time
Webhook failures are invisible from the front of your site: checkout looks fine, and the customer just never gets access. Check Event deliveries after every change to payment code.
Heedza never pays, so no webhook is sent for its checks and it can't see this error. What its payment page check does is open your live checkout page on a schedule, check that the payment form or pay button is there, stop before paying, and send you a screenshot and a fix prompt if it breaks.
Let Heedza check this for you. Heedza’s payment page check opens your live payment page on a schedule and tells you if the Pay button disappears. It never pays. It never logs in or fills in forms, so it can’t see errors that only show after that.
Made with Lovable, Bolt, Replit, Cursor or v0? Free for 1 site: start free or see prices.