Stripe webhook fails with 301, 307, or 308: the redirect is the bug
Stripe's delivery log shows 3xx errors and paid users never get access. Stripe does not follow redirects, so a www, https, or trailing-slash redirect silently drops every event. How to find the real URL and fix it.
What you are seeing
- In the Stripe Dashboard, the webhook endpoint's event deliveries show
301,302,307, or308ERR. - Checkout succeeds, but the user stays on the free plan and your webhook handler logs nothing at all.
- The same handler works with
stripe listenlocally, which forwards straight to localhost with no redirect. - Opening the webhook URL in a browser seems fine, because browsers follow redirects silently.
Why it happens
- Stripe treats any redirect response to a webhook as a failed delivery. Its docs say to set the endpoint to the URL the redirect resolves to.
- Apex versus www: your host redirects
example.comtowww.example.com(or the reverse), and Stripe was given the other one. - Trailing slash: Next.js redirects
/api/webhooks/stripe/to the version without the slash by default (and the reverse withtrailingSlash: true). - A broad redirect rule in
next.config.js,vercel.json,_redirects, or middleware catches the webhook path, for example a locale or login redirect. - The endpoint was registered with
http://and the host upgrades it tohttps://.
How to fix it
1. See exactly what Stripe gets
curl -i -X POST https://example.com/api/webhooks/stripe
# a 3xx with a location: header means a redirect;
# the location value is the URL Stripe should use.
# A 400 about a missing signature is fine: that is your handler answering.
2. Point Stripe at the final URL
In the Stripe Dashboard, open Developers, then Webhooks (Workbench), select the endpoint, and update the URL to the exact final value from step 1: https, the canonical host, and the same trailing slash. Updating the URL keeps the endpoint's signing secret, but if you create a new endpoint instead, copy its new whsec_ secret into your host's env vars and redeploy.
3. Keep middleware and redirects off the webhook path
// Next.js middleware: skip auth, locale, and www redirects for webhooks
export const config = {
matcher: ['/((?!api/webhooks|_next/static|_next/image|favicon.ico).*)'],
};
4. Resend the events that failed
In the endpoint's Event deliveries tab, open each failed checkout.session.completed or customer.subscription event and resend it, or replay from the CLI:
stripe events resend evt_123 --webhook-endpoint we_123
5. Check other callback URLs for the same bug
The same apex versus www mismatch breaks OAuth callbacks, Supabase auth redirect URLs, and other providers' webhooks. Run the curl check from step 1 against each one.
How to know it is fixed
New deliveries in Stripe show 200, the resent events unlock the users who already paid, and a fresh test-mode checkout updates the plan within seconds. Source: Stripe docs: Receive Stripe events in your webhook endpoint. If deliveries now show 400 with a signature error, see Stripe checkout works but the user is still on the free plan.