跳到正文

Webhooks

订阅 XID 事件,在用户、会话和组织发生变更时接收已签名的 HTTP payload。

事件命名

事件遵循 <object>.<action> 格式。每个事件包含稳定的类型名称、唯一的 svix-id 和 ISO 8601 时间戳。

对象 操作
user created, updated, deleted
session created, ended, removed, revoked
organization created, updated, deleted
organizationMembership created, updated, deleted
organizationInvitation created, accepted, revoked
organizationDomain created, updated, deleted, verified, verification_failed
authentication password_succeeded, password_failed, passkey_succeeded, passkey_failed, mfa_succeeded, mfa_failed, oauth_succeeded, oauth_failed, sso_succeeded, sso_failed, magic_auth_succeeded, magic_auth_failed, email_verification_succeeded, email_verification_failed, radar_risk_detected
connection activated, deactivated, deleted, saml_certificate_renewed, renewal_required
dsync activated, deleted, user.created, user.updated, user.deleted, group.created, group.updated, group.deleted, group.user_added, group.user_removed
role created, updated, deleted
permission created, updated, deleted
email created(开发者接管发送时触发)
sms created(开发者接管发送时触发)
billing subscription.created, subscription.updated, paymentAttempt.succeeded, paymentAttempt.failed

payload 结构

每次 webhook 投递都是带 Content-Type: application/json 的 HTTP POST 请求。请求体包含 typedata 和顶层元数据头。

{
  "type": "user.created",
  "data": {
    "id": "usr_01abc",
    "email_addresses": [{ "email_address": "alice@example.com" }],
    "created_at": 1700000000000
  }
}

签名验证

XID 对每次投递使用 HMAC-SHA256 签名。在处理 payload 前请先验证签名。拒绝 5 分钟前的投递以防重放攻击。

请求头 描述
svix-id 唯一消息 ID。用于对重试投递去重。
svix-timestamp 消息发送时的 Unix 时间戳(秒)。
svix-signature 使用端点签名密钥对 ${svix-id}.${svix-timestamp}.${raw-body} 计算的 Base64 编码 HMAC-SHA256。
// Node.js / Cloudflare Workers example
async function verifyWebhook(request, secret) {
  const svixId = request.headers.get('svix-id')
  const svixTimestamp = request.headers.get('svix-timestamp')
  const svixSignature = request.headers.get('svix-signature')
  const body = await request.text()

  // Reject messages older than 5 minutes
  const ts = Number(svixTimestamp)
  if (Math.abs(Date.now() / 1000 - ts) > 300) {
    throw new Error('webhook timestamp out of tolerance')
  }

  const signedContent = `${svixId}.${svixTimestamp}.${body}`
  const keyData = Uint8Array.from(atob(secret), c => c.charCodeAt(0))
  const key = await crypto.subtle.importKey(
    'raw', keyData, { name: 'HMAC', hash: 'SHA-256' }, false, ['verify']
  )
  const msgData = new TextEncoder().encode(signedContent)

  // svix-signature may contain multiple comma-separated values
  for (const sig of svixSignature.split(' ')) {
    const prefix = 'v1,'
    if (!sig.startsWith(prefix)) continue
    const sigBytes = Uint8Array.from(atob(sig.slice(prefix.length)), c => c.charCodeAt(0))
    const valid = await crypto.subtle.verify('HMAC', key, sigBytes, msgData)
    if (valid) return JSON.parse(body)
  }
  throw new Error('invalid webhook signature')
}

重试与死信

  • 投递失败会以指数退避方式重试。达到最大重试次数后,消息会写入 D1 的死信存储以供人工检查。
  • 交付通过 Cloudflare Queues 与认证路径解耦。端点缓慢或不可用不会影响登录延迟。
  • 使用 svix-id 请求头在接收端去重。重试的投递与原始尝试携带相同的 svix-id
flowchart LR
  XID --> Queue
  Queue -->|HTTPS| Endpoint
  Endpoint -->|2xx| ACK
  Endpoint -->|non-2xx| Retry
  Retry --> Queue
  Retry -->|max_retries| dlq["D1 DLQ"]

手动重放

使用 POST /v1/webhooks/:endpointId/replay 按消息 ID 或时间范围重放事件。重放的事件携带新的 svix-id,但保留原始的 typedata

# Replay events from the last hour
curl -X POST https://xid.dev/v1/webhooks/whe_xxx/replay \
  -H 'Authorization: Bearer sk_live_xxx' \
  -H 'Content-Type: application/json' \
  -d '{ "since": "2024-01-01T00:00:00Z", "until": "2024-01-01T01:00:00Z" }'

事件 API

除 push webhook 外,XID 还通过 GET /v1/events 公开带游标分页的有序不可变事件流。主动拉取事件流可实现可靠同步,不会在 webhook 重试间隙丢失事件。

curl 'https://xid.dev/v1/events?limit=100&after=evt_xxx' \
  -H 'Authorization: Bearer sk_live_xxx'
导航

输入内容以搜索...

使用方向键导航按 Enter 键选择按 Escape 键关闭