К основному содержанию
скилл CX Codex

clerk-webhooks — скилл для Claude Code: вебхуки Clerk

Учит агента писать обработчики вебхуков Clerk с проверкой подписи и разбором событий: пользователи, сессии, организации, биллинг, платежи.

Установить в Claude Code · Через Skills CLI (Vercel) Все способы и платформы ↓
npx skills add clerk/skills --skill clerk-webhooks -a claude-code
Открыть на GitHub

Что делает clerk-webhooks

Скилл даёт агенту рабочий шаблон обработчика вебхуков Clerk: проверка verifyWebhook(req) из пакета под фреймворк (@clerk/nextjs/webhooks, @clerk/express/webhooks), разбор события, запись в базу. Событий хватает: пользователи, сессии, организации, биллинг, платежи.

Примеры собраны под Next.js App Router. Синхронизация user.created, user.updated и user.deleted в таблицу users, членство в организации в team_members. Ещё один пример шлёт приветственное письмо через Resend и пост в Slack.

Заранее нужен CLERK_WEBHOOK_SIGNING_SECRET, svix-секрет из дашборда Clerk: verifyWebhook читает его сам. Маршрут /api/webhooks(.*) держите открытым, иначе auth.protect() вернёт 404 на каждую доставку. И помните, что доставка асинхронная: Svix повторяет отправку по расписанию, поэтому данные о только что созданном пользователе берите из сессионного токена или Backend API.

Какие задачи решает

  • собрать обработчик user.created и записать нового пользователя в свою таблицу
  • проверить подпись входящего события, чтобы не ловить подделки
  • синхронизировать членство в организациях Clerk с таблицей команды
  • слать приветственное письмо и уведомление в Slack при регистрации
  • разобраться, почему доставки вебхуков отвечают 404

Примеры запросов агенту

  • Сделай обработчик вебхуков Clerk в app/api/webhooks/route.ts: проверка через verifyWebhook и запись user.created в таблицу users
  • Добавь в тот же обработчик organizationMembership.created и organizationMembership.deleted, чтобы таблица team_members не расходилась с Clerk
  • На user.created отправь письмо через Resend и пост в Slack

Как установить clerk-webhooks

1Через Skills CLI (Vercel)

npx skills add clerk/skills --skill clerk-webhooks -a claude-code

2Вручную: положить файлы

~/.claude/skills/clerk-webhooks/SKILL.md

Папка скилла целиком; для одного проекта — .claude/skills/clerk-webhooks/

По шагам, с командами для каждой платформы: Как установить скилл в Claude Code: пять способов

Из того же набора19

clerk-setup Настраивает аутентификацию Clerk через официальный CLI или создаёт новый проект с готовой авторизацией. Помогает подключить провайдера без ручной правки конфигов. CX Codex +1 56 600установок clerk-custom-ui Скилл по Clerk: агент собирает свою форму входа на хуках useSignIn и useSignUp либо перекрашивает готовые компоненты под бренд. CX Codex +1 55 200установок clerk-nextjs-patterns Скилл обучает ИИ-агента паттернам аутентификации Clerk в Next.js: от серверных функций до кэширования. Пригодится при настройке middleware, Server Actions и защите роутов. CX Codex +1 53 400установок clerk-orgs Скилл помогает подключить организации Clerk для командных пространств: настроить роли, приглашения и доступ к данным по организациям. Нужен для B2B- и многопользовательских приложений. CX Codex +1 50 700установок clerk-testing Проводит агента по настройке E2E-тестов входа через Clerk в Playwright или Cypress и держит тестовые ключи отдельно от продакшн-конфига. CX Codex +1 46 300установок clerk-cli Скилл учит агента работать с бинарником clerk вместо ручных curl: пользователи, организации, сессии, вебхуки, деплой. CX Codex +1 33 800установок clerk Разбирает запрос про Clerk и отправляет к нужному скиллу: настройка, CLI, паттерны фреймворков, биллинг, вебхуки. CX Codex +1 30 000установок clerk-react-patterns Скилл помогает настроить аутентификацию в React SPA на базе @clerk/react. Пригодится при сборке проектов на Vite или Create React App с закрытыми маршрутами. CX Codex +1 19 700установок

Похожие

clerk-nextjs-skills gocallum Подключает аутентификацию Clerk в Next.js 16 с App Router: proxy.ts, переменные окружения, защита маршрутов и MCP-сервер. CX Codex +1 436установок clerk sanjeed5 Правило для Cursor задаёт жёсткие требования к аутентификации Clerk: PKCE, серверные проверки, короткие токены. Пригодится, когда в проекте уже есть Clerk и нужен порядок в безопасности. ★ 3 579звёзд репо clerk-authentication Mindrally Правила подключения Clerk в Next.js: middleware, хуки, защита серверных действий и маршрутов API. Пригодится, когда проект уже на Clerk и нужен порядок в проверках доступа. CX Codex +1 683установок auth официальный Vercel Labs Помогает подключить Clerk, Descope или Auth0 к Next.js и настроить вход, регистрацию и защиту маршрутов. CX Codex GP ChatGPT 8установок setup-zoom-webhooks официальный anthropics Справочник по вебхукам Zoom: подписки, проверка подписей и обработка событий. Нужен при сборке HTTP-интеграции с Zoom. CX Codex +1 2 100установок stripe-webhooks hookdeck Скилл для работы с вебхуками Stripe: проверка подписи, разбор событий и готовые примеры обработчиков. Пригодится, когда платёжные события не проходят проверку. CX Codex +1 595установок

Рядом в теме «Бэкенд и API»

Всё по теме «Бэкенд и API» →

Вопросы о clerk-webhooks

Как установить clerk-webhooks в Claude Code?

Выполните npx skills add clerk/skills --skill clerk-webhooks -a claude-code. Скилл окажется в ~/.claude/skills/clerk-webhooks/SKILL.md.

Что делает clerk-webhooks?

Помогает агенту выдавать готовые обработчики вебхуков Clerk с обязательной проверкой подписи и разбором событий вроде user.created или organizationMembership.created.

Почему маршрут вебхука отвечает 404?

Скорее всего, на /api/webhooks(.*) стоит auth.protect(). Доставка идёт без пользовательской сессии, так что защита отбивает каждое событие, пока вызов не уберут.

Исходный текст

SKILL.md · MIT
---
name: clerk-webhooks
description: Clerk webhooks for real-time events and data syncing. Verify with verifyWebhook
  from the framework-specific package. Handle user, session, organization, billing, and
  payment events. Build event-driven features like database sync, notifications, and
  integrations.
allowed-tools: WebFetch
license: MIT
metadata:
  author: clerk
  version: 1.2.0
compatibility: Requires CLERK_WEBHOOK_SIGNING_SECRET (svix signing secret from Clerk dashboard)
---

# Webhooks

Output complete, working webhook handlers with `verifyWebhook(req)` verification in every handler.

## When to Use Webhooks

Webhooks are **asynchronous and eventually consistent**. Delivery is fast but not guaranteed to be immediate, and may occasionally fail (Svix retries on a fixed schedule). Use them for:

- Database sync (a separate users / orgs table that follows Clerk)
- Notifications (welcome emails, Slack pings, internal alerts)
- Integrations triggered by lifecycle events

Do NOT rely on webhook delivery as part of a synchronous flow such as onboarding ("user signs up, then we read X from our DB"). For data the user just created, read it from the [Clerk session token](https://clerk.com/docs/guides/sessions/session-tokens) or call the Backend API directly. Webhooks fill the gap when you need data about *other* users or events the session token doesn't carry.

## Verify Every Webhook

Use `verifyWebhook(req)` from the framework-specific package (`@clerk/nextjs/webhooks`, `@clerk/express/webhooks`, etc.). It reads `CLERK_WEBHOOK_SIGNING_SECRET` automatically and throws on bad signatures. Skipping verification, even for notification-only handlers, exposes the endpoint to spoofed events.

## Keep the Webhook Route Unprotected

Webhook deliveries carry no user session. `verifyWebhook()` is the only check the route needs. A bare `clerkMiddleware()` protects nothing, so the route is already reachable. Don't call `auth.protect()` for `/api/webhooks(.*)`, in middleware or in the handler. A project that does returns `404` for every delivery until that call is removed. See [Ensure the webhook route is public](https://clerk.com/docs/guides/development/webhooks/syncing#ensure-the-webhook-route-is-public).

## Complete Webhook Handler (Next.js App Router)

```typescript
// 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 })
}
```

## Full Example: Welcome Email (Resend) + Slack Notification on user.created

Notification-only handlers still verify the signature. Same pattern as the database-sync handler:

```typescript
// 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 })
}
```

## Full Example: Organization Membership Sync to Database

```typescript
// 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 })
}
```

## Other Frameworks

For Express, Astro, Fastify, Nuxt, React Router, and TanStack Start, use the framework-specific `verifyWebhook` adapter. Each Clerk SDK package ships its own (`@clerk/express/webhooks`, `@clerk/astro/webhooks`, `@clerk/fastify/webhooks`, etc.).

See `references/frameworks.md` for full handler examples per framework.

## Type Narrowing for `evt.data`

`verifyWebhook` returns `WebhookEvent`, a discriminated union of all event types. Narrow with `evt.type` to get type-safe access to `evt.data`:

```typescript
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)
}
```

For manual typing of nested payloads, import the JSON types from your framework's webhook subpath: `DeletedObjectJSON`, `EmailJSON`, `OrganizationInvitationJSON`, `OrganizationJSON`, `OrganizationMembershipJSON`, `SessionJSON`, `SMSMessageJSON`, `UserJSON`.

## Payload Field Reference

### User events (`user.created`, `user.updated`, `user.deleted`)
```typescript
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 events (`organization.created`, `organization.updated`, `organization.deleted`)
```typescript
const {
  id,    // org ID
  name,  // org name
  slug,
} = evt.data
```

### Organization Membership events (`organizationMembership.created`, `organizationMembership.updated`, `organizationMembership.deleted`)
```typescript
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
```

## Supported Events (Full Catalog)

**User**: `user.created` `user.updated` `user.deleted`

**Session**: `session.created` `session.ended` `session.removed` `session.revoked`

**Organization**: `organization.created` `organization.updated` `organization.deleted`

**Organization Membership**: `organizationMembership.created` `organizationMembership.updated` `organizationMembership.deleted`

**Organization Domain**: `organizationDomain.created` `organizationDomain.updated` `organizationDomain.deleted`

**Organization Invitation**: `organizationInvitation.accepted` `organizationInvitation.created` `organizationInvitation.revoked`

**Communication**: `email.created` `sms.created`

**Waitlist**: `waitlistEntry.created` `waitlistEntry.updated`

**Permission**: `permission.created` `permission.updated` `permission.deleted`

**Role**: `role.created` `role.updated` `role.deleted`

**Subscription**: `subscription.created` `subscription.updated` `subscription.active` `subscription.pastDue`

**Subscription Item**: `subscriptionItem.created` `subscriptionItem.active` `subscriptionItem.updated` `subscriptionItem.canceled` `subscriptionItem.upcoming` `subscriptionItem.ended` `subscriptionItem.abandoned` `subscriptionItem.incomplete` `subscriptionItem.pastDue` `subscriptionItem.freeTrialEnding`

**Payment**: `paymentAttempt.created` `paymentAttempt.updated`

## Webhook Reliability

**Retries**: Svix retries failed webhooks on a set schedule (see [Svix Retry Schedule](https://docs.svix.com/retries)). Return 2xx to succeed, 4xx/5xx to retry. Use the `svix-id` header as an idempotency key to deduplicate retried events.

**Replay**: Failed webhooks can be replayed from Dashboard.

## Common Pitfalls

| Symptom | Cause | Fix |
|---------|-------|-----|
| Verification fails (Next.js) | Wrong import or usage | Use `@clerk/nextjs/webhooks`, pass `req` directly |
| Verification fails (Express) | Using `express.json()` | Use `express.raw({ type: 'application/json' })` for webhook route |
| Route not found (404) | Wrong path | Use `/api/webhooks` or preserve existing path |
| 401 or 404 on every delivery | `auth.protect()` covers the webhook route (in middleware or in the handler) | Remove that check; `verifyWebhook()` is the only check the route needs |
| No data in DB | Async job pending | Wait/check logs |
| Duplicate entries | Only handling `user.created` | Also handle `user.updated` |
| Timeouts | Handler too slow | Queue async work, return 200 first |

## Testing & Deployment

**Local**: Use the Clerk CLI's first-party tunnel — no auth or linked project needed:

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

Add the printed relay URL (`https://webhooks.clerk.com/in/c_.../`) as a webhook endpoint in the Dashboard — events don't flow until you do. `svix-*` headers are preserved, so `verifyWebhook()` works against that endpoint's signing secret as usual. Flags, offline signature checks (`clerk webhooks verify`), and agent-mode behavior are in the `clerk-cli` skill. Without the CLI, tunnel `localhost:3000` yourself (`ngrok`, `localtunnel`, `Cloudflare Tunnel`) and add the public URL to the Dashboard endpoint.

**Production**: Update webhook endpoint URL to production domain. Copy `CLERK_WEBHOOK_SIGNING_SECRET` to production env vars.

## References

| Reference | Description |
|-----------|-------------|
| `references/frameworks.md` | Webhook handler examples for Express, Astro, Fastify, Nuxt, React Router, TanStack Start |

## See Also

- `clerk-cli` - `clerk webhooks listen`/`verify` for local webhook testing
- `clerk-setup` - Initial Clerk install
- `clerk-orgs` - Org membership events
- `clerk-billing` - Subscription, subscription item, and payment attempt events
- `clerk-backend-api` - Sync via direct API calls

© clerk · MIT · clerk/skills

Смотрите также

Спонсоры проекта

Компании, которые помогают держать аналитику открытой