Webhook 404
What this symptom means
404 Not Found means an HTTP server accepted the TCP/TLS connection, then had no matching route for that method and path. The Host header was good enough to reach some app.
Mercur does not use 404 for “this tunnel does not exist.” Unknown hostname, wrong protocol, or no connected agent is 503 with a body such as No agent for client. If the provider shows 404, something in the path — your framework, a reverse proxy, or a leftover trailingSlash rule — answered, not the unknown-endpoint case.
Guest inspector accepts any method and path and still returns 204. A 404 while using the inspector URL almost always means the provider is posting somewhere else.
Common causes
- Provider URL is
https://….mercur.sh/webhooks/stripebut the app only mountsPOST /api/webhooks/stripe - Next.js App Router file is
app/webhooks/stripe/page.tsx(UI) instead ofapp/api/webhooks/stripe/route.ts - Trailing-slash mismatch (
/webhookvs/webhook/) - GitHub payload URL includes a leftover
/payloadfrom another host - Telegram
setWebhookURL path is not the Express route (/telegram/webhookvs/) - Local static server (Vite preview,
python -m http.server) answering POST with 404 - You are reading a 404 from your production app while watching a Mercur log that never received the POST
Verify the incoming request
Compare three strings, character for character:
- The URL saved in the provider dashboard
- The public URL shown in Mercur
- The path your framework actually routes (including
/api)
Then curl -i -X POST "$URL/the/path" -H 'Content-Type: application/json' -d '{}'. If Mercur history shows path /the/path and your app log shows a different path, the tunnel is fine and the router is not.
On a connected Endpoint, the passed-through status is your local 404. On an unknown Mercur host, expect 503, documented in troubleshooting.
Provider and framework notes
GitHub: the payload URL is used as-is. There is no extra /github suffix unless you typed one.
Telegram: POSTs to the exact URL you passed to setWebhook, including path. getWebhookInfo shows that URL.
Stripe: the Dashboard endpoint URL is the full URL. If you append /stripe locally but paste a URL without it, Stripe’s POST 404s in your app while Mercur still logs the request (proxy) or 204s (guest, any path).
Next.js: trailingSlash: true in next.config redirects POST in ways that drop bodies. Prefer an explicit route.ts path that matches the provider URL with no redirect. See Next.js webhook localhost. Django APPEND_SLASH is the same class of bug — Django webhook localhost.
Debugging checklist
- Rule out Mercur unknown-host 503 vs app 404.
- Dump the path from Mercur history.
- Hit that same path with curl against localhost once the agent is up.
- Fix the framework route or change the provider URL — pick one, not both at random.
- If history is empty, this is webhook not received, not 404.
Confirm with the webhook inspector
Paste the inspector URL (optionally with the path your app will use later). Guest mode records whatever path the provider called, returns 204, forwards nothing, idle TTL 1 day, capture cap 256 KB.
If the inspector row’s path is / but your production route is /api/webhooks, you now know what to put on the Endpoint URL or in setWebhook.
Inspect the request Connect localhost
Connect localhost
Forward to the local port that actually serves the webhook route. A Next.js app on 3000 with the route at /api/webhooks needs the provider URL to include /api/webhooks, not only the Mercur origin.
Forwarding needs a connected agent — the macOS app or the npm package @mercur_dev/cli.
If localhost itself 404s with curl, Mercur will pass that 404 through. Fix the local router before blaming the tunnel.
Replay the same event
Retry repeats the stored path. If the stored path was wrong, replay will 404 again until you change the provider URL (new deliveries) or add a route for the old path. Replay does not rewrite the path.
Needs proxy mode, agent connected, complete payload. Not available on the guest inspector.