# 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.