Webhooks 让您的后端能够实时接收 RealtimeKit 事件。当订阅的事件发生时(例如会议开始、参与者加入或录制文件上传),RealtimeKit 会向您配置的端点发送带有 JSON 负载的 HTTP POST 请求。
在依赖异步事件的后端工作流中使用 webhooks,例如开始会后处理、下载转录文本、跟踪录制状态或更新您自己的会话记录。
- 在您的后端创建一个可以接收
POST请求的 HTTP 端点。 - 将端点 URL 注册到 RealtimeKit Webhooks API。
- 选择应触发 webhook 的事件类型。
- 使用
rtk-signature请求头验证传入的请求。 - 接受事件后返回
2xx响应。
Webhook 事件仅限订阅。您的端点仅接收 webhook 的 events 数组中包含的事件。
您的 webhook 端点必须接受 JSON POST 请求。该端点可以通过在请求体中对 event 字段进行条件分支处理来处理多种事件类型。
async function handleEvent(event) {
switch (event.event) {
case "meeting.participantJoined":
// 更新考勤记录。
break;
case "recording.statusUpdate":
// 跟踪录制状态变化。
break;
default:
console.log(`Unhandled RealtimeKit event: ${event.event}`);
}
}
export default {
async fetch(request, _env, ctx) {
const url = new URL(request.url);
if (request.method !== "POST" || url.pathname !== "/webhook") {
return new Response("Not found", { status: 404 });
}
const event = await request.json();
ctx.waitUntil(handleEvent(event));
return new Response(null, { status: 200 });
},
};type RealtimeKitWebhookEvent = {
event: string;
};
async function handleEvent(event: RealtimeKitWebhookEvent): Promise<void> {
switch (event.event) {
case "meeting.participantJoined":
// 更新考勤记录。
break;
case "recording.statusUpdate":
// 跟踪录制状态变化。
break;
default:
console.log(`Unhandled RealtimeKit event: ${event.event}`);
}
}
export default {
async fetch(request, _env, ctx): Promise<Response> {
const url = new URL(request.url);
if (request.method !== "POST" || url.pathname !== "/webhook") {
return new Response("Not found", { status: 404 });
}
const event = await request.json<RealtimeKitWebhookEvent>();
ctx.waitUntil(handleEvent(event));
return new Response(null, { status: 200 });
},
} satisfies ExportedHandler;您的端点应该在接受事件后立即返回 2xx 响应。将耗时较长的工作(例如下载文件或调用第三方 API)移动到后台任务中。
使用 RealtimeKit Webhooks API 注册公开可访问的端点 URL:
curl --request POST "https://api.cloudflare.com/client/v4/accounts/$ACCOUNT_ID/realtime/kit/$APP_ID/webhooks" \
--header "Authorization: Bearer $CLOUDFLARE_API_TOKEN" \
--header "Content-Type: application/json" \
--data '{
"name": "Production webhook",
"url": "https://example.com/webhook",
"events": [
"meeting.started",
"meeting.ended",
"meeting.participantJoined",
"meeting.participantLeft",
"recording.statusUpdate"
],
"enabled": true
}'您还可以从 RealtimeKit 仪表板 ↗管理 webhooks。
RealtimeKit 包含请求头,可帮助您识别、去重和验证 webhook 递送:
| 请求头 | 描述 |
|---|---|
rtk-signature |
请求体的 Base64 编码 RSA-SHA256 签名。使用此请求头来验证请求是否来自 RealtimeKit。 |
rtk-uuid |
webhook 递送的唯一 ID。如果需要避免处理重复递送,请存储此值。 |
rtk-webhook-id |
触发此递送的 webhook 配置的 ID。 |
RealtimeKit 使用 RSA-SHA256 对每个 webhook 请求体进行签名。在处理事件之前验证签名。
从以下地址获取 RealtimeKit webhook 公钥:
curl "https://api.realtime.cloudflare.com/.well-known/webhooks.json"响应包含 PEM 编码的公钥:
{
"success": true,
"data": {
"publicKey": "-----BEGIN PUBLIC KEY-----\n...\n-----END PUBLIC KEY-----"
},
"message": ""
}根据原始请求体验证 rtk-signature。在验证之前不要对已解析的 JSON 进行重新序列化,因为空格或键顺序的变化会改变已签名的字节。
async function verifySignature(publicKeyPem, signature, body) {
const publicKey = await crypto.subtle.importKey(
"spki",
Uint8Array.from(atob(publicKeyPem), (c) => c.charCodeAt(0)),
{ name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" },
false,
["verify"],
);
return crypto.subtle.verify(
"RSASSA-PKCS1-v1_5",
publicKey,
Uint8Array.from(atob(signature), (c) => c.charCodeAt(0)),
body,
);
}
async function handleEvent(event) {
// 处理事件。
}
export default {
async fetch(request, env, ctx) {
const signature = request.headers.get("rtk-signature");
if (!signature) {
return new Response("Missing signature", {
status: 400,
});
}
const body = await request.arrayBuffer();
const resp = await fetch(env.REALTIMEKIT_WEBHOOK_PUBLIC_KEY_URL);
if (!resp.ok) {
return new Response("Missing public key", {
status: 400,
});
}
const respBody = await resp.json();
const cleanPem = respBody.data.publicKey
.replace(/\\n/g, "")
.replace(/-----BEGIN PUBLIC KEY-----/, "")
.replace(/-----END PUBLIC KEY-----/, "")
.replace(/\s+/g, "");
const verified = await verifySignature(cleanPem, signature, body);
if (!verified) {
return new Response("Invalid signature", { status: 401 });
}
const event = JSON.parse(new TextDecoder().decode(body));
ctx.waitUntil(handleEvent(event));
return new Response(null, { status: 200 });
},
};type Env = {
REALTIMEKIT_WEBHOOK_PUBLIC_KEY_URL: string;
};
type RealtimeKitWebhookEvent = {
event: string;
};
async function verifySignature(
publicKeyPem: string,
signature: string,
body: ArrayBuffer,
): Promise<boolean> {
const publicKey = await crypto.subtle.importKey(
"spki",
Uint8Array.from(atob(publicKeyPem), (c) => c.charCodeAt(0)),
{ name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" },
false,
["verify"],
);
return crypto.subtle.verify(
"RSASSA-PKCS1-v1_5",
publicKey,
Uint8Array.from(atob(signature), (c) => c.charCodeAt(0)),
body,
);
}
async function handleEvent(event: RealtimeKitWebhookEvent): Promise<void> {
// 处理事件。
}
export default {
async fetch(
request: Request,
env: Env,
ctx: ExecutionContext,
): Promise<Response> {
const signature = request.headers.get("rtk-signature");
if (!signature) {
return new Response("Missing signature", {
status: 400,
});
}
const body = await request.arrayBuffer();
const resp = await fetch(env.REALTIMEKIT_WEBHOOK_PUBLIC_KEY_URL);
if (!resp.ok) {
return new Response("Missing public key", {
status: 400,
});
}
const respBody = await resp.json<{
success: true;
data: { publicKey: string };
}>();
const cleanPem = respBody.data.publicKey
.replace(/\\n/g, "")
.replace(/-----BEGIN PUBLIC KEY-----/, "")
.replace(/-----END PUBLIC KEY-----/, "")
.replace(/\s+/g, "");
const verified = await verifySignature(cleanPem, signature, body);
if (!verified) {
return new Response("Invalid signature", { status: 401 });
}
const event = JSON.parse(new TextDecoder().decode(body));
ctx.waitUntil(handleEvent(event));
return new Response(null, { status: 200 });
},
} satisfies ExportedHandler<Env>;RealtimeKit 将任何 2xx 响应视为成功递送。
如果您的端点返回 5xx 响应,或者请求因网络错误而失败,RealtimeKit 将重试递送。如果您的端点返回低于 500 的非 2xx 响应,RealtimeKit 会将递送记录为失败且不会重试。
在多次递送失败后,RealtimeKit 可能会暂时减少对该 webhook URL 的递送尝试。请仅在您的应用程序接受事件后才返回 2xx 响应。
RealtimeKit 支持这些 webhook 事件:
| 事件 | 触发条件 |
|---|---|
meeting.started |
第一个参与者加入会议。 |
meeting.ended |
会议结束,原因为主持人结束了会议或所有参与者均已离开。 |
meeting.participantJoined |
参与者加入会议。 |
meeting.participantLeft |
参与者离开会议。 |
meeting.chatSynced |
已完成会议的聊天记录导出可用。 |
recording.statusUpdate |
录制状态发生改变。 |
livestreaming.statusUpdate |
直播状态发生改变。 |
meeting.transcript |
已完成会议的转录文本可用。 |
meeting.summary |
AI 生成的已完成会议摘要可用。 |
使用 Webhooks API 获取当前的事件列表:
curl "https://api.cloudflare.com/client/v4/accounts/$ACCOUNT_ID/realtime/kit/$APP_ID/webhooks/all" \
--header "Authorization: Bearer $CLOUDFLARE_API_TOKEN"所有 webhook 负载都包含 event 字段。其余字段取决于事件类型。
{
"event": "meeting.started",
"meeting": {
"id": "bbb8940e-1b97-402a-97d6-2708b7feca41",
"title": "Weekly sync",
"status": "LIVE",
"createdAt": "2026-06-03T10:00:00.000Z",
"sessionId": "05e57591-d89e-45c9-ae44-08dc1eaad0e0",
"startedAt": "2026-06-03T10:00:00.000Z",
"organizedBy": {
"id": "c94c437b-592a-4a39-b9e2-47ef1451e43b",
"name": "Example organization"
}
}
}{
"event": "meeting.ended",
"meeting": {
"id": "bbb8940e-1b97-402a-97d6-2708b7feca41",
"sessionId": "05e57591-d89e-45c9-ae44-08dc1eaad0e0",
"title": "Weekly sync",
"status": "LIVE",
"createdAt": "2026-06-03T10:00:00.000Z",
"startedAt": "2026-06-03T10:00:00.000Z",
"endedAt": "2026-06-03T10:30:00.000Z",
"organizedBy": {
"id": "c94c437b-592a-4a39-b9e2-47ef1451e43b",
"name": "Example organization"
}
},
"reason": "ALL_PARTICIPANTS_LEFT"
}reason 的值可以是 HOST_ENDED_MEETING 或 ALL_PARTICIPANTS_LEFT。
{
"event": "meeting.participantJoined",
"meeting": {
"id": "bbb8940e-1b97-402a-97d6-2708b7feca41",
"sessionId": "05e57591-d89e-45c9-ae44-08dc1eaad0e0",
"title": "Weekly sync",
"status": "LIVE",
"createdAt": "2026-06-03T10:00:00.000Z",
"startedAt": "2026-06-03T10:00:00.000Z",
"organizedBy": {
"id": "c94c437b-592a-4a39-b9e2-47ef1451e43b",
"name": "Example organization"
}
},
"participant": {
"peerId": "e32fb785-ddd0-4b96-b577-879327c0082f",
"userDisplayName": "Mary Sue",
"customParticipantId": "user-123",
"joinedAt": "2026-06-03T10:05:00.000Z"
}
}使用 customParticipantId 作为您自己的参与者标识符。为了与旧版集成兼容,保留了 clientSpecificId。
{
"event": "meeting.participantLeft",
"meeting": {
"id": "bbb8940e-1b97-402a-97d6-2708b7feca41",
"title": "Weekly sync",
"status": "LIVE",
"createdAt": "2026-06-03T10:00:00.000Z",
"sessionId": "05e57591-d89e-45c9-ae44-08dc1eaad0e0",
"startedAt": "2026-06-03T10:00:00.000Z",
"endedAt": "2026-06-03T10:30:00.000Z",
"organizedBy": {
"id": "c94c437b-592a-4a39-b9e2-47ef1451e43b",
"name": "Example organization"
}
},
"participant": {
"peerId": "e32fb785-ddd0-4b96-b577-879327c0082f",
"userDisplayName": "Mary Sue",
"customParticipantId": "user-123",
"joinedAt": "2026-06-03T10:05:00.000Z",
"leftAt": "2026-06-03T10:25:00.000Z"
}
}{
"event": "meeting.chatSynced",
"title": "Weekly sync",
"endedAt": "2026-06-03T10:30:00.000Z",
"createdAt": "2026-06-03T10:00:00.000Z",
"meetingId": "bbb8940e-1b97-402a-97d6-2708b7feca41",
"sessionId": "05e57591-d89e-45c9-ae44-08dc1eaad0e0",
"startedAt": "2026-06-03T10:00:00.000Z",
"chatDownloadUrl": "https://example.com/chat.json",
"chatDownloadUrlExpiry": "2026-06-10T10:30:00.000Z",
"organizedBy": {
"id": "c94c437b-592a-4a39-b9e2-47ef1451e43b",
"name": "Example organization"
}
}当录制经历其生命周期时,RealtimeKit 会发送 recording.statusUpdate。录制状态包括 RECORDING、UPLOADING、UPLOADED 和 ERRORED。欲了解更多信息,请参阅 监控录制状态。
{
"event": "recording.statusUpdate",
"recording": {
"id": "97cb480d-5840-4528-ace3-919b5e386c68",
"recordingId": "97cb480d-5840-4528-ace3-919b5e386c68",
"status": "UPLOADED",
"downloadUrl": "https://example.com/recording.mp4",
"audioDownloadUrl": "https://example.com/recording.mp3",
"downloadUrlExpiry": "2026-06-10T10:30:00.000Z",
"startedTime": "2026-06-03T10:00:00.000Z",
"stoppedTime": "2026-06-03T10:30:00.000Z",
"fileSize": "2044680",
"outputFileName": "weekly-sync.mp4",
"meetingId": "50c8940e-1b97-402a-97d6-2708b7feca41",
"recordingDuration": 1800,
"organizationId": "c94c437b-592a-4a39-b9e2-47ef1451e43b",
"roomUUID": "05e57591-d89e-45c9-ae44-08dc1eaad0e0"
},
"meeting": {
"id": "bbb8940e-1b97-402a-97d6-2708b7feca41",
"sessionId": "05e57591-d89e-45c9-ae44-08dc1eaad0e0",
"title": "Weekly sync",
"status": "LIVE",
"createdAt": "2026-06-03T10:00:00.000Z",
"startedAt": "2026-06-03T10:00:00.000Z",
"endedAt": "2026-06-03T10:30:00.000Z",
"organizedBy": {
"id": "c94c437b-592a-4a39-b9e2-47ef1451e43b",
"name": "Example organization"
}
}
}直播状态包括 LIVE、OFFLINE 和 IDLE。
{
"event": "livestreaming.statusUpdate",
"streamId": "d231d346-c422-43a6-a324-c0d65b79c8a7",
"status": "LIVE",
"manualIngest": false,
"playbackUrl": "https://example.com/live.m3u8",
"ingestServer": "rtmps://example.com/live",
"streamKey": "stream-key",
"meeting": {
"id": "bbb8940e-1b97-402a-97d6-2708b7feca41",
"title": "Weekly sync",
"createdAt": "2026-06-03T10:00:00.000Z",
"status": "LIVE",
"organizedBy": {
"id": "c94c437b-592a-4a39-b9e2-47ef1451e43b",
"name": "Example organization"
}
}
}{
"event": "meeting.transcript",
"meeting": {
"id": "bbb8940e-1b97-402a-97d6-2708b7feca41",
"title": "Weekly sync",
"endedAt": "2026-06-03T10:30:00.000Z",
"createdAt": "2026-06-03T10:00:00.000Z",
"sessionId": "05e57591-d89e-45c9-ae44-08dc1eaad0e0",
"startedAt": "2026-06-03T10:00:00.000Z",
"status": "LIVE",
"organizedBy": {
"id": "c94c437b-592a-4a39-b9e2-47ef1451e43b",
"name": "Example organization"
}
},
"transcriptDownloadUrl": "https://example.com/transcript.csv",
"transcriptDownloadUrlExpiry": "2026-06-10T10:30:00.000Z"
}{
"event": "meeting.summary",
"meeting": {
"id": "bbb8940e-1b97-402a-97d6-2708b7feca41",
"sessionId": "05e57591-d89e-45c9-ae44-08dc1eaad0e0",
"organizedBy": {
"id": "c94c437b-592a-4a39-b9e2-47ef1451e43b",
"name": "Example organization"
}
},
"summaryDownloadUrl": "https://example.com/summary.txt",
"summaryDownloadUrlExpiry": "2026-06-10T10:30:00.000Z"
}