INTEGRITY Dokumentace

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

  1. Vytvořte v backendu HTTP koncový bod, který dokáže přijímat POST požadavky.
  2. Zaregistrujte URL adresu endpointu u RealtimeKit Webhooks API.
  3. Vyberte typy událostí, které mají webhook spouštět.
  4. Ověřte příchozí požadavky pomocí rtk-signature hlavička.
  5. Vrátí 2xx odpověď 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.

src/index.js
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 });
	},
};
src/index.ts
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.

src/index.js
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 });
	},
};
src/index.ts
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"
}