mcpField
C
clerk-webhooks

clerk-webhooks

github.com/clerk开发8953.3k 次安装今天更新
代码仓库

关于 clerk-webhooks

clerk-webhooks 是面向 Clerk 应用开发者的 Webhook 处理技能,提供验证签名、处理用户与组织事件的指导。内容涵盖多种框架,并说明重试、幂等和异步处理注意事项。

主要功能

  1. 01使用 verifyWebhook(req) 验证签名
  2. 02处理用户、组织及订阅事件
  3. 03按事件类型安全读取数据
  4. 04提供多框架处理器示例
  5. 05说明重试与重复事件处理
  6. 06支持本地转发测试

适用场景

  1. 01将 Clerk 用户事件同步到数据库
  2. 02在用户注册后发送通知
  3. 03响应组织成员变更
  4. 04在本地测试 Webhook 交付

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/clerk/skills --skill clerk-webhooks

安装后在 Claude Code 中输入/clerk-webhooks确认已加载。

附带文件evals/evals.jsonreferences/frameworks.md

详细介绍

Webhooks

输出完整且可运行的 webhook 处理程序,并在每个处理程序中使用 verifyWebhook(req) 验证。

何时使用 Webhooks

Webhooks 是异步且最终一致的。事件会快速送达,但不保证立即送达,并且偶尔可能会失败(Svix 会按固定计划重试)。可用于:

  • 数据库同步(单独的 users / orgs 表,随 Clerk 保持同步)
  • 通知(欢迎邮件、Slack 提醒、内部警报)
  • 由生命周期事件触发的集成

不要在同步流程中依赖 webhook 送达,例如入门流程(“用户注册后,我们再从数据库读取 X”)。对于用户刚刚创建的数据,请从 Clerk 会话令牌中读取,或直接调用 Backend API。当你需要会话令牌中未包含的其他用户或事件的数据时,webhook 可以填补这一空缺。

验证每个 Webhook

使用框架专用包中的 verifyWebhook(req)(例如 @clerk/nextjs/webhooks、@clerk/express/webhooks 等)。它会自动读取 CLERK_WEBHOOK_SIGNING_SECRET,并在签名无效时抛出错误。即使处理程序仅用于发送通知,跳过验证也会使端点暴露于伪造事件。

保持 Webhook 路由不受保护

Webhook 投递不携带用户会话。verifyWebhook() 是路由唯一需要的检查。单独使用 clerkMiddleware() 不会保护任何内容,因此该路由本来就可以访问。不要在中间件或处理程序中对 /api/webhooks(.*) 调用 auth.protect()。如果项目中这样做,在移除该调用之前,每次投递都会返回 404。请参阅确保 webhook 路由是公开的。

完整的 Webhook 处理程序(Next.js App Router)

// app/api/webhooks/route.ts
import { verifyWebhook } from '@clerk/nextjs/webhooks'
import { NextRequest } from 'next/server'
import { db } from '@/lib/db'

export async function POST(req: NextRequest) {
  // ALWAYS verify - never skip, even for notification-only handlers
  let evt
  try {
    evt = await verifyWebhook(req) // uses CLERK_WEBHOOK_SIGNING_SECRET automatically
  } catch (err) {
    console.error('Webhook verification failed:', err)
    return new Response('Verification failed', { status: 400 })
  }

  if (evt.type === 'user.created') {
    const { id, email_addresses, first_name, last_name } = evt.data
    const email = email_addresses[0]?.email_address
    const name = `${first_name ?? ''} ${last_name ?? ''}`.trim()
    await db.users.create({ data: { clerkId: id, email, name } })
  }

  if (evt.type === 'user.updated') {
    const { id, email_addresses, first_name, last_name } = evt.data
    const email = email_addresses[0]?.email_address
    await db.users.update({ where: { clerkId: id }, data: { email, first_name, last_name } })
  }

  if (evt.type === 'user.deleted') {
    const { id } = evt.data
    await db.users.delete({ where: { clerkId: id } })
  }

  if (evt.type === 'organizationMembership.created') {
    const { organization, public_user_data, role } = evt.data
    const orgId = organization.id
    const userId = public_user_data.user_id
    await db.teamMembers.create({ data: { orgId, userId, role } })
  }

  if (evt.type === 'organizationMembership.deleted') {
    const { organization, public_user_data } = evt.data
    const orgId = organization.id
    const userId = public_user_data.user_id
    await db.teamMembers.delete({ where: { orgId_userId: { orgId, userId } } })
  }

  return new Response('OK', { status: 200 })
}

完整示例:用户创建时发送欢迎邮件(Resend)+ Slack 通知

仅用于发送通知的处理程序仍需验证签名。使用与数据库同步处理程序相同的模式:

// app/api/webhooks/route.ts
import { verifyWebhook } from '@clerk/nextjs/webhooks'
import { NextRequest } from 'next/server'
import { Resend } from 'resend'

const resend = new Resend(process.env.RESEND_API_KEY)

export async function POST(req: NextRequest) {
  // Step 1: ALWAYS verify the webhook signature - NEVER skip this
  let evt
  try {
    evt = await verifyWebhook(req) // uses CLERK_WEBHOOK_SIGNING_SECRET env var
  } catch (err) {
    console.error('Webhook verification failed:', err)
    return new Response('Verification failed', { status: 400 })
  }

  // Step 2: Listen for user.created event
  if (evt.type === 'user.created') {
    // Step 3: Extract user email and name from webhook payload
    const { id, email_addresses, first_name, last_name } = evt.data
    const email = email_addresses[0]?.email_address
    const name = `${first_name ?? ''} ${last_name ?? ''}`.trim()

    // Step 4: Call Resend API to send welcome email
    await resend.emails.send({
      from: '[email protected]',
      to: email,
      subject: 'Welcome!',
      html: `<p>Hi ${name}, welcome to our app!</p>`,
    })

    // Step 5: Post notification to Slack channel
    await fetch(process.env.SLACK_WEBHOOK_URL!, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        text: `New user signed up: ${name} (${email})`,
      }),
    })
  }

  // Always return 200 to acknowledge receipt
  return new Response('OK', { status: 200 })
}

完整示例:将组织成员资格同步到数据库

// app/api/webhooks/route.ts
import { verifyWebhook } from '@clerk/nextjs/webhooks'
import { NextRequest } from 'next/server'
import { db } from '@/lib/db' // your database client

export async function POST(req: NextRequest) {
  // ALWAYS verify signature - never skip, even for simple handlers
  let evt
  try {
    evt = await verifyWebhook(req) // uses CLERK_WEBHOOK_SIGNING_SECRET env var
  } catch (err) {
    console.error('Webhook verification failed:', err)
    return new Response('Verification failed', { status: 400 })
  }

  if (evt.type === 'organization.created') {
    const { id, name } = evt.data
    await db.workspaces.create({
      data: { orgId: id, name, createdAt: new Date() },
    })
  }

  if (evt.type === 'organizationMembership.created') {
    // Extract organization ID, user ID, and role from payload
    const { organization, public_user_data, role } = evt.data
    const orgId = organization.id
    const userId = public_user_data.user_id

    // Add to team_members table
    await db.team_members.create({
      data: { orgId, userId, role },
    })

    // Create workspace record for new member
    await db.workspaces.create({
      data: { orgId, userId, createdAt: new Date() },
    })
  }

  if (evt.type === 'organizationMembership.deleted') {
    // Extract organization ID and user ID from payload
    const { organization, public_user_data } = evt.data
    const orgId = organization.id
    const userId = public_user_data.user_id

    // Remove from team_members table
    await db.team_members.delete({
      where: { orgId, userId },
    })

    // Remove workspace record
    await db.workspaces.deleteMany({
      where: { orgId, userId },
    })
  }

  // Return 200 status on success
  return new Response('OK', { status: 200 })
}

其他框架

对于 Express、Astro、Fastify、Nuxt、React Router 和 TanStack Start,请使用框架专用的 verifyWebhook 适配器。每个 Clerk SDK 包都提供自己的适配器(@clerk/express/webhooks、@clerk/astro/webhooks、@clerk/fastify/webhooks 等)。

请参阅 references/frameworks.md,查看各框架的完整处理程序示例。

evt.data 的类型收窄

verifyWebhook 返回 WebhookEvent,这是一个涵盖所有事件类型的判别联合类型。使用 evt.type 进行收窄,即可对 evt.data 进行类型安全的访问:

const evt = await verifyWebhook(req)

if (evt.type === 'user.created') {
  // evt.data is now UserJSON, autocompletes id, email_addresses, etc.
  console.log(evt.data.id)
}

如需手动为嵌套负载指定类型,请从框架的 webhook 子路径导入 JSON 类型:DeletedObjectJSON、EmailJSON、OrganizationInvitationJSON、OrganizationJSON、OrganizationMembershipJSON、SessionJSON、SMSMessageJSON、UserJSON。

负载字段参考

用户事件(user.created、user.updated、user.deleted)
const {
  id,                  // Clerk user ID
  email_addresses,     // array; [0].email_address is primary email
  first_name,
  last_name,
  image_url,
  public_metadata,
} = evt.data
组织事件(organization.created、organization.updated、organization.deleted)
const {
  id,    // org ID
  name,  // org name
  slug,
} = evt.data
组织成员资格事件(organizationMembership.created、organizationMembership.updated、organizationMembership.deleted)
const {
  organization,        // { id, name, ... }
  public_user_data,    // { user_id, first_name, last_name, ... }
  role,                // e.g. 'org:admin', 'org:member'
} = evt.data
// Access: organization.id, public_user_data.user_id, role

支持的事件(完整目录)

用户:user.created user.updated user.deleted

会话:session.created session.ended session.removed session.revoked

组织:organization.created organization.updated organization.deleted

组织成员资格:organizationMembership.created organizationMembership.updated organizationMembership.deleted

组织域名:organizationDomain.created organizationDomain.updated organizationDomain.deleted

组织邀请:organizationInvitation.accepted organizationInvitation.created organizationInvitation.revoked

通信:email.created sms.created

候补名单:waitlistEntry.created waitlistEntry.updated

权限:permission.created permission.updated permission.deleted

角色:role.created role.updated role.deleted

订阅:subscription.created subscription.updated subscription.active subscription.pastDue

订阅项:subscriptionItem.created subscriptionItem.active subscriptionItem.updated subscriptionItem.canceled subscriptionItem.upcoming subscriptionItem.ended subscriptionItem.abandoned subscriptionItem.incomplete subscriptionItem.pastDue subscriptionItem.freeTrialEnding

付款:paymentAttempt.created paymentAttempt.updated

Webhook 可靠性

重试:Svix 会按照设定的计划重试失败的 webhook(参见 Svix 重试计划)。返回 2xx 表示成功,返回 4xx/5xx 则会重试。使用 svix-id 标头作为幂等键,以去重重试的事件。

重放:可以从 Dashboard 重放失败的 webhook。

常见问题

症状原因解决方法
验证失败(Next.js)导入或用法错误使用 @clerk/nextjs/webhooks,直接传入 req
验证失败(Express)使用了 express.json()在 webhook 路由中使用 express.raw({ type: 'application/json' })
找不到路由(404)路径错误使用 /api/webhooks 或保留现有路径
每次投递都返回 401 或 404auth.protect() 覆盖了 webhook 路由(在中间件或处理程序中)移除此检查;路由只需使用 `verifyWebhook()``
数据库中没有数据异步任务仍在等待处理等待/检查日志
条目重复只处理了 user.created也处理 user.updated
超时处理程序太慢将工作放入异步队列,先返回 200

测试与部署

本地:使用 Clerk CLI 的官方隧道,无需身份验证或关联项目:

clerk webhooks listen --token "$(clerk webhooks token)" --forward-to http://localhost:3000/api/webhooks

将输出的中继 URL(https://webhooks.clerk.com/in/c_.../)添加为 Dashboard 中的 webhook 端点——完成此操作后,事件才会开始流动。svix-* 标头会保留,因此 verifyWebhook() 可以像往常一样使用该端点的签名密钥进行验证。相关标志、离线签名检查(clerk webhooks verify)和 Agent 模式行为,请参阅 clerk-cli 技能。如不使用 CLI,请自行隧道转发 localhost:3000(使用 ngrok、localtunnel、Cloudflare Tunnel),并将公开 URL 添加到 Dashboard 端点。

生产环境:将 webhook 端点 URL 更新为生产域名。将 CLERK_WEBHOOK_SIGNING_SECRET 复制到生产环境变量中。

参考资料

参考资料说明
references/frameworks.mdExpress、Astro、Fastify、Nuxt、React Router、TanStack Start 的 webhook 处理程序示例

另请参阅

  • clerk-cli - 用于本地 webhook 测试的 clerk webhooks listen/verify
  • clerk-setup - Clerk 初始安装
  • clerk-orgs - 组织成员资格事件
  • clerk-billing - 订阅、订阅项和付款尝试事件
  • clerk-backend-api - 通过直接 API 调用进行同步

评论

支持 Markdown