# Express webhook handler
## Route-scoped raw parser
```ts
import express from 'express'
import { verifyWebhook } from '@clerk/backend/webhooks'
const app = express()
app.use(express.json()) // fine for the rest of the API...
// ...but the webhook route gets the raw body
app.post(
'/api/webhooks',
express.raw({ type: 'application/json' }),
async (req, res) => {
try {
const evt = await verifyWebhook(req)
// handle evt by evt.type, write to DB
res.status(200).send('Webhook received')
} catch (err) {
console.error('webhook verification failed', err)
res.status(400).send('Error verifying webhook')
}
},
)
```
`express.raw({ type: 'application/json' })` on this route overrides the global JSON parser for matching requests, so `req.body` is a Buffer with the exact bytes Svix signed.
## Failure checklist
- If verification fails on every event, check parser order: any `express.json()` or `body-parser` that runs before the raw parser on this route breaks it. Route-level middleware ordering is the whole game.
- `clerkMiddleware()` must not gate this route. Webhooks carry no session; put the webhook route before any auth-requiring middleware or exclude its path.
- `CLERK_WEBHOOK_SIGNING_SECRET` for this endpoint, not the dev one, in production.
- Return 200 after the DB write. On 4xx/5xx Clerk retries, so make the handler idempotent: dedupe on the event id before writing.
- Local testing needs a tunnel; also confirm the Dashboard endpoint URL is the exact public URL (protocol, host, path all exact, no www mismatch).