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. 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? | one event per client event, named after it |
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, 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:
Codebeispiel
{
"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_name, event, data, connection_id, plus member_id on a presence channel |
For client events, the client chooses the event suffix. Triggering client-typing produces realtime.client-typing, and data.event contains client-typing. See Client events.
Verifying a delivery
Realtime webhooks follow Standard Webhooks and include webhook-id, webhook-timestamp, and webhook-signature headers. Verify them with the endpoint's signing secret.
Codebeispiel
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 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.
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 to retrieve current state after delayed or missing events.
Next steps
- Client events is the group whose event names and payloads you define yourself.
- Cache channels explains what to do with realtime.cache_miss.
- Webhooks & events covers endpoint setup, signature verification, and secret rotation for every Bird webhook.