Configure Clerk webhooks and handle authentication events.
Use when setting up user sync, handling auth events,
or integrating Clerk with external systems.
Trigger with phrases like "clerk webhooks", "clerk events",
"clerk user sync", "clerk notifications", "clerk event handling".
Configure and handle Clerk webhooks for user lifecycle events and data synchronization. Clerk uses Svix for webhook delivery with HMAC-SHA256 signature verification. As of 2025, Clerk provides a built-in verifyWebhook() helper in @clerk/backend alongside the manual Svix approach.
Prerequisites
Clerk account with webhook endpoint configured in Dashboard
HTTPS endpoint (use ngrok for local dev)
CLERK_WEBHOOK_SECRET environment variable (starts with whsec_)
Instructions
Step 1: Install Dependencies
# Option A: Use @clerk/backend's built-in verifyWebhook() (recommended)
# Already included with @clerk/nextjs — no extra install needed
# Option B: Manual Svix verification
npm install svix
Session events:session.created, session.ended (optional, high volume)
Copy the Signing Secret (whsec_...) to your .env.local:
CLERK_WEBHOOK_SECRET=whsec_...
Step 6: Express.js Webhook Endpoint
import express from 'express'
import { Webhook } from 'svix'
const app = express()
// CRITICAL: Use express.raw(), NOT express.json() for webhook routes
app.post('/api/webhooks/clerk',
express.raw({ type: 'application/json' }),
(req, res) => {
const wh = new Webhook(process.env.CLERK_WEBHOOK_SECRET!)
try {
const evt = wh.verify(req.body, {
'svix-id': req.headers['svix-id'] as string,
'svix-timestamp': req.headers['svix-timestamp'] as string,
'svix-signature': req.headers['svix-signature'] as string,
})
// Handle event...
res.status(200).json({ received: true })
} catch (err) {
console.error('Webhook verification failed:', err)
res.status(400).json({ error: 'Invalid signature' })
}
}
)
Local Development with ngrok
# Start ngrok tunnel for local webhook testing
ngrok http 3000
# Copy the https://xxx.ngrok-free.app URL
# Add it as webhook endpoint in Clerk Dashboard > Webhooks
# URL: https://xxx.ngrok-free.app/api/webhooks/clerk
Error Handling
Error
Cause
Solution
Invalid signature
Wrong CLERK_WEBHOOK_SECRET
Re-copy signing secret from Dashboard > Webhooks
Invalid signature
Body parsed with json() before verify
Use req.text() (Next.js) or express.raw() (Express)
Missing svix headers
Request not from Clerk/Svix
Verify endpoint URL; check sender
Duplicate processing
Clerk retried delivery
Implement idempotency with svix-id as unique key
Handler timeout
Slow DB operations
Offload heavy work to a background job queue
404 on webhook URL
Route not matching
Ensure /api/webhooks is in middleware's isPublicRoute
Enterprise Considerations
Treat CLERK_WEBHOOK_SECRET like a password -- rotate it if compromised (Dashboard > Webhooks > Signing Secret > Rotate)
Svix headers include svix-timestamp for replay attack protection (rejects events older than 5 minutes by default)
For high-volume apps, offload webhook processing to a queue (BullMQ, Inngest, Trigger.dev) and return 200 immediately
Monitor webhook delivery in Dashboard > Webhooks > Message Logs -- failed messages auto-retry with exponential backoff
Use verifyWebhook() from @clerk/backend/webhooks when possible -- it handles header extraction and secret key resolution automatically