mcpField
C
clerk-webhooks

clerk-webhooks

github.com/clerk開発8953.3k 回インストール今日更新
リポジトリ

clerk-webhooks について

clerk-webhooksは、ClerkのWebhookハンドラーを実装する開発者向けのスキルです。複数のフレームワークでの署名検証とイベント処理に加え、非同期配信、再試行、冪等性を説明します。

主な機能

  1. 01verifyWebhook(req)による署名検証
  2. 02ユーザー・組織・サブスクリプションイベントの処理
  3. 03イベント種別に応じたデータの型絞り込み
  4. 04複数フレームワークのハンドラー例
  5. 05再試行と重複イベントへの対応
  6. 06ローカル転送テストのサポート

活用シーン

  1. 01Clerkのユーザーイベントをデータベースに同期
  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

詳細

Webhook

すべてのハンドラーで verifyWebhook(req) による検証を行う、完全に動作するWebhookハンドラーを出力します。

Webhookを使用する場面

Webhookは非同期で、結果整合性があります。配信は速いものの、即時配信が保証されているわけではなく、失敗することもあります(Svixは固定スケジュールで再試行します)。次の用途に使用してください。

  • データベースの同期(Clerkに追従する別のユーザー/組織テーブル)
  • 通知(ウェルカムメール、Slackへの通知、内部アラート)
  • ライフサイクルイベントをトリガーとする連携

オンボーディングのような同期フロー(「ユーザーがサインアップした後、DBからXを読み取る」など)の一部として、Webhookの配信を前提にしないでください。ユーザーが作成したばかりのデータについては、Clerkセッショントークンから読み取るか、Backend APIを直接呼び出してください。セッショントークンに含まれない、他のユーザーやイベントのデータが必要な場合に、Webhookがその不足を補います。

すべてのWebhookを検証する

フレームワーク固有のパッケージ(@clerk/nextjs/webhooks、@clerk/express/webhooks など)の verifyWebhook(req) を使用してください。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 })
}

完全な例:user.createdでのウェルカムメール(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 ヘッダーを冪等性キーとして使用してください。

再送:失敗した webhook は Dashboard から再送できます。

よくある問題

症状原因対処方法
検証に失敗する(Next.js)インポートまたは使い方が誤っている@clerk/nextjs/webhooks を使用し、req を直接渡します
検証に失敗する(Express)express.json() を使用しているwebhook のルートには express.raw({ type: 'application/json' }) を使用します
ルートが見つからない(404)パスが誤っている/api/webhooks を使用するか、既存のパスを維持します
配信のたびに 401 または 404 が返るauth.protect() が(ミドルウェアまたはハンドラー内で)webhook のルートを対象にしているそのチェックを削除します。ルートに必要なチェックは verifyWebhook() のみです
DB にデータがない非同期ジョブが保留中待機してログを確認します
エントリーが重複する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)、エージェントモードの動作については、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 の直接呼び出しによる同期

コメント