Realtime webhooks

Publishing goes out. Webhooks come back.

Clients subscribe and disconnect without ever reaching your backend, which means your backend has no idea anyone is listening. Webhooks close that loop: the edge posts a signed event when a channel fills or empties, when a member arrives or leaves, and when a cache channel has nothing to serve.

webhooks.ts
signed
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_occupied":
      startStreaming(event.data.channel);
      break;
    case "realtime.channel_vacated":
      stopStreaming(event.data.channel);
      break;
    case "realtime.cache_miss":
      backfill(event.data.channel);
      break;
    default:
      break;
  }
});

Stop paying for an audience of nobody.

This is the cheapest optimization in Bird Realtime. A channel-occupied event is the signal to start the expensive job, the market-data subscription, the per-second publish loop; channel-vacated is the signal to stop it. Nothing fires for the subscribers arriving and leaving in between, so the two events mark exactly the edges of a channel's life and nothing else.

Five groups. Subscribe to what you need.

You subscribe to a group; the group delivers the individual event types. One endpoint can carry Realtime events alongside the rest of the platform, so email.bounced and realtime.presence can land on the same route with the same signing secret.

POST /webhooks/bird
realtime.member_added
{
  "type": "realtime.member_added",
  "timestamp": "2026-07-31T09:00:00Z",
  "data": {
    "channel": "presence-room-1",
    "member_id": "u_42"
  }
}
  • realtime.channel_existenceChannel occupied and vacated: the two edges of a channel's life, and nothing in between.
  • realtime.presenceMember added and removed. Follows the identity, so a person's second tab produces nothing.
  • realtime.connection_countHow many connections a channel holds. Needs connection counting enabled on the app.
  • realtime.cache_channelsA cache miss, which is your cue to read current state and publish it.
  • realtime.client_eventsOne delivery per client event, named after it: client-typing arrives as realtime.client-typing.

What people build with them

Four patterns that need a server to know what the clients are doing.

  1. 01

    Work that runs only while watched.

    Start the upstream feed, the poller, or the render job on channel occupied and shut it down on channel vacated. A dashboard nobody has open costs nothing to keep live.

  2. 02

    A backend copy of the roster.

    Member added and removed keep your own view of who is in a room, which is what an agent-availability indicator or a seat count is really made of. They follow identities, so a person closing one of three tabs produces nothing.

  3. 03

    Filling a cold cache.

    A client subscribing to a cache channel with nothing cached triggers a miss on your endpoint. Read current state, publish it, and the client that caused the miss receives it, because it is already subscribed by then.

  4. 04

    Watching the peer-to-peer traffic.

    Client events travel between clients without your API. Subscribe to the client-events group and your server gets a copy, with the channel, the connection id, and on presence channels the member id. Worth knowing before you subscribe: a high-frequency signal like a cursor position produces one delivery per event.

Signed like every other Bird webhook.

Deliveries follow Standard Webhooks, with webhook-id, webhook-timestamp, and webhook-signature headers you verify against the endpoint's signing secret. The SDK unwraps and verifies in one call. Verify the raw body rather than a reparsed copy, since re-serializing JSON can change the signed bytes, and keep a default branch so a new event type cannot break the route.

The delivery guarantees, stated plainly.

Treat these as change notifications rather than a ledger. A non-2xx response is retried with exponential backoff for up to five minutes, after which the event is gone: Realtime events have no replay and do not appear in the endpoint's delivery-attempts log. Deliveries are unordered and carry no per-event receipt, so after a delayed or missing one, read current state from the channel-state API rather than reconstructing it. Return 2xx as soon as you have durably accepted the event and do the work afterwards.

Configured in the dashboard.

Realtime subscriptions are set up on the Webhooks page: create or edit an endpoint, pick one Realtime app, and select the groups. The app cannot be changed afterwards, though the groups can. This is the one part of the platform's webhook surface that is dashboard-only today, and a public API request that includes a realtime event type is rejected rather than silently accepted.

Go deeper in the docs.

Realtime webhooks lists every delivered type and its data fields. Client events covers the group you name yourself, cache channels explains what to do with a miss, and webhooks and events is the platform-wide contract for endpoints, signatures, and secret rotation.

Find out when somebody starts listening.

Point one endpoint at Realtime and the rest of the platform. Same envelope, same signing secret, same verification call.

Inizia con un canale.
Aggiungi gli altri quando sei pronto.

Una chiave API di test è subito tua. La produzione si sblocca quando aggiungi un metodo di pagamento e verifichi un mittente.

Usi Claude Code, Cursor o Codex? Copia un prompt di configurazione e il tuo agente installerà la CLI e le skill di Bird per te. Scegli il tuo:

Cursor