Claude-skill-registry clerk-webhooks-events
install
source · Clone the upstream repo
git clone https://github.com/majiayu000/claude-skill-registry
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/clerk-webhooks-events-byteworthy-upstream" ~/.claude/skills/majiayu000-claude-skill-registry-clerk-webhooks-events && rm -rf "$T"
manifest:
skills/data/clerk-webhooks-events-byteworthy-upstream/SKILL.mdsource content
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
// app/api/webhooks/clerk/route.ts 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') } // Get Svix headers 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 }) } // Get body const payload = await req.json() const body = JSON.stringify(payload) // Verify webhook 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 }) } // Handle event 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
// lib/webhook-handlers.ts 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.image_url, createdAt: new Date(data.created_at) } }) // Send welcome email await sendWelcomeEmail(primaryEmail) console.log(`User created: ${data.id}`) } export async function handleUserUpdated(data: ClerkUserData) { const primaryEmail = data.email_addresses.find( e => e.id === data.primary_email_address_id )?.email_address await db.user.update({ where: { clerkId: data.id }, data: { email: primaryEmail, firstName: data.first_name, lastName: data.last_name, imageUrl: data.image_url, updatedAt: new Date(data.updated_at) } }) console.log(`User updated: ${data.id}`) } export async function handleUserDeleted(data: { id: string }) { await db.user.delete({ where: { clerkId: data.id } }) // Clean up user data await cleanupUserData(data.id) console.log(`User deleted: ${data.id}`) } export async function handleSessionCreated(data: any) { // Log session for analytics await db.sessionLog.create({ data: { userId: data.user_id, sessionId: data.id, createdAt: new Date(data.created_at), userAgent: data.user_agent } }) console.log(`Session created: ${data.id}`) } export async function handleOrgCreated(data: any) { await db.organization.create({ data: { clerkOrgId: data.id, name: data.name, slug: data.slug, createdAt: new Date(data.created_at) } }) console.log(`Organization created: ${data.id}`) }
Step 4: Idempotency and Error Handling
// lib/webhook-idempotency.ts import { Redis } from '@upstash/redis' const redis = Redis.fromEnv() export async function processWithIdempotency( eventId: string, handler: () => Promise<void> ) { const key = `webhook:${eventId}` // Check if already processed const processed = await redis.get(key) if (processed) { console.log(`Event ${eventId} already processed`) return { skipped: true } } try { await handler() // Mark as processed (expire after 24 hours) await redis.set(key, 'processed', { ex: 86400 }) return { success: true } } catch (error) { // Log error but don't mark as processed console.error(`Failed to process ${eventId}:`, error) throw error } } // Usage in webhook handler export async function POST(req: Request) { // ... verification code ... const svix_id = headerPayload.get('svix-id')! const result = await processWithIdempotency(svix_id, async () => { switch (evt.type) { case 'user.created': await handleUserCreated(evt.data) break // ... other handlers } }) return Response.json(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.createduser.updateduser.deletedsession.createdsession.ended
(if using organizations)organization.*
- Copy webhook secret to environment
Available Events
| Event | Description |
|---|---|
| New user signed up |
| User profile changed |
| User account deleted |
| New session started |
| Session terminated |
| Session manually revoked |
| Org created |
| Org settings changed |
| Org deleted |
| Member added/removed |
| Email verification sent |
Output
- Webhook endpoint configured
- Event handlers implemented
- Idempotency protection
- User data sync working
Testing Webhooks Locally
# Use ngrok for local testing npx ngrok http 3000 # Or use Clerk CLI npx @clerk/cli dev # Test with curl 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.