이벤트 명명 규칙
이벤트는 <object>.<action> 패턴을 따릅니다. 각 이벤트는 안정적인 type 이름, 고유한 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 |
페이로드 구조
모든 webhook 전송은 Content-Type: application/json을 사용하는 HTTP POST입니다. 본문에는 type, data, 최상위 메타데이터 헤더가 포함됩니다.
{
"type": "user.created",
"data": {
"id": "usr_01abc",
"email_addresses": [{ "email_address": "alice@example.com" }],
"created_at": 1700000000000
}
}서명 검증
XID는 모든 전송에 HMAC-SHA256 서명을 합니다. 페이로드를 처리하기 전에 서명을 검증하세요. 재사용 공격을 방지하려면 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')
}재시도 및 dead-letter
- 전송 실패 시 지수 백오프로 재시도합니다. 최대 재시도 횟수 이후 메시지는 수동 검사를 위해 D1의 dead-letter 저장소에 기록됩니다.
- 전송은 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 값을 가지지만 원본 type과 data는 보존됩니다.
# 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에서 cursor 페이지네이션을 갖춘 순서 있는 불변 이벤트 스트림을 제공합니다. webhook 재시도 사이에 이벤트를 놓치지 않고 신뢰할 수 있는 동기화를 구축하려면 이 스트림을 폴링하세요.
curl 'https://xid.dev/v1/events?limit=100&after=evt_xxx' \
-H 'Authorization: Bearer sk_live_xxx'