DEV Community

Cover image for Stripe webhook shows 307 or 308 on Vercel / Next.js? It's a redirect, and Stripe won't follow it
WebhookProof
WebhookProof

Posted on

Stripe webhook shows 307 or 308 on Vercel / Next.js? It's a redirect, and Stripe won't follow it

The checkout works and the money arrives, but your app never finds out. Open the webhook endpoint in Stripe and every delivery attempt says 307 or 308. Your route handler logs are empty.

That's not a bug in your handler. Something in front of it answered with a redirect, and Stripe doesn't follow redirects. This post covers where those redirects come from on Next.js and Vercel, how to find the exact URL Stripe should call, and how to check the fix. Every claim links to the doc it comes from (see Sources at the end).

Why a redirect counts as a failure

Stripe's webhook docs list this under delivery errors: "302 ERR (or other 3xx status)... The destination server attempted to redirect the request to another location. We consider redirect responses to webhook requests as failures." Stripe's recommended fix is short: "Set the webhook endpoint destination to the URL resolved by the redirect."

In other words, don't fight the redirect. Find where it ends up, and register that URL.

The same docs say a registered endpoint must be a "publicly accessible HTTPS URL", and in live mode Stripe retries a failed event "for up to three days with an exponential back off". So a redirect doesn't lose the event right away, but every retry hits the same redirect until the URL is corrected.

Why 307 and 308 specifically

Next.js explains its choice in the redirects reference. Older 301/302 redirects let many browsers switch a POST to a GET, so "Next.js uses the 307 temporary redirect, and 308 permanent redirect status codes to explicitly preserve the request method used." Vercel's redirect docs recommend 307 or 308 for the same reason.

When you see a 307 or 308 in Stripe's delivery log, the response came from a Next.js or Vercel redirect rule, not from your code. There are four usual sources.

Cause 1: the trailing slash

From the Next.js trailingSlash docs: "By default Next.js will redirect URLs with trailing slashes to their counterpart without a trailing slash. For example /about/ will redirect to /about." If you set trailingSlash: true, it works the other way round.

The skipTrailingSlashRedirect reference spells out the status codes:

trailingSlash Request Response
false (default) /api/webhooks/stripe/ 308 to /api/webhooks/stripe
true /api/webhooks/stripe 308 to /api/webhooks/stripe/

Fix: copy the endpoint URL from Stripe, compare the end of it with your trailingSlash setting, and edit the endpoint URL to match. You don't need to change your Next.js config for this.

Cause 2: apex vs www (domain redirect)

Vercel's domain docs say that if a visitor uses your domain with or without www, Vercel "will attempt to redirect automatically", and you can also add the redirect yourself under Domains in Project Settings. Vercel recommends "using the www subdomain as your primary domain, with a redirect from the non-www domain to it."

So if your project's primary domain is www.example.com and the Stripe endpoint says https://example.com/api/webhooks/stripe, every delivery gets redirected before it reaches your function. The reverse happens if apex is primary and Stripe has the www URL.

Fix: in Vercel, open Project Settings → Domains and see which domain is primary and which one redirects. Put the primary host in the Stripe endpoint URL.

Cause 3: a redirects() rule that matches the webhook path

In the redirects reference, permanent: true "will use the 308 status code" and permanent: false "will use the 307 status code". Redirects are also "checked before the filesystem", so a wide rule beats your route handler. A catch-all like source: '/:path*' that moves traffic to a new domain or locale will catch /api/webhooks/stripe too.

The same goes for redirects in vercel.json, and for Vercel Firewall redirects. Vercel says a dashboard firewall redirect "creates a temporary 307 redirect", and that firewall redirects run "before CDN configuration redirects".

Fix: narrow the rule so it skips the webhook path, or point Stripe at the destination the rule sends traffic to.

Cause 4: Proxy (formerly Middleware) redirecting to login

Next.js renamed middleware to proxy in v16. Its docs say it's commonly used "for implementing custom server-side logic like authentication", and that "without a matcher, Proxy runs on every request". Auth middleware that sends logged-out visitors to /login with NextResponse.redirect(...) treats Stripe as a logged-out visitor. Stripe has no session cookie, so it gets redirected.

The docs' own negative-matcher example leaves out api:

// proxy.ts (or middleware.ts on Next.js 15 and earlier)
export const config = {
  matcher: ['/((?!api|_next/static|_next/image|favicon.ico|sitemap.xml|robots.txt).*)'],
}
Enter fullscreen mode Exit fullscreen mode

Fix: exclude the webhook route from the matcher, or return early for that path. In either case, the handler itself still has to verify the Stripe-Signature header, as Stripe's docs require.

One more: double slashes

Vercel's CDN "normalizes URLs containing consecutive slashes (e.g., //) to single slashes and redirects with a 308 status code." An endpoint pasted as https://www.example.com//api/webhooks/stripe fails for that reason alone.

How to find the real URL

curl shows the redirect and where it points. Send a POST, since that's what Stripe sends:

curl -i -X POST -H 'content-type: application/json' -d '{}' \
  https://example.com/api/webhooks/stripe/
Enter fullscreen mode Exit fullscreen mode

If the first line is HTTP/2 307 or HTTP/2 308, the location: header is where the request is meant to go. Run the command again against that URL, and repeat until the response is no longer a 3xx. Vercel's redirect docs use the same method (curl -I and look for the Location).

When you reach the final URL, an unsigned POST like the one above should get a 400, because your handler rejected a request with no valid signature. If it gets a 200, your endpoint accepts unsigned events, which is a separate and worse problem.

Then:

  1. Edit the endpoint in Stripe (Workbench → Webhooks) and paste the final URL.
  2. Open a failed event and click Resend. Stripe's docs say Dashboard resends work "for up to 15 days after the event creation" (or 30 days with stripe events resend in the CLI).
  3. Check that the event delivery now shows 200.

The same mistake can happen locally. If stripe listen --forward-to localhost:3000/api/webhooks/stripe/ uses a trailing slash your app redirects away from, the CLI shows the same 308.

Checklist

  • [ ] Host in the Stripe endpoint = the primary domain in Vercel (apex vs www)
  • [ ] Trailing slash in the Stripe endpoint matches trailingSlash in next.config.js
  • [ ] No redirects(), vercel.json or Firewall rule matches the webhook path
  • [ ] Proxy/Middleware matcher excludes the webhook route
  • [ ] curl -i -X POST to the registered URL returns a non-3xx (ideally 400 for an unsigned request)
  • [ ] A resent event shows 200 in Event deliveries

If you'd rather not follow redirects by hand, WebhookProof's free checker sends 3 harmless requests to your webhook URL and reports redirects, auth walls, and whether the endpoint accepts unsigned events. The guide for this case is here: Stripe webhook 307 / 308 redirect error (Vercel, Next.js).

WebhookProof is an independent tool, not affiliated with Stripe, Inc. or Vercel Inc. Quotes are from the linked documentation as read on 2026-10-18.

Sources

Top comments (0)