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