---
title: Webhooks
description: Receive RealtimeKit events in your application through signed HTTP callbacks.
image: https://developers.cloudflare.com/dev-products-preview.png
---

> Documentation Index  
> Fetch the complete documentation index at: https://developers.cloudflare.com/realtime/llms.txt  
> Use this file to discover all available pages before exploring further. 

[Skip to content](#%5Ftop) 

# Webhooks

Webhooks let your backend receive RealtimeKit events as they happen. RealtimeKit sends an HTTP `POST` request to your configured endpoint with a JSON payload when a subscribed event occurs, such as when a meeting starts, a participant joins, or a recording is uploaded.

Use webhooks for backend workflows that depend on asynchronous events, such as starting post-meeting processing, downloading transcripts, tracking recording status, or updating your own session records.

## How webhooks work

1. Create an HTTP endpoint in your backend that can receive `POST` requests.
2. Register the endpoint URL with the RealtimeKit [Webhooks API](https://developers.cloudflare.com/api/resources/realtime%5Fkit/subresources/webhooks/).
3. Choose the event types that should trigger the webhook.
4. Verify incoming requests with the `rtk-signature` header.
5. Return a `2xx` response after accepting the event.

Webhook events are subscription-only. Your endpoint receives only the events included in the webhook's `events` array.

## Create a webhook endpoint

Your webhook endpoint must accept JSON `POST` requests. The endpoint can handle multiple event types by switching on the `event` field in the request body.

* [  JavaScript ](#tab-panel-10121)
* [  TypeScript ](#tab-panel-10122)

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;
```

Your endpoint should return a `2xx` response as soon as it accepts the event. Move slow work, such as downloading files or calling third-party APIs, to a background job.

## Register a webhook

Register the publicly accessible endpoint URL using the RealtimeKit [Webhooks API](https://developers.cloudflare.com/api/resources/realtime%5Fkit/subresources/webhooks/):

Terminal window

```
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  }'
```

You can also manage webhooks from the [RealtimeKit dashboard ↗](https://dash.cloudflare.com/?to=/:account/realtime/kit).

## Webhook headers

RealtimeKit includes headers that help you identify, deduplicate, and verify webhook deliveries:

| Header         | Description                                                                                                                 |
| -------------- | --------------------------------------------------------------------------------------------------------------------------- |
| rtk-signature  | Base64-encoded RSA-SHA256 signature for the request body. Use this header to verify that the request came from RealtimeKit. |
| rtk-uuid       | Unique ID for the webhook delivery. Store this value if you need to avoid processing duplicate deliveries.                  |
| rtk-webhook-id | ID of the webhook configuration that triggered the delivery.                                                                |

## Verify webhook signatures

RealtimeKit signs each webhook request body with RSA-SHA256\. Verify the signature before processing the event.

### Fetch the public key

Fetch the RealtimeKit webhook public key from:

Terminal window

```
curl "https://api.realtime.cloudflare.com/.well-known/webhooks.json"
```

The response includes a PEM-encoded public key:

```
{  "success": true,  "data": {    "publicKey": "-----BEGIN PUBLIC KEY-----\n...\n-----END PUBLIC KEY-----"  },  "message": ""}
```

### Verify the request body

Verify `rtk-signature` against the raw request body. Do not reserialize parsed JSON before verification because changes in whitespace or key order can change the signed bytes.

* [  JavaScript ](#tab-panel-10123)
* [  TypeScript ](#tab-panel-10124)

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>;
```

## Retry behavior

RealtimeKit treats any `2xx` response as a successful delivery.

If your endpoint returns a `5xx` response or the request fails because of a network error, RealtimeKit retries the delivery. If your endpoint returns a non-`2xx` response below `500`, RealtimeKit records the delivery as failed and does not retry it.

After repeated delivery failures, RealtimeKit may temporarily reduce delivery attempts to that webhook URL. Return a `2xx` response only after your application has accepted the event.

## Supported events

RealtimeKit supports these webhook events:

| Event                      | Trigger                                                              |
| -------------------------- | -------------------------------------------------------------------- |
| meeting.started            | The first participant joins a meeting.                               |
| meeting.ended              | The meeting ends because the host ended it or all participants left. |
| meeting.participantJoined  | A participant joins a meeting.                                       |
| meeting.participantLeft    | A participant leaves a meeting.                                      |
| meeting.chatSynced         | The chat export for a completed meeting is available.                |
| recording.statusUpdate     | A recording changes status.                                          |
| livestreaming.statusUpdate | A livestream changes status.                                         |
| meeting.transcript         | The transcript for a completed meeting is available.                 |
| meeting.summary            | The AI-generated summary for a completed meeting is available.       |

Fetch the current event list with the Webhooks API:

Terminal window

```
curl "https://api.cloudflare.com/client/v4/accounts/$ACCOUNT_ID/realtime/kit/$APP_ID/webhooks/all" \  --header "Authorization: Bearer $CLOUDFLARE_API_TOKEN"
```

## Event payloads

All webhook payloads include an `event` field. The remaining fields depend on the event type.

### `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"}
```

The `reason` value can be `HOST_ENDED_MEETING` or `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"  }}
```

Use `customParticipantId` for your own participant identifier. `clientSpecificId` is included for compatibility with older integrations.

### `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 sends `recording.statusUpdate` when a recording moves through its lifecycle. Recording statuses include `RECORDING`, `UPLOADING`, `UPLOADED`, and `ERRORED`. For more information, refer to [Monitor Recording Status](https://developers.cloudflare.com/realtime/realtimekit/recording-guide/monitor-status/).

```
{  "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`

Livestream statuses include `LIVE`, `OFFLINE`, and `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"}
```

```json
{"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/realtime/realtimekit/webhooks/#page","headline":"Webhooks · Cloudflare Realtime docs","description":"Receive RealtimeKit events in your application through signed HTTP callbacks.","url":"https://developers.cloudflare.com/realtime/realtimekit/webhooks/","inLanguage":"en","image":"https://developers.cloudflare.com/dev-products-preview.png","dateModified":"2026-06-12","publisher":{"@type":"Organization","name":"Cloudflare","url":"https://www.cloudflare.com/"},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}}
{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"item":{"@id":"/directory/","name":"Directory"}},{"@type":"ListItem","position":2,"item":{"@id":"/realtime/","name":"Realtime"}},{"@type":"ListItem","position":3,"item":{"@id":"/realtime/realtimekit/","name":"RealtimeKit"}},{"@type":"ListItem","position":4,"item":{"@id":"/realtime/realtimekit/webhooks/","name":"Webhooks"}}]}
```
