---
title: "Realtime webhooks"
description: "Have the Realtime edge tell your server what is happening on your channels: occupancy, presence, connection counts, cache misses, and client events."
canonical: "https://bird.com/docs/guides/realtime/webhooks"
---

# Realtime webhooks

Publishing sends events out to clients. Webhooks run the other direction: the Realtime edge POSTs a signed event to your endpoint when something happens on a channel.

You can already read channel state on demand with [Querying channel state](/docs/guides/realtime/querying-channel-state). Webhooks are how you hear about a change as it happens, without polling: a client subscribing, a member closing their last tab, one client sending another a cursor position.

## The five event groups

Subscribe to one or more event groups:

| Group                        | The question it answers      | What it delivers                                        |
| ---------------------------- | ---------------------------- | ------------------------------------------------------- |
| `realtime.channel_existence` | Is anyone listening?         | `realtime.channel_occupied`, `realtime.channel_vacated` |
| `realtime.presence`          | Who is here?                 | `realtime.member_added`, `realtime.member_removed`      |
| `realtime.connection_count`  | How many connections?        | `realtime.connection_count`                             |
| `realtime.cache_channels`    | Does this channel need data? | `realtime.cache_miss`                                   |
| `realtime.client_events`     | What are clients sending?    | `realtime.client_event`                                 |

`channel_existence` reports only the two edges of a channel's life: `channel_occupied` when it goes from zero connections to one, `channel_vacated` when its last connection leaves. Subscribers arriving and leaving in between produce nothing, which makes it the cheap way to know whether publishing is worth it.

`connection_count` requires connection counting on the app. If the setting is disabled, the subscribed group produces no events.

## Subscribe an endpoint

Open [Webhooks](https://bird.com/dashboard/w/webhooks), create or edit an endpoint, and find the **Realtime events** section. Select one Realtime app and the groups to receive.

Platform event subscriptions apply across the workspace, while `realtime.*` subscriptions belong to one app. You cannot change the endpoint's Realtime app after creating it, but you can update its subscribed groups.

Platform and Realtime events can share an endpoint. For example, one endpoint can subscribe to `email.bounced` and `realtime.presence`. Both use the endpoint's signing secret.

Realtime groups are currently dashboard-only. A public `POST /v1/webhooks` request that includes a `realtime.*` event type is rejected, so configure these subscriptions in the dashboard.

## What a delivery looks like

Each `POST` contains one event in Bird's standard webhook envelope:

```json
{
  "data": { "channel": "presence-room-1", "member_id": "u_42" },
  "timestamp": "2026-07-31T09:00:00Z",
  "type": "realtime.member_added"
}
```

`type` identifies the delivered event. For example, the `realtime.presence` group delivers `realtime.member_added` and `realtime.member_removed`:

| Delivered `type`            | `data` fields                                                                       |
| --------------------------- | ----------------------------------------------------------------------------------- |
| `realtime.channel_occupied` | `channel`                                                                           |
| `realtime.channel_vacated`  | `channel`                                                                           |
| `realtime.member_added`     | `channel`, `member_id`                                                              |
| `realtime.member_removed`   | `channel`, `member_id`                                                              |
| `realtime.connection_count` | `channel`, `connection_count`                                                       |
| `realtime.cache_miss`       | `channel`                                                                           |
| `realtime.client_event`     | `channel`, `event`, `data`, `connection_id`, plus `member_id` on a presence channel |

For client events, `type` is `realtime.client_event`, and `data.event` contains the original client event name, such as `client-typing`. `data.channel` identifies the channel, and `data.data` is a JSON string containing the client payload. See [Client events](/docs/guides/realtime/client-events).

## Verifying a delivery

Realtime webhooks follow [Standard Webhooks](https://www.standardwebhooks.com/) and include `webhook-id`, `webhook-timestamp`, and `webhook-signature` headers. Verify them with the endpoint's signing secret.

```typescript
import { BirdClient } from "@messagebird/sdk";

const bird = new BirdClient({
  apiKey: process.env.BIRD_API_KEY,
  webhooks: { secret: process.env.BIRD_WEBHOOK_SECRET },
});

app.post("/webhooks/bird", express.raw({ type: "*/*" }), (req, res) => {
  const event = bird.webhooks.unwrap(req.body, req.headers);
  res.sendStatus(200);

  switch (event.type) {
    case "realtime.channel_vacated":
      stopExpensiveWorkFor(event.data.channel);
      break;
    case "realtime.member_removed":
      markAway(event.data.member_id);
      break;
  }
});
```

Verify the raw request body because parsing and re-serializing JSON can change the signed bytes. Handle unknown event types in a `default` branch so new types do not break the endpoint. See [Verify webhook signatures](/docs/guides/webhooks#verify-signatures) for the complete contract.

## Presence events count members

`member_added` and `member_removed` follow the identity, so they do not line up one-to-one with connections. Somebody with your app open in three tabs is one member:

| What happens          | Webhook                   |
| --------------------- | ------------------------- |
| First tab subscribes  | `realtime.member_added`   |
| Second tab subscribes | none                      |
| Second tab closes     | none                      |
| Last tab closes       | `realtime.member_removed` |

Use `member_removed` to detect when an identity leaves a channel. A single session ending does not trigger it while another session remains. To track connections, subscribe to `realtime.connection_count`. See [Presence channels](/docs/guides/realtime/presence-channels).

## Realtime webhook delivery behavior

Realtime deliveries use these retry and visibility rules:

- Return `2xx` promptly after durably accepting the event, then process it asynchronously.
- If your endpoint returns a non-2xx response, Realtime retries with exponential backoff for up to 5 minutes.
- Realtime events have no replay and do not appear in the endpoint's delivery-attempts log.
- Pausing the endpoint stops Realtime deliveries along with everything else, and re-enabling it resumes them.

Deliveries are unordered and provide no per-event receipt. Treat them as change notifications. Use [Querying channel state](/docs/guides/realtime/querying-channel-state) to retrieve current state after delayed or missing events.

## Next steps

- [Client events](/docs/guides/realtime/client-events) explains the client event names and payloads carried by `realtime.client_event`.
- [Cache channels](/docs/guides/realtime/cache-channels) explains what to do with `realtime.cache_miss`.
- [Webhooks & events](/docs/guides/webhooks) covers endpoint setup, signature verification, and secret rotation for every Bird webhook.

## Related resources

- [Realtime](/realtime-api) (product)
- [Build your first integration](/learn/paths/integration) (course)
- [Send your first realtime event](/docs/get-started/send-your-first-realtime-event) (docs)

[Get an implementation brief](/learn/workspace?topic=realtime)
