← Cloudflare Realtime / realtime / realtimekit
Webhooks
Webhooky umožňují vašemu backendu přijímat události RealtimeKit v reálném čase. RealtimeKit odesílá HTTP POST požadavek na váš nakonfigurovaný endpoint s obsahem JSON, když dojde k odebíranému typu události, například když schůzka začne, účastník se připojí nebo se nahraje záznam.
Webhooky použijte pro backendové workflow, které závisí na asynchronních událostech, jako je spuštění zpracování po skončení schůzky, stahování přepisů, sledování stavu nahrávání nebo aktualizace vlastních záznamů relací.
Jak fungují webhooky
- Vytvořte v backendu HTTP koncový bod, který dokáže přijímat
POSTpožadavky. - Zaregistrujte URL adresu endpointu u RealtimeKit Webhooks API.
- Vyberte typy událostí, které mají webhook spouštět.
- Ověřte příchozí požadavky pomocí
rtk-signaturehlavička. - Vrátí
2xxodpověď po přijetí události.
Webhookové události jsou dostupné pouze na základě odběru. Váš endpoint přijímá pouze události, které jsou součástí odběru events pole.
Vytvoření koncového bodu webhooku
Váš webhook endpoint musí přijímat JSON POST požadavků. Endpoint dokáže zpracovat více typů událostí pomocí přepínače na event pole v těle požadavku.
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;Váš endpoint by měl vrátit 2xx odpověď ihned po přijetí události. Pomalejší úkoly, jako je stahování souborů nebo volání API třetích stran, přesuňte do úlohy na pozadí.
Zaregistrujte webhook
Zaregistrujte veřejně přístupnou URL adresu endpointu pomocí 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
}'Webhooky můžete spravovat také z dashboard RealtimeKit ↗.
Hlavičky webhooku
RealtimeKit obsahuje hlavičky, které pomáhají identifikovat, deduplikovat a ověřovat doručení webhooků:
| Hlavička | Popis |
|---|---|
rtk-signature |
Podpis RSA-SHA256 kódovaný v Base64 pro tělo požadavku. Pomocí této hlavičky ověřte, že požadavek pochází z RealtimeKit. |
rtk-uuid |
Jedinečné ID pro doručení webhooku. Uložte si tuto hodnotu, pokud potřebujete zabránit zpracování duplicitních doručení. |
rtk-webhook-id |
ID konfigurace webhooku, která spustila doručení. |
Ověřte podpisy webhooků
RealtimeKit podepisuje tělo každého požadavku webhooku pomocí RSA-SHA256. Před zpracováním události podpis ověřte.
Načtení veřejného klíče
Veřejný klíč webhooku RealtimeKit získáte z:
curl "https://api.realtime.cloudflare.com/.well-known/webhooks.json"Odpověď obsahuje veřejný klíč zakódovaný ve formátu PEM:
{
"success": true,
"data": {
"publicKey": "-----BEGIN PUBLIC KEY-----\n...\n-----END PUBLIC KEY-----"
},
"message": ""
}Ověřte tělo požadavku
Ověřte rtk-signature oproti nezpracovanému tělu požadavku. Parsovaný JSON před ověřením znovu neserializujte, protože změny v mezerách nebo pořadí klíčů mohou změnit podepsané bajty.
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>;Chování při opakování
RealtimeKit považuje jakékoli 2xx odpověď jako úspěšné doručení.
Pokud váš endpoint vrací 5xx odpověď, nebo pokud požadavek selže kvůli síťové chybě, RealtimeKit doručení zopakuje. Pokud váš endpoint vrátí jiný než 2xx odpověď níže 500, RealtimeKit označí doručení jako neúspěšné a nebude ho opakovat.
Při opakovaném selhání doručení může RealtimeKit dočasně omezit počet pokusů o doručení na danou URL adresu webhooku. Vraťte 2xx odpověď až poté, co vaše aplikace událost přijme.
Podporované události
RealtimeKit podporuje následující události webhooku:
| Událost | Trigger |
|---|---|
meeting.started |
První účastník se připojí ke schůzce. |
meeting.ended |
Schůzka končí, protože ji ukončil hostitel nebo ji opustili všichni účastníci. |
meeting.participantJoined |
Účastník se připojí ke schůzce. |
meeting.participantLeft |
Účastník opustí schůzku. |
meeting.chatSynced |
Export chatu je k dispozici po dokončení schůzky. |
recording.statusUpdate |
Nahrávání změní stav. |
livestreaming.statusUpdate |
Živé vysílání změní stav. |
meeting.transcript |
Přepis dokončené schůzky je k dispozici. |
meeting.summary |
Souhrn schůzky vygenerovaný pomocí AI je k dispozici po jejím ukončení. |
Aktuální seznam událostí načtěte pomocí Webhooks API:
curl "https://api.cloudflare.com/client/v4/accounts/$ACCOUNT_ID/realtime/kit/$APP_ID/webhooks/all" \
--header "Authorization: Bearer $CLOUDFLARE_API_TOKEN"Payloady událostí
Všechny payloady webhooků obsahují event pole. Zbývající pole závisí na typu události.
meeting.started
{
"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"
}
}
}meeting.ended
{
"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 hodnota může být HOST_ENDED_MEETING nebo ALL_PARTICIPANTS_LEFT.
meeting.participantJoined
{
"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"
}
}Použijte customParticipantId pro identifikátor vlastního účastníka. clientSpecificId je zahrnuta kvůli kompatibilitě se staršími integracemi.
meeting.participantLeft
{
"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"
}
}meeting.chatSynced
{
"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"
}
}recording.statusUpdate
RealtimeKit odesílá recording.statusUpdate když nahrávání prochází svým životním cyklem. Mezi stavy nahrávání patří RECORDING, UPLOADING, UPLOADED, a ERRORED. Další informace najdete v Sledování stavu nahrávání.
{
"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"
}
}
}livestreaming.statusUpdate
Stavy Livestreamu zahrnují LIVE, OFFLINE, a 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"
}
}
}meeting.transcript
{
"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"
}meeting.summary
{
"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"
}