A webhook 404 means the provider sent the delivery to a URL that returned "not found". The URL is reachable (the server responded), but no route matched the path. This is almost always a configuration issue — wrong URL registered in the provider dashboard, or a missing route in the server.
Root Causes
1. Wrong URL registered in the provider dashboard
The most common cause. Compare the exact URL in the provider's webhook settings character-by-character with your server route. Common mistakes: missing trailing slash, wrong API version (/v1 vs /v2), typo in the path, http vs https.
2. Route not registered in your app
The route exists in your local code but is not deployed, or the route file is not imported in the entry point.
3. Reverse proxy stripping or rewriting the path
nginx or Caddy may strip the path prefix before forwarding. If the provider sends to /api/webhooks/stripe but nginx strips /api, your app receives /webhooks/stripe and returns 404.
4. Case sensitivity
Linux paths are case-sensitive. /webhook and /Webhook are different routes. macOS (case-insensitive) hides this bug in local development.
Fix: Express
const express = require('express')
const app = express()
// Register route BEFORE app.listen()
app.post('/webhooks/stripe', express.raw({ type: 'application/json' }), (req, res) => {
res.json({ received: true })
})
// Debug: list all registered routes
app._router.stack
.filter(r => r.route)
.forEach(r => console.log(Object.keys(r.route.methods)[0].toUpperCase(), r.route.path))
Fix: Next.js App Router
// app/api/webhooks/stripe/route.ts
import { NextRequest } from 'next/server'
export async function POST(request: NextRequest) {
const rawBody = await request.text()
// verify signature, process event...
return Response.json({ received: true })
}
// URL: https://yourdomain.com/api/webhooks/stripe
Fix: nginx Reverse Proxy
# Preserve the full path — trailing slash on BOTH sides is required
location /webhooks/ {
proxy_pass http://localhost:3001/webhooks/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
Fix: Django
# urls.py
from django.urls import path
from . import views
urlpatterns = [
path('webhooks/stripe/', views.stripe_webhook),
]
# views.py
from django.views.decorators.csrf import csrf_exempt
from django.http import JsonResponse
@csrf_exempt
def stripe_webhook(request):
if request.method != 'POST':
return JsonResponse({'error': 'Method not allowed'}, status=405)
return JsonResponse({'received': True})
Debug Checklist
- Curl the exact URL:
curl -X POST https://yourdomain.com/webhooks/stripe - Check server access logs — confirm the path actually received by the server
- List all registered routes and confirm the webhook path is present
- Check nginx/Caddy config for path rewriting
- Confirm the latest deployment is live (not a cached older build)
FAQ
The route works with curl but the provider gets 404 — why?
The provider may be hitting a different URL than you expect. Check the delivery log in the provider dashboard to see the exact URL used. Some providers also use GET for verification handshakes while POST is used for event deliveries — confirm the method matches your route definition.
How do I confirm the exact URL a provider will use?
Use WebhookWhisper to capture a real delivery and inspect the URL path in the request log. This shows the exact path the provider sends before you register the route in your app.
My route is correct but I still get 404 in staging — why?
Staging may be running an older build that does not include the route file. Confirm the deployment is current, and check whether the staging environment has the same route configuration and environment variables as production.