# App Router webhook handler
## The handler
```ts
import { verifyWebhook } from '@clerk/nextjs/webhooks'
export async function POST(req: Request) {
try {
const evt = await verifyWebhook(req)
const { id } = evt.data
console.log('got webhook', id, evt.type)
// write to your DB here, then:
return new Response('Webhook received', { status: 200 })
} catch (err) {
console.error('webhook verification failed', err)
return new Response('Error verifying webhook', { status: 400 })
}
}
```
## The three rules
1. **Keep the route public.** Incoming webhooks are never signed in; they come from Clerk's servers. If your middleware or any auth check gates `/api/webhooks`, delivery fails. `clerkMiddleware()` protects nothing by default, so the failure mode is always an auth check you added yourself. Exclude the route explicitly.
2. **Do not parse the body first.** `verifyWebhook(req)` needs the raw request. Calling `await req.json()` before it breaks the Svix signature.
3. **Return 200 after handling.** Clerk retries on 4xx/5xx or no response. Return 200 only after your DB write succeeds; a 200 before the write means a crashed write loses the event silently.
## Checklist
- Signing secret value `CLERK_WEBHOOK_SIGNING_SECRET` in env, copied from the endpoint's settings page in the Dashboard. Dev and prod endpoints have different secrets.
- Test from the Dashboard: Webhooks page, endpoint settings, Testing tab, Send Example for `user.created`, then check Message Attempts for Succeeded.
- Local dev needs a tunnel (ngrok or similar); Clerk cannot reach YOUR_HOST directly.
- Narrow `evt.type` (e.g. `user.created`) for typed `evt.data`. Handle `user.updated` and `user.deleted` too if you mirror users; syncing is eventually consistent, so design for out-of-order delivery.