| name | clerk-webhooks-events |
| description | 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".
|
| allowed-tools | Read, Write, Edit, Bash(npm:*), Grep |
| version | 1.0.0 |
| license | MIT |
| author | Jeremy Longshore <jeremy@intentsolutions.io> |
Clerk Webhooks & Events
Overview
Configure and handle Clerk webhooks for user lifecycle events and data synchronization.
Prerequisites
- Clerk account with webhook access
- HTTPS endpoint for webhooks
- svix package for verification
Instructions
Step 1: Install Dependencies
npm install svix
Step 2: Create Webhook Endpoint
import { Webhook } from 'svix'
import { headers } from 'next/headers'
import { WebhookEvent } from '@clerk/nextjs/server'
export async function POST(req: Request) {
const WEBHOOK_SECRET = process.env.CLERK_WEBHOOK_SECRET
if (!WEBHOOK_SECRET) {
throw new Error('CLERK_WEBHOOK_SECRET not set')
}
const headerPayload = await headers()
const svix_id = headerPayload.get('svix-id')
const svix_timestamp = headerPayload.get('svix-timestamp')
const svix_signature = headerPayload.get('svix-signature')
if (!svix_id || !svix_timestamp || !svix_signature) {
return Response.json({ error: 'Missing headers' }, { status: 400 })
}
const payload = await req.json()
const body = JSON.stringify(payload)
const wh = new Webhook(WEBHOOK_SECRET)
let evt: WebhookEvent
try {
evt = wh.verify(body, {
'svix-id': svix_id,
'svix-timestamp': svix_timestamp,
'svix-signature': svix_signature,
}) as WebhookEvent
} catch (err) {
console.error('Webhook verification failed:', err)
return Response.json({ error: 'Invalid signature' }, { status: 400 })
}
const eventType = evt.type
console.log(`Received webhook: ${eventType}`)
switch (eventType) {
case 'user.created':
await handleUserCreated(evt.data)
break
case 'user.updated':
await handleUserUpdated(evt.data)
break
case 'user.deleted':
await handleUserDeleted(evt.data)
break
case 'session.created':
await handleSessionCreated(evt.data)
break
case 'organization.created':
await handleOrgCreated(evt.data)
break
default:
console.log(`Unhandled event type: ${eventType}`)
}
return Response.json({ success: true })
}
Step 3: Implement Event Handlers
import { db } from './db'
interface ClerkUserData {
id: string
email_addresses: Array<{ email_address: string; id: string }>
first_name: string | null
last_name: string | null
image_url: string
created_at: number
updated_at: number
}
export async function handleUserCreated(data: ClerkUserData) {
const primaryEmail = data.email_addresses.find(
e => e.id === data.primary_email_address_id
)?.email_address
await db.user.create({
data: {
clerkId: data.id,
email: primaryEmail,
firstName: data.first_name,
lastName: data.last_name,
imageUrl: data.,
: (data.)
}
})
(primaryEmail)
.()
}
() {
primaryEmail = data..(
e. === data.
)?.
db..({
: { : data. },
: {
: primaryEmail,
: data.,
: data.,
: data.,
: (data.)
}
})
.()
}
() {
db..({
: { : data. }
})
(data.)
.()
}
() {
db..({
: {
: data.,
: data.,
: (data.),
: data.
}
})
.()
}
() {
db..({
: {
: data.,
: data.,
: data.,
: (data.)
}
})
.()
}
Step 4: Idempotency and Error Handling
import { Redis } from '@upstash/redis'
const redis = Redis.fromEnv()
export async function processWithIdempotency(
eventId: string,
handler: () => Promise<void>
) {
const key = `webhook:${eventId}`
const processed = await redis.get(key)
if (processed) {
console.log(`Event ${eventId} already processed`)
return { skipped: true }
}
try {
await handler()
await redis.set(key, 'processed', { ex: 86400 })
return { success: true }
} catch (error) {
console.error(`Failed to process ${eventId}:`, error)
error
}
}
() {
svix_id = headerPayload.()!
result = (svix_id, () => {
(evt.) {
:
(evt.)
}
})
.(result)
}
Step 5: Configure Webhook in Clerk Dashboard
- Go to Clerk Dashboard > Webhooks
- Add endpoint URL:
https://yourdomain.com/api/webhooks/clerk
- Select events:
user.created
user.updated
user.deleted
session.created
session.ended
organization.* (if using organizations)
- Copy webhook secret to environment
Available Events
| Event | Description |
|---|
user.created | New user signed up |
user.updated | User profile changed |
user.deleted | User account deleted |
session.created | New session started |
session.ended | Session terminated |
session.revoked | Session manually revoked |
organization.created | Org created |
organization.updated | Org settings changed |
organization.deleted | Org deleted |
organizationMembership.* | Member added/removed |
email.created | Email verification sent |
Output
- Webhook endpoint configured
- Event handlers implemented
- Idempotency protection
- User data sync working
Testing Webhooks Locally
npx ngrok http 3000
npx @clerk/cli dev
curl -X POST http://localhost:3000/api/webhooks/clerk \
-H "Content-Type: application/json" \
-H "svix-id: test" \
-H "svix-timestamp: $(date +%s)" \
-H "svix-signature: v1,..." \
-d '{"type":"user.created","data":{}}'
Error Handling
| Error | Cause | Solution |
|---|
| Invalid signature | Wrong secret | Verify CLERK_WEBHOOK_SECRET |
| Missing headers | Request not from Clerk | Check sender is Clerk |
| Duplicate processing | Event sent twice | Implement idempotency |
| Timeout | Handler too slow | Use background jobs |
Resources
Next Steps
Proceed to clerk-performance-tuning for optimization strategies.