00 / guide
Stripe webhook 401 or 403 on Vercel: Deployment Protection
Event deliveries in Stripe shows 401 and your handler logs are empty. Your code never ran. On Vercel, a common reason is that Deployment Protection answered first and asked Stripe to log in. Stripe's docs list 401, 403 and 405 as errors that can mean “the destination URL has access restrictions”, and say to make sure the endpoint is publicly accessible and accepts POST. Stripe: fix HTTP status codes
Free tool: decode any Event deliveries status
01 / what CheckoutAlarm checks
a
What it would catch here
With a read-only Stripe key, CheckoutAlarm's Stripe check emails you when events older than 15 minutes still haven't been delivered to your endpoint. It runs at its next Stripe check: once a day on Free, hourly on Pro.
b
What it won't catch
It can't tell you the status code or that Vercel is the cause, it doesn't read your Vercel settings, and it doesn't alert when Stripe has no endpoint for an event. Its page check can pass while this is broken.
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 / confirm it is Deployment Protection
1
POST to the exact URL Stripe has
Send a JSON POST, because that is what Stripe sends, and look at the status:
curl -i -X POST \
-H "Content-Type: application/json" \
-d '{}' \
https://your-url.example/api/stripe/webhookYour handler should answer 400 here, because the request has no Stripe signature. We ran the same check on one of our own protected Preview URLs: the JSON POST got 401 with a body that said “Protected by Vercel Authentication”, and a POST without a JSON content type got a 302 redirect to vercel.com/sso-api. Stripe counts a redirect as a failure too. Stripe: fix HTTP status codes
2
Check which URL Stripe has
In Stripe, open the endpoint and read its URL. A *.vercel.app address, or a preview URL, is the first thing to suspect. Compare it with the one your production domain uses.
03 / why Vercel answers instead of your code
1
What each scope protects
Deployment Protection has two scopes. Standard Protection “protects all deployments except production domains”. All Deployments “protects all URLs, including production domains”. Vercel: Deployment Protection
2
The generated deployment URL counts as protected
Vercel's migration note says that once Standard Protection is on, the production generated deployment URL becomes restricted. A webhook pointed at that address, rather than your own domain, will be stopped. Vercel: Standard Protection
3
Vercel Authentication is a login wall
Vercel says people who reach a protected deployment “will encounter a Vercel login redirect”. Stripe can't sign in to Vercel. Vercel: Vercel Authentication Vercel also says Deployment Protection “requires authentication for all requests, including those to Routing Middleware”, so a middleware rule in your app can't let the request through. Vercel: Deployment Protection
04 / fix it, narrowest first
a
Register your production domain
Set the endpoint URL in Stripe to your own domain, such as https://yourdomain.com/api/stripe/webhook. Under Standard Protection that domain isn't protected. Repeat the curl above and look for your handler's 400. Stripe needs a publicly accessible HTTPS URL. Stripe: set up your endpoint
b
If your production domain is protected
If you chose All Deployments, your own domain is behind the login too. Move the project to Standard Protection if you don't need production locked. Vercel: choose which URLs to protect
c
Testing against a protected preview
For a test endpoint, Vercel's Protection Bypass for Automation lets a request through when it carries a secret. For tools that can't set headers, it documents a query parameter named x-vercel-protection-bypass on the URL, and it shows a Stripe webhook URL as an example. Vercel: Protection Bypass for Automation
Treat that URL like a password. Each secret works on all deployments in the project until it is revoked, and regenerating it invalidates previous deployments until you redeploy. Vercel: configuring the bypass
d
Make one preview domain public
Deployment Protection Exceptions let you turn protection off for a list of preview domains. Vercel says a domain you add “becomes publicly accessible”, so only add one you are fine exposing. Vercel: Deployment Protection Exceptions
05 / after the fix
1
Let Stripe retry, or resend
In live mode Stripe tries to deliver an event for up to three days with exponential backoff. A sandbox retries three times over a few hours. Stripe: automatic retries For events that already failed, click Resend on the event in the Dashboard (up to 15 days after creation), or run stripe events resend <event_id> --webhook-endpoint=<endpoint_id> (up to 30 days). Stripe: manual retries
2
Check the deliveries and your data
Open the endpoint's Event deliveries and confirm new events show 200. Then check the records those events should have written. See recover missed Stripe webhook events, and 307 or 308 on Vercel if the status is a redirect. If the 401 comes from a Supabase Edge Function or your own auth middleware, read the Edge Function 401 guide.
06 / questions
- Why does Stripe get 401 from my Vercel app?
- If Deployment Protection covers the URL you gave Stripe, Vercel asks for a login before your code runs. Stripe can't log in, so the delivery fails. Stripe lists 401 and 403 as access restrictions and says to make sure the endpoint is publicly accessible and accepts POST.
- Is my production domain protected?
- Not under Standard Protection, which protects all deployments except production domains. It is protected if you picked All Deployments. The generated *.vercel.app deployment URL is restricted under Standard Protection, so don't register that one in Stripe.
- Can I put the bypass secret in the Stripe webhook URL?
- Vercel's docs show exactly that for Stripe, as the x-vercel-protection-bypass query parameter. The secret then sits in the URL, so anyone who can see the endpoint can use it to reach every deployment in the project until you revoke it. For a live endpoint, pointing Stripe at an unprotected production domain is the safer fix.
- Will Stripe retry the failed events?
- Yes. In live mode Stripe retries for up to three days with exponential backoff. In a sandbox it retries three times over a few hours. You can also resend an event by hand from the Dashboard (up to 15 days after it was created) or with the Stripe CLI (up to 30 days).
- Does CheckoutAlarm catch this?
- It catches the effect, not the cause. With a read-only Stripe key, its Stripe check emails you when events older than 15 minutes still haven't been delivered, at its next check (once a day on Free, hourly on Pro). It doesn't see the 401 or your Vercel settings, and it doesn't alert when Stripe has no endpoint for the event.
07 / where CheckoutAlarm fits
CheckoutAlarm doesn't see the 401 or your Vercel settings. With a read-only Stripe key, its Stripe check emails you when events older than 15 minutes still haven't been delivered, at its next check (once a day on Free, hourly on Pro).
CheckoutAlarm is new. It only checks sites you prove you own. How to verify
Watch my app free08 / sources
- Stripe: fix HTTP status codes (401, 403, 405 = access restrictions; 3xx = failure)
- Stripe: webhooks (publicly accessible HTTPS URL, retries, resend)
- Vercel: Deployment Protection (Standard Protection, All Deployments)
- Vercel: Vercel Authentication
- Vercel: Protection Bypass for Automation (query parameter, Stripe example)
- Vercel: Deployment Protection Exceptions
Docs checked on Oct 22, 2026.