00 / guide
Supabase Edge Function returns 401 to Stripe webhooks (and your logs are empty)
Stripe shows 401 for every delivery to your Edge Function, and your function logs show nothing at all. That empty log is the clue: your code never ran. Here is why, how to confirm it, and the fix.
Free tool: decode a 401 or any other webhook error
01 / what CheckoutAlarm checks
a
What it would catch here
A 401 is a failed delivery, so Stripe keeps the event as not delivered. With a read-only key, CheckoutAlarm emails you when events are still undelivered about 15 minutes after they were created.
b
What it won't catch
It doesn't see the status code, your function logs or which layer sent the 401. It doesn't send test requests to your function. If your function answers 200 but then does nothing, Stripe sees a success and so do we.
c
Plans
Free: 1 site, checked once a day. Pro: $19 a month, up to 3 sites checked hourly. You prove you own the site first. For Stripe we only take a restricted key with read access to Events, Webhook Endpoints and Charges. Full secret keys are refused. Compare plans
02 / why it happens
a
A platform check runs before your code
Every request to an Edge Function passes a platform check, verify_jwt, before your code runs. It is on by default and expects a valid user JWT in the Authorization header. If the header is missing or wrong, the platform answers 401 and your code never executes. Supabase: authorization headers
Stripe doesn't have your users' tokens, so every delivery is turned away. Supabase: function configuration
b
Why your logs are empty
The Logs view shows platform events, uncaught exceptions and your own console.log lines. Supabase: logging If your code never started, there is nothing of yours to log. The rejected request still lands in the function's edge logs, which Supabase's 401 guide queries in part 03. Supabase: Edge Function 401 error response
c
How it looks in Stripe
In Workbench, open Webhooks, pick your endpoint and open Event deliveries. Stripe lists 401 as a sign that the address has access limits. Stripe: fix HTTP status codes
03 / confirm it's the platform
1
Check Invocations, not just Logs
In the Dashboard, open Functions, pick the function, and choose Invocations. It shows each request and response, with headers, status code and duration, and you can filter by status code. Supabase: logging
2
Read the response body
Supabase's 401 guide says Missing authorization header or Invalid Token or Protected Header formatting come from the built-in check. A message you wrote, or an empty body, means your own code returned the 401. Supabase: Edge Function 401 error response To see the body yourself, send a plain POST to the function URL with no Authorization header, the same way Stripe does.
3
Or ask the logs table
The same guide has a SQL query for the SQL Editor that sorts recent 401s by cause. It reads function_edge_logs: a row with an empty execution_id means your code never ran. Supabase: Edge Function 401 error response
04 / the fix
1
Turn off the JWT check for this one function
In supabase/config.toml, add a block with your function's name:
[functions.stripe-webhook] verify_jwt = false
Or pass the flag when you deploy: supabase functions deploy stripe-webhook --no-verify-jwt Supabase CLI: functions deploy Supabase's docs present config.toml as the place to set this once per function. Supabase's 401 guide now recommends turning the built-in check off and handling auth in your function code. Supabase: Edge Function 401 error response
Only do this for the webhook function. Functions called by signed-in users should keep the check.
2
Keep Stripe's signature check
With the check off, anyone can call the function. Supabase warns about this. Supabase: skipping authorization checks For external webhooks, Supabase says to skip its credential check (auth: 'none' with its withSupabase wrapper) and verify the provider's signature in the handler. Supabase: securing Edge Functions Read the raw body as text and verify it with the async method, as in Supabase's Stripe example: Supabase: handling Stripe webhooks
const body = await req.text() // raw body, not req.json()
const event = await stripe.webhooks.constructEventAsync(
body, req.headers.get("Stripe-Signature")!, signingSecret,
undefined, Stripe.createSubtleCryptoProvider())If the check fails, log why and answer 400, so a rejected request leaves a trace.
3
Deploy and test again
Deploy the function again. Then resend a failed event from Event deliveries, or wait for the next one. A good delivery shows 200.
If you now see a signature error, see signature verification failed. The raw body rule is the same.
05 / the new API keys
a
Publishable and secret keys are not JWTs
Supabase is deprecating the anon and service_role keys by the end of 2026, in favor of publishable (sb_publishable_...) and secret (sb_secret_...) keys. Supabase: API keys The new keys are not JWTs. Send them in the apikey header, not as Authorization: Bearer. Supabase: authorization headers Stripe sends neither, so a Stripe webhook still needs verify_jwt = false.
b
Writing to your database from the webhook
Edge Functions get SUPABASE_SECRET_KEYS, a JSON dictionary of your secret keys, alongside the legacy SUPABASE_SERVICE_ROLE_KEY. Secret keys bypass Row Level Security, so use them only on the server. Supabase: environment variables
If the database write fails, answer non-2xx so Stripe keeps retrying, and log the error. A 200 tells Stripe the event was delivered.
06 / not on Supabase? other 401 causes
1
Vercel Deployment Protection
Vercel's Standard Protection protects every deployment except your production domains. A webhook pointed at a preview or a generated *.vercel.app deployment URL hits a login wall and gets a 401. Point the live endpoint at your real domain. If you picked "All Deployments", your production domain is protected too. Vercel: Deployment Protection
Testing a preview? Vercel's Protection Bypass for Automation can go in the webhook URL as ?x-vercel-protection-bypass=… for services that can't set headers. Treat that whole URL as a password. Vercel: Protection Bypass for Automation
2
A login check in proxy or middleware
In Next.js 16, middleware is renamed proxy. Without a matcher, it runs on every request. If it answers 401 to anyone without a session, Stripe gets a 401 too. Leave the webhook path out with a negative matcher, as in the Next.js docs. Ask your AI builder: "Exclude /api/webhooks/stripe from the auth proxy matcher." Next.js: proxy matcher
3
Keep Stripe's own check
Skipping a login check is safe only if the route checks the Stripe-Signature header with your whsec_ secret. Stripe: verify events
Questions
- Why are my Edge Function logs empty when Stripe gets a 401?
- Because your code never ran. With verify_jwt on (the default), the platform checks the Authorization header before your function starts. Stripe sends no Supabase JWT, so the platform answers 401 and your console.log lines never execute.
- How do I prove the 401 came from the platform and not my code?
- Read the response body. Supabase says "Missing authorization header" or "Invalid Token or Protected Header formatting" come from the built-in check. A message you wrote, or an empty body, means your code ran and returned 401 itself. The Invocations view shows each request's status code.
- Is verify_jwt = false safe for a Stripe webhook?
- Only with Stripe's signature check in your code. Turning it off lets anyone call the function, so verify the Stripe-Signature header against the raw body and answer 400 if it fails. Supabase's own Stripe example does this.
- Do the new sb_publishable_ and sb_secret_ keys change this?
- Stripe still sends no Supabase credential, so the fix is the same. The new keys are not JWTs. Supabase says to send them in the apikey header, not as a Bearer token, and to authenticate webhooks in your own code.
- My webhook runs now, but the database write fails. What then?
- Return a non-2xx when the write fails, so Stripe keeps retrying, and log the error. A 200 tells Stripe the event was delivered, and it stops retrying.
Missed events while it was failing? See how to find and replay undelivered events.
07 / where CheckoutAlarm fits
With a read-only Stripe key, CheckoutAlarm emails you when Stripe events are still not delivered after about 15 minutes, which is what a platform 401 causes. It doesn't see the status code or your function logs, so Event deliveries and the Invocations view are still where you find the 401.
CheckoutAlarm is new. It only checks sites you prove you own. How to verify
Watch my app free08 / sources
- Supabase: authorization headers (verify_jwt runs before your code; new keys are not JWTs)
- Supabase: Edge Function 401 error response (platform messages, function_edge_logs query)
- Supabase: Edge Function logging (Invocations vs Logs)
- Supabase: function configuration (verify_jwt in config.toml)
- Supabase CLI: supabase functions deploy (--no-verify-jwt)
- Supabase: securing Edge Functions (external webhooks)
- Supabase: handling Stripe webhooks with Edge Functions
- Supabase: API keys (sb_publishable_, sb_secret_, legacy deprecation)
- Supabase: Edge Function environment variables (SUPABASE_SECRET_KEYS)
- Stripe: fix HTTP status codes
- Vercel: Deployment Protection (Standard Protection, All Deployments)
- Vercel: Protection Bypass for Automation
- Next.js: proxy (formerly middleware) and matcher
- Stripe: verify events are sent from Stripe
Docs checked on Oct 20, 2026.