Skip to content

Clerk Integration

Receive and route Clerk webhooks for user lifecycle, authentication, organization, and session events.

Setup

1. Create a Source in Hookbase

Clerk uses Svix for webhook delivery, signing payloads with HMAC-SHA256.

bash
curl -X POST https://api.hookbase.app/api/sources \
  -H "Authorization: Bearer whr_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Clerk Production",
    "slug": "clerk",
    "verificationConfig": {
      "type": "svix",
      "secret": "whsec_..."
    }
  }'

Save your webhook URL:

https://api.hookbase.app/ingest/{orgSlug}/clerk

2. Configure Clerk Webhook

  1. Go to Clerk Dashboard → Webhooks
  2. Click Add Endpoint
  3. Enter your Hookbase ingest URL
  4. Select the events you want to subscribe to
  5. Click Create
  6. Copy the Signing Secret (starts with whsec_) and update your Hookbase source

3. Create Routes

bash
# Create destination for your user sync handler
curl -X POST https://api.hookbase.app/api/destinations \
  -H "Authorization: Bearer whr_your_api_key" \
  -d '{"name": "User Sync Service", "url": "https://api.myapp.com/webhooks/clerk"}'

# Create route
curl -X POST https://api.hookbase.app/api/routes \
  -H "Authorization: Bearer whr_your_api_key" \
  -d '{"name": "Clerk to User Sync", "sourceId": "src_...", "destinationIds": ["dst_..."]}'

Signature Verification

Clerk uses Svix for webhook delivery. The signature is sent across three headers:

HeaderDescription
svix-idUnique message ID
svix-timestampUnix timestamp (seconds)
svix-signaturev1,<base64-encoded-signature>

Hookbase verifies Svix signatures automatically:

json
{
  "verificationConfig": {
    "type": "svix",
    "secret": "whsec_..."
  }
}

Timestamp Tolerance

Svix signatures include a timestamp. Hookbase rejects webhooks older than 5 minutes by default to prevent replay attacks.

Common Events

user.created

Triggered when a new user signs up.

json
{
  "type": "user.created",
  "object": "event",
  "data": {
    "id": "user_2abc123def456",
    "object": "user",
    "first_name": "Jane",
    "last_name": "Smith",
    "email_addresses": [
      {
        "id": "idn_2abc123",
        "email_address": "[email protected]",
        "verification": {
          "status": "verified",
          "strategy": "email_code"
        }
      }
    ],
    "primary_email_address_id": "idn_2abc123",
    "username": "janesmith",
    "profile_image_url": "https://img.clerk.com/...",
    "created_at": 1709812800000,
    "updated_at": 1709812800000,
    "external_accounts": [],
    "public_metadata": {},
    "private_metadata": {},
    "unsafe_metadata": {}
  }
}

user.updated

Triggered when a user profile is modified.

json
{
  "type": "user.updated",
  "object": "event",
  "data": {
    "id": "user_2abc123def456",
    "object": "user",
    "first_name": "Jane",
    "last_name": "Doe",
    "email_addresses": [
      {
        "id": "idn_2abc123",
        "email_address": "[email protected]",
        "verification": { "status": "verified" }
      }
    ],
    "primary_email_address_id": "idn_2abc123",
    "public_metadata": {
      "plan": "pro"
    },
    "updated_at": 1709899200000
  }
}

user.deleted

Triggered when a user account is deleted.

json
{
  "type": "user.deleted",
  "object": "event",
  "data": {
    "id": "user_2abc123def456",
    "object": "user",
    "deleted": true
  }
}

organization.created

Triggered when a new organization is created.

json
{
  "type": "organization.created",
  "object": "event",
  "data": {
    "id": "org_2xyz789ghi012",
    "object": "organization",
    "name": "Acme Corp",
    "slug": "acme-corp",
    "members_count": 1,
    "max_allowed_memberships": 5,
    "created_by": "user_2abc123def456",
    "public_metadata": {},
    "private_metadata": {},
    "created_at": 1709812800000,
    "updated_at": 1709812800000
  }
}

organizationMembership.created

Triggered when a user is added to an organization.

json
{
  "type": "organizationMembership.created",
  "object": "event",
  "data": {
    "id": "orgmem_2abc123",
    "object": "organization_membership",
    "role": "org:member",
    "organization": {
      "id": "org_2xyz789ghi012",
      "name": "Acme Corp",
      "slug": "acme-corp"
    },
    "public_user_data": {
      "user_id": "user_2def456ghi789",
      "first_name": "Bob",
      "last_name": "Wilson",
      "identifier": "[email protected]"
    },
    "created_at": 1709899200000,
    "updated_at": 1709899200000
  }
}

session.created

Triggered when a user signs in and a new session is created.

json
{
  "type": "session.created",
  "object": "event",
  "data": {
    "id": "sess_2abc123",
    "object": "session",
    "user_id": "user_2abc123def456",
    "status": "active",
    "last_active_at": 1709812800000,
    "expire_at": 1710417600000,
    "created_at": 1709812800000,
    "updated_at": 1709812800000
  }
}

Transform Examples

Sync New Users to Your Database

javascript
function transform(payload) {
  const user = payload.data;
  const primaryEmail = user.email_addresses.find(
    e => e.id === user.primary_email_address_id
  );

  return {
    external_id: user.id,
    email: primaryEmail ? primaryEmail.email_address : null,
    first_name: user.first_name,
    last_name: user.last_name,
    username: user.username,
    avatar_url: user.profile_image_url,
    metadata: user.public_metadata,
    created_at: new Date(user.created_at).toISOString()
  };
}

Slack Alert for New Sign-Ups

javascript
function transform(payload) {
  const user = payload.data;
  const primaryEmail = user.email_addresses.find(
    e => e.id === user.primary_email_address_id
  );

  return {
    blocks: [
      {
        type: "header",
        text: {
          type: "plain_text",
          text: "New User Sign-Up"
        }
      },
      {
        type: "section",
        fields: [
          {
            type: "mrkdwn",
            text: `*Name:*\n${user.first_name} ${user.last_name}`
          },
          {
            type: "mrkdwn",
            text: `*Email:*\n${primaryEmail ? primaryEmail.email_address : 'N/A'}`
          },
          {
            type: "mrkdwn",
            text: `*Username:*\n${user.username || 'N/A'}`
          },
          {
            type: "mrkdwn",
            text: `*Auth Method:*\n${user.external_accounts.length > 0 ? 'OAuth' : 'Email'}`
          }
        ]
      }
    ]
  };
}

User Deletion Cleanup

javascript
function transform(payload) {
  return {
    action: "delete_user",
    clerk_user_id: payload.data.id,
    deleted_at: new Date().toISOString(),
    cleanup_tasks: [
      "remove_from_database",
      "cancel_subscriptions",
      "delete_uploaded_files",
      "send_confirmation_email"
    ]
  };
}

Filter Examples

User Events Only

json
{
  "name": "User Events",
  "logic": "and",
  "conditions": [
    {
      "field": "type",
      "operator": "starts_with",
      "value": "user."
    }
  ]
}

Organization Events

json
{
  "name": "Organization Events",
  "logic": "or",
  "conditions": [
    {
      "field": "type",
      "operator": "starts_with",
      "value": "organization."
    },
    {
      "field": "type",
      "operator": "starts_with",
      "value": "organizationMembership."
    }
  ]
}

New Sign-Ups Only

json
{
  "name": "New Sign-Ups",
  "logic": "and",
  "conditions": [
    {
      "field": "type",
      "operator": "equals",
      "value": "user.created"
    }
  ]
}

Specific Organization

json
{
  "name": "Acme Corp Events",
  "logic": "and",
  "conditions": [
    {
      "field": "type",
      "operator": "starts_with",
      "value": "organizationMembership."
    },
    {
      "field": "data.organization.slug",
      "operator": "equals",
      "value": "acme-corp"
    }
  ]
}

Headers

Clerk (Svix) webhooks include these headers:

HeaderDescription
svix-idUnique message identifier
svix-timestampUnix timestamp (seconds)
svix-signaturev1,<base64-signature>
Content-Typeapplication/json
User-AgentSvix-Webhooks

Troubleshooting

Signature Verification Failed

  1. Verify you're using the webhook signing secret (starts with whsec_)
  2. Ensure verification type is set to svix, not hmac
  3. Check that the secret matches what's shown in the Clerk dashboard
  4. Note: Clerk rotates signing secrets when you regenerate them — update your Hookbase source immediately

Missing Events

  1. Check which events are selected in Clerk's webhook endpoint settings
  2. Verify filters aren't blocking expected events
  3. Check Clerk's webhook logs (Dashboard → Webhooks → your endpoint → Message Attempts)

User Data Incomplete

  1. Some fields are only populated based on your Clerk instance configuration
  2. username is null unless you enable usernames in Clerk settings
  3. external_accounts only populated when users sign in via OAuth
  4. private_metadata and unsafe_metadata are not included in webhook payloads by default

Released under the MIT License.