Documentation
Sign inGet started

Realtime webhooks

Publishing pushes events out to clients. Webhooks run the other direction: the Realtime edge POSTs a signed event to your own endpoint when something happens on a channel, so your backend learns about activity it never took part in.
That closes a gap the API cannot. A client subscribing, a member closing their last tab, one browser sending another a cursor position: none of it passes through your server. Webhooks are how you find out.

The five event groups

You subscribe to groups, and a group covers a small family of delivered events. Each one answers a different question:
GroupThe question it answersWhat it delivers
realtime.channel_existenceIs anyone listening?realtime.channel_occupied, realtime.channel_vacated
realtime.presenceWho is here?realtime.member_added, realtime.member_removed
realtime.connection_countHow many connections?realtime.connection_count
realtime.cache_channelsDoes this channel need data?realtime.cache_miss
realtime.client_eventsWhat are clients sending?one event per client event, named after it
channel_existence reports the two edges of a channel's life and nothing in between: channel_occupied when a channel goes from zero connections to one, channel_vacated when its last connection leaves. Subscribers arriving and leaving in the middle produce nothing, which makes this the cheap way to know whether it is worth publishing at all.
connection_count needs the app's connection-counting setting turned on. Without it the edge never computes the count, so the group is subscribed and silent.

Subscribe an endpoint

Realtime groups are configured in the dashboard, on the endpoint itself. Open Developers → Webhooks, create or edit an endpoint, and use the Realtime events block: pick one Realtime app, then tick the groups you want.
The app choice is what makes a Realtime webhook different from a platform one. Platform events (email.delivered and friends) are workspace-wide, while realtime.* events belong to a single app, so the endpoint is scoped to that app when you create it and cannot be moved to another one afterwards. You can change which groups it receives at any time.
The two kinds can share an endpoint. Ticking email.bounced and realtime.presence on the same endpoint is allowed, and both arrive at the same URL with the same signing secret.
Realtime groups are not part of the public webhooks API yet, so POST /v1/webhooks with a realtime.* type in events is rejected as an unknown event type. Use the dashboard until that changes.

What a delivery looks like

The body is Bird's standard webhook envelope, three fields, one event per POST:
Contoh kode
{
  "data": { "channel": "presence-room-1", "member_id": "u_42" },
  "timestamp": "2026-07-31T09:00:00Z",
  "type": "realtime.member_added"
}
type names the specific thing that happened. The group you subscribed to never appears in a delivery, so subscribing to realtime.presence gets you realtime.member_added and realtime.member_removed, and your handler switches on those:
Delivered typedata fields
realtime.channel_occupiedchannel
realtime.channel_vacatedchannel
realtime.member_addedchannel, member_id
realtime.member_removedchannel, member_id
realtime.connection_countchannel, connection_count
realtime.cache_misschannel
realtime.<client event>channel_name, event, data, connection_id, plus member_id on a presence channel
Client events are the one group whose type you choose. A client that triggers client-typing produces a delivery of type realtime.client-typing, and event inside data repeats the name. Bind on the prefix rather than an exact match if your clients send several names. See Client events.

Verifying a delivery

Realtime webhooks are signed exactly like every other Bird webhook: Standard Webhooks, with webhook-id, webhook-timestamp, and webhook-signature headers and your endpoint's own signing secret. One handler covers your whole workspace.
Contoh kode
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 against the raw request body. Parsing the JSON and re-serializing it changes the bytes, and the signature is over bytes. An event type your code has never seen still verifies and decodes, so a default branch is enough to keep an older handler working when new types ship. The full contract, including the by-hand recipe, is in Webhooks & events.

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 happensWebhook
First tab subscribesrealtime.member_added
Second tab subscribesnone
Second tab closesnone
Last tab closesrealtime.member_removed
So member_removed is a reasonable trigger for "this person left the room" and a bad one for "this session ended". If you need connections, that is realtime.connection_count, which counts three for the member above. Presence channels covers the same distinction from the client's side.

Delivery differences worth knowing

Realtime deliveries take a different path than platform events, and the operational details do not all match Webhooks & events:
  • Your endpoint has one second to respond. That is the single most important number here: a delivery is counted as failed if the request has not completed in that time, so answer 2xx immediately and do the actual work after responding, never before.
  • A failed delivery is retried four times, waiting 10 seconds, then 30 seconds, then 2 minutes, then 5 minutes. After the last attempt the event is dropped.
  • There is no replay, and realtime.* deliveries 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 not ordered, and there is no per-event receipt, so treat each one as a hint that state changed rather than as the state itself. A channel_vacated arriving after somebody has already re-subscribed is normal, and Querying channel state is how you check what is true now.

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.