Skip to content
Webhooks SDK
Esc
↑↓navigate↵open⌘Jpreview
On this page

Clerk

Verify and handle Clerk webhooks — user, session, organization, and billing events.

Create the endpoint on the Webhooks page in the Clerk Dashboard, pass its signing secret, and handle events by name:

import { createWebhookHandler } from 'webhooks-sdk'
import { clerk } from 'webhooks-sdk/clerk'

const handler = createWebhookHandler({
  provider: clerk({ secret: process.env.CLERK_WEBHOOK_SIGNING_SECRET! }),
  on: {
    'user.created': async (event) => {
      await syncUser(event.payload.data.id)
    },
    'session.revoked': async (event) => {
      await dropSession(event.payload.data.id)
    },
  },
})

export const POST = handler.fetch

The secret is the whsec_… value shown when you select the endpoint on the Webhooks page. Clerk delivers through Svix and signs with Standard Webhooks over the legacy svix-* headers — the id and timestamp are inside the signed material, so a replayed request is rejected on its own.

Options

Option Type Default
secret string | string[] — The whsec_… signing secret(s). Pass an array during rotation.
tolerance number 300 Replay window in seconds.

Events

user.created · user.updated · user.deleted · session.created · session.ended · session.removed · session.revoked · email.created · sms.created · organization.created · organization.updated · organization.deleted · organizationDomain.created · organizationDomain.updated · organizationDomain.deleted · organizationInvitation.created · organizationInvitation.accepted · organizationInvitation.revoked · organizationMembership.created · organizationMembership.updated · organizationMembership.deleted · role.created · role.updated · role.deleted · permission.created · permission.updated · permission.deleted · waitlistEntry.created · waitlistEntry.updated · paymentAttempt.created · paymentAttempt.updated · subscription.created · subscription.updated · subscription.active · subscription.pastDue · subscriptionItem.created · subscriptionItem.updated · subscriptionItem.active · subscriptionItem.canceled · subscriptionItem.upcoming · subscriptionItem.ended · subscriptionItem.abandoned · subscriptionItem.incomplete · subscriptionItem.pastDue · subscriptionItem.freeTrialEnding

These autocomplete in the on map; any other string still routes, so an event type Clerk adds later is handled without an SDK update. The authoritative list for your instance is the Event Catalog tab on the endpoint in the Clerk Dashboard. The subscription.*, subscriptionItem.*, and paymentAttempt.* families are Clerk Billing events.

Mind the casing: multi-word resources are camelCase on the wire — organizationMembership.created, waitlistEntry.updated, subscription.pastDue, subscriptionItem.freeTrialEnding. Those are Clerk’s own strings, kept verbatim because they must match the wire.

Two timestamps, two units

The body’s timestamp is milliseconds since the epoch, while the signed svix-timestamp header is seconds. event.timestamp is built from the header, so use it — or divide before comparing the two.

The envelope

  • event.id — the svix-id header, the canonical idempotency key.
  • event.type — the body’s type.
  • event.timestamp — the signed svix-timestamp header.
  • event.payload — { data, object: 'event', type, timestamp, instance_id }; the affected resource is payload.data, identified by payload.data.id.

Standalone & testing

The wrapper is the Standard Webhooks provider with id: 'clerk', so the standalone triple lives there:

import {
  verifyStandardWebhook,  // (raw, { secret }) — throws on failure
  parseStandardWebhook,   // (raw) — the envelope
  signStandardWebhook,    // (body, secret, { headerPrefix: 'svix' }) — for tests
} from 'webhooks-sdk/standard-webhooks'

Pass headerPrefix: 'svix' to signStandardWebhook to produce the header names Clerk actually sends. See Testing.

Last updated on September 1, 2026

Was this page helpful?