Webhook を使うと、バックエンドは RealtimeKit のイベントを発生時点で受け取れます。RealtimeKit は、購読しているイベント(ミーティング開始、参加者の入室、録画のアップロードなど)が起きると、設定したエンドポイントへ JSON ペイロード付きの HTTP POST リクエストを送ります。
非同期イベントに依存するバックエンド処理(ミーティング後の処理開始、トランスクリプトのダウンロード、録画状態の追跡、独自のセッション記録の更新など)に Webhook を使います。
- バックエンドに
POSTリクエストを受け取れる HTTP エンドポイントを作成します。 - RealtimeKit の Webhooks API にエンドポイント URL を登録します。
- Webhook を発火させるイベント種別を選びます。
rtk-signatureヘッダーで受信リクエストを検証します。- イベントを受け付けたあと、
2xxレスポンスを返します。
Webhook イベントは購読方式です。エンドポイントが受け取るのは、Webhook の events 配列に含まれるイベントだけです。
Webhook エンドポイントは JSON の POST リクエストを受け付ける必要があります。リクエストボディの event フィールドで分岐すれば、複数のイベント種別を同じエンドポイントで処理できます。
async function handleEvent(event) {
switch (event.event) {
case "meeting.participantJoined":
// Update attendance records.
break;
case "recording.statusUpdate":
// Track recording state changes.
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":
// Update attendance records.
break;
case "recording.statusUpdate":
// Track recording state changes.
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 の呼び出しなど、時間がかかる処理はバックグラウンドジョブへ移します。
公開アクセス可能なエンドポイント URL を、RealtimeKit の Webhooks API で登録します。
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 ダッシュボード ↗ からも Webhook を管理できます。
RealtimeKit は、配信の識別、重複排除、検証に使えるヘッダーを付けます。
| ヘッダー | 説明 |
|---|---|
rtk-signature |
リクエストボディの Base64 エンコードされた RSA-SHA256 署名です。RealtimeKit からのリクエストであることを検証するために使います。 |
rtk-uuid |
Webhook 配信の一意な ID です。重複配信の処理を避ける必要がある場合は、この値を保存します。 |
rtk-webhook-id |
配信をトリガーした Webhook 設定の ID です。 |
RealtimeKit は各 Webhook のリクエストボディを RSA-SHA256 で署名します。イベントを処理する前に署名を検証してください。
RealtimeKit の Webhook 公開鍵は、次の URL から取得します。
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) {
// Process the 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> {
// Process the event.
}
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"
}