A 401 on your Stripe webhook endpoint is almost never a Stripe problem. It means something on your server is rejecting the request before your handler runs.
Clerk, NextAuth, Better Auth, and JWT middleware run on all routes by default. Stripe is not an authenticated user — it sends no session cookie, no Authorization header. The middleware rejects it with 401.
// middleware.ts — add webhook path to exclusion list
// Clerk
import { clerkMiddleware } from '@clerk/nextjs/server';
export default clerkMiddleware();
export const config = {
matcher: ['/((?!api/webhooks|_next/static|_next/image|favicon.ico).*)'],
};
// NextAuth / Better Auth — same pattern, different import
export { default } from 'next-auth/middleware';
export const config = {
matcher: ['/((?!api/webhooks|_next|[^?]*\.(?:html?|css|js)).*)', '/(api|trpc)(.*)'],
};
// app.js — webhook routes must be registered BEFORE auth middleware
app.use('/api/webhooks', webhookRouter); // ✓ no auth check
app.use('/api', jwtMiddleware, protectedRouter); // auth comes after
If you have Vercel password protection enabled on a preview or staging deployment, all requests including webhooks require authentication. Disable it or add the webhook URL to the bypass list in Vercel's project settings.
If your webhook URL points to a route that doesn't exist, some frameworks return 401 or 403 instead of 404. Check the Stripe Dashboard → Developers → Webhooks and confirm the endpoint URL matches your deployed route exactly.
# Send a test request without a Stripe signature
curl -X POST https://yoursite.com/api/webhooks/stripe -H "Content-Type: application/json" -d '{"test": true}'
# If you get 401 → middleware is blocking
# If you get 400 (missing signature) → handler is reached, middleware is fine
# If you get 404 → wrong URL
Generate the complete handler with auth middleware exclusion included.
Open WebhookFix →