Clerk middleware protects all routes by default. Stripe is not an authenticated user — it has no Clerk session. The middleware returns 401 before your webhook handler ever runs.
// middleware.ts
import { clerkMiddleware } from '@clerk/nextjs/server';
export default clerkMiddleware();
export const config = {
matcher: [
// Exclude webhook routes from Clerk auth
'/((?!api/webhooks|_next/static|_next/image|favicon.ico).*)',
'/(api|trpc)(.*)',
],
};
# After deploying, send a test request without a Stripe signature
# If Clerk is no longer blocking, you'll get 400 (missing signature) not 401
curl -X POST https://yoursite.com/api/webhooks/stripe -H "Content-Type: application/json" -d '{}'
# Expected: 400 Bad Request (missing stripe-signature)
# Was getting: 401 Unauthorized
If you need to protect some API routes while keeping webhooks open, use the callback form:
// middleware.ts
import { clerkMiddleware, createRouteMatcher } from '@clerk/nextjs/server';
const isProtectedRoute = createRouteMatcher(['/api/protected(.*)']);
export default clerkMiddleware((auth, req) => {
if (isProtectedRoute(req)) auth().protect();
// Webhook routes are not matched — they pass through
});
export const config = {
matcher: ['/((?!_next/static|_next/image|favicon.ico).*)'],
};
// middleware.ts — NextAuth
export { default } from 'next-auth/middleware';
export const config = {
matcher: ['/((?!api/webhooks|_next|[^?]*\.(?:html?|css|js)).*)', '/(api|trpc)(.*)'],
};
Generate the complete Stripe + Clerk handler with middleware exclusion.
Open WebhookFix →