Stripe and Vercel
Stripe webhooks to a Vercel preview: Deployment Protection and x-vercel-protection-bypass
A Stripe webhook pointed at a Vercel preview URL fails before your code runs because Deployment Protection, which with Standard Protection covers every deployment except production domains, asks the caller to log in to Vercel, and Stripe cannot. Create a Protection Bypass for Automation secret in the project's Deployment Protection settings and add it to the endpoint URL in Stripe as ?x-vercel-protection-bypass=<secret>, the method Vercel's docs give for Stripe webhooks. Point the endpoint at the branch URL, which stays the same across commits, keep it in a sandbox, and give the Preview environment that endpoint's own whsec_ secret. Checked against Vercel's and Stripe's docs on 2026-10-26.
Webhook fails in production too? Start from the Stripe webhook questions, one page each.
Why the event never reaches your route
Vercel's Standard Protection protects all deployments except production domains, so every preview URL and generated deployment URL is behind it. With Vercel Authentication, a visitor without a Vercel session is sent to a Vercel login. Stripe's request stops there: the function logs show nothing, and the attempt shows up as failed under the endpoint's event deliveries in Stripe.
Put the bypass secret in the endpoint URL
Protection Bypass for Automation accepts the secret as an x-vercel-protection-bypass header or query parameter. Stripe cannot add custom headers, so use the query parameter; Vercel's docs show this exact Stripe URL. A project can have several bypass secrets, so create one only for Stripe that you can revoke alone. Regenerating or deleting a secret invalidates it on existing deployments.
https://my-app-git-feature-x-my-team.vercel.app/api/webhooks/stripe?x-vercel-protection-bypass=<secret>Does the query string break the signature?
No. Stripe signs the payload and the timestamp, and constructEvent checks the raw body, the stripe-signature header and the endpoint secret; the URL is not part of it. Read the body with await req.text() as usual.
Which URL and which secret
A commit URL changes with every push, so register the branch URL, <project-name>-git-<branch-name>-<scope-slug>.vercel.app, which points at the latest deployment of that branch. The endpoint gets its own whsec_ signing secret: set it as STRIPE_WEBHOOK_SECRET for the Preview environment only, and keep the live one for Production. For local work, the Stripe CLI's stripe listen --forward-to sends events to localhost instead; see which webhook secret goes with which mode.
Checklist
How to fix it, then check the fix
- 01In Vercel, open the project's Deployment Protection settings and create a Protection Bypass for Automation secret named for Stripe.
- 02In a Stripe sandbox, add a webhook endpoint on the branch URL with ?x-vercel-protection-bypass=<secret> after the route path.
- 03Copy that endpoint's whsec_ signing secret into STRIPE_WEBHOOK_SECRET for Preview, then redeploy the branch.
- 04Check the route answers outside the login page:
curl -i -X POST "https://my-app-git-feature-x-my-team.vercel.app/api/webhooks/stripe?x-vercel-protection-bypass=$SECRET" - 05Send a test event from Stripe and check that the delivery succeeds and the function logs show it.
Related
Related guides
Sources
Official docs behind this checklist
Browse all launch guides · Read resources · How to make and check your next change
FAQ
FAQ for builders before launch
Should I just turn off protection for previews?
That makes every preview public, with whatever data and keys it uses. A bypass secret for one webhook URL opens only what Stripe needs.
Who can see the secret?
It is part of the endpoint URL, so anyone who can view webhook endpoints in your Stripe account can read it. That is one more reason to give Stripe its own secret.