Publishing events
Publish from your server using your Bird API key and the Realtime app's key and secret. Never ship the app secret in client code. To let subscribed clients exchange short-lived signals, use client events on private or presence channels.
A minimal publish
An event requires a name and at least one channel. Its optional payload can contain any JSON object, array, or scalar.
import { BirdClient } from "@messagebird/sdk";
const bird = new BirdClient({
apiKey: process.env.BIRD_API_KEY,
realtime: {
key: process.env.BIRD_REALTIME_KEY,
secret: process.env.BIRD_REALTIME_SECRET,
},
});
await bird.realtime.publish("rap_01krdgeqcxet5s7t44vh8rt9mg", {
event: "order-updated",
channels: ["orders"],
data: { id: 42, status: "shipped" },
});import os
from bird import Bird
client = Bird(
api_key=os.environ["BIRD_API_KEY"],
realtime_key=os.environ["BIRD_REALTIME_KEY"],
realtime_secret=os.environ["BIRD_REALTIME_SECRET"],
)
client.realtime.publish(
"rap_01krdgeqcxet5s7t44vh8rt9mg",
event="order-updated",
channels=["orders"],
data={"id": 42, "status": "shipped"},
)client, err := bird.NewClient(
option.WithAPIKey(os.Getenv("BIRD_API_KEY")),
option.WithRealtimeCredentials(os.Getenv("BIRD_REALTIME_KEY"), os.Getenv("BIRD_REALTIME_SECRET")),
)
if err != nil {
log.Fatal(err)
}
_, err = client.Realtime.Publish(context.Background(), "rap_01krdgeqcxet5s7t44vh8rt9mg", bird.RealtimePublishParams{
Event: "order-updated",
Channels: []string{"orders"},
Data: map[string]any{"id": 42, "status": "shipped"},
})use MessageBird\Bird;
use MessageBird\RealtimeOptions;
use MessageBird\Wire\Model\RealtimePublish;
$bird = new Bird(
getenv('BIRD_API_KEY') ?: '',
realtime: new RealtimeOptions(
key: getenv('BIRD_REALTIME_KEY') ?: '',
secret: getenv('BIRD_REALTIME_SECRET') ?: '',
),
);
$bird->realtime->publish('rap_01krdgeqcxet5s7t44vh8rt9mg', (new RealtimePublish())
->setEvent('order-updated')
->setChannels(['orders'])
->setData(['id' => 42, 'status' => 'shipped']));curl -X POST https://us1.platform.bird.com/v1/realtime/apps/rap_01krdgeqcxet5s7t44vh8rt9mg/events \
-H "Authorization: Bearer $BIRD_API_KEY" \
-H "X-Realtime-Key: $BIRD_REALTIME_KEY" \
-H "X-Realtime-Secret: $BIRD_REALTIME_SECRET" \
-H "Content-Type: application/json" \
-d '{
"event": "order-updated",
"channels": ["orders"],
"data": { "id": 42, "status": "shipped" }
}'Clients bound to order-updated on orders receive the event. The API rejects server-published names that start with the protocol prefixes bird: or bird_internal:. Client-originated event names must start with client-.
The full request and response, including every field, is in the publish an event reference.
What a 200 means
The publish completes after the Realtime edge accepts the event. Delivery is asynchronous and has no per-client receipt. A client that disconnects during delivery can miss the event, and Realtime does not replay it after reconnecting.
Store durable state in your database. Use events to announce changes, then have clients reload current state after reconnecting.
Broadcasting to several channels
One call can send the same event to up to 100 channels. A private-encrypted- channel must be the only channel in its publish because each encrypted channel uses a different key. The API rejects an encrypted fan-out with E23000. See Encrypted channels.
await bird.realtime.publish(appId, {
event: "price-changed",
channels: ["ticker-btc", "ticker-eth", "ticker-sol"],
data: { at: "2026-07-31T09:00:00Z" },
});client.realtime.publish(
app_id,
event="price-changed",
channels=["ticker-btc", "ticker-eth", "ticker-sol"],
data={"at": "2026-07-31T09:00:00Z"},
)_, err := client.Realtime.Publish(context.Background(), appID, bird.RealtimePublishParams{
Event: "price-changed",
Channels: []string{"ticker-btc", "ticker-eth", "ticker-sol"},
Data: map[string]any{"at": "2026-07-31T09:00:00Z"},
})$bird->realtime->publish($appId, (new RealtimePublish())
->setEvent('price-changed')
->setChannels(['ticker-btc', 'ticker-eth', 'ticker-sol'])
->setData(['at' => '2026-07-31T09:00:00Z']));curl -X POST "https://us1.platform.bird.com/v1/realtime/apps/$APP_ID/events" \
-H "Authorization: Bearer $BIRD_API_KEY" \
-H "X-Realtime-Key: $BIRD_REALTIME_KEY" \
-H "X-Realtime-Secret: $BIRD_REALTIME_SECRET" \
-H "Content-Type: application/json" \
-d '{
"event": "price-changed",
"channels": ["ticker-btc", "ticker-eth", "ticker-sol"],
"data": { "at": "2026-07-31T09:00:00Z" }
}'Each target channel counts as a separate message for usage. This example counts as three messages. A publish to 10,000 per-user channels therefore counts as 10,000 messages.
Batching unrelated events
A broadcast sends one event to many channels. A batch sends up to 10 different events, each to one channel, in one request.
await bird.realtime.publishBatch(appId, {
events: [
{ event: "order-updated", channels: ["orders-42"], data: { status: "shipped" } },
{ event: "stock-changed", channels: ["inventory-99"], data: { left: 3 } },
],
});client.realtime.publish_batch(
app_id,
events=[
{"event": "order-updated", "channels": ["orders-42"], "data": {"status": "shipped"}},
{"event": "stock-changed", "channels": ["inventory-99"], "data": {"left": 3}},
],
)_, err := client.Realtime.PublishBatch(context.Background(), appID, bird.RealtimePublishBatchParams{
Events: []bird.RealtimeBatchEventParams{
{Event: "order-updated", Channel: "orders-42", Data: map[string]any{"status": "shipped"}},
{Event: "stock-changed", Channel: "inventory-99", Data: map[string]any{"left": 3}},
},
})$bird->realtime->publishBatch($appId, (new RealtimeBatchPublish())
->setEvents([
(new RealtimeBatchEvent())->setEvent('order-updated')->setChannel('orders-42')->setData(['status' => 'shipped']),
(new RealtimeBatchEvent())->setEvent('stock-changed')->setChannel('inventory-99')->setData(['left' => 3]),
]));curl -X POST "https://us1.platform.bird.com/v1/realtime/apps/$APP_ID/batch-events" \
-H "Authorization: Bearer $BIRD_API_KEY" \
-H "X-Realtime-Key: $BIRD_REALTIME_KEY" \
-H "X-Realtime-Secret: $BIRD_REALTIME_SECRET" \
-H "Content-Type: application/json" \
-d '{
"events": [
{ "event": "order-updated", "channel": "orders-42", "data": { "status": "shipped" } },
{ "event": "stock-changed", "channel": "inventory-99", "data": { "left": 3 } }
]
}'Use a batch to combine unrelated updates into one request. Each event still counts separately toward usage, and a batch accepts at most 10 events. See Publish a batch.
Excluding the client that acted
If a client already applied its action locally, pass its connection ID to prevent the resulting publish from applying the same change again. The edge skips only that connection.
await bird.realtime.publish(appId, {
event: "message.created",
channels: ["presence-room-1"],
data: { body: "hello" },
exclude_connection_id: "26896.319537",
});client.realtime.publish(
app_id,
event="message.created",
channels=["presence-room-1"],
data={"body": "hello"},
exclude_connection_id="26896.319537",
)_, err := client.Realtime.Publish(context.Background(), appID, bird.RealtimePublishParams{
Event: "message.created",
Channels: []string{"presence-room-1"},
Data: map[string]any{"body": "hello"},
ExcludeConnectionID: "26896.319537",
})$bird->realtime->publish($appId, (new RealtimePublish())
->setEvent('message.created')
->setChannels(['presence-room-1'])
->setData(['body' => 'hello'])
->setExcludeConnectionId('26896.319537'));curl -X POST "https://us1.platform.bird.com/v1/realtime/apps/$APP_ID/events" \
-H "Authorization: Bearer $BIRD_API_KEY" \
-H "X-Realtime-Key: $BIRD_REALTIME_KEY" \
-H "X-Realtime-Secret: $BIRD_REALTIME_SECRET" \
-H "Content-Type: application/json" \
-d '{
"event": "message.created",
"channels": ["presence-room-1"],
"data": { "body": "hello" },
"exclude_connection_id": "26896.319537"
}'Read the ID from the client's current connection and include it in the request that triggers the change. Other tabs use separate connections and still receive the event.
Reading channel state as you publish
Use include to return each target channel's state at publish time and avoid a separate channel-state request:
const result = await bird.realtime.publish(appId, {
event: "order-updated",
channels: ["presence-lobby"],
data: { id: 42 },
include: ["member_count", "connection_count"],
});result = client.realtime.publish(
app_id,
event="order-updated",
channels=["presence-lobby"],
data={"id": 42},
include=["member_count", "connection_count"],
)result, err := client.Realtime.Publish(context.Background(), appID, bird.RealtimePublishParams{
Event: "order-updated",
Channels: []string{"presence-lobby"},
Data: map[string]any{"id": 42},
Include: []bird.RealtimeChannelInclude{bird.RealtimeIncludeMemberCount, bird.RealtimeIncludeConnectionCount},
})$result = $bird->realtime->publish($appId, (new RealtimePublish())
->setEvent('order-updated')
->setChannels(['presence-lobby'])
->setData(['id' => 42])
->setInclude(['member_count', 'connection_count']));curl -X POST "https://us1.platform.bird.com/v1/realtime/apps/$APP_ID/events" \
-H "Authorization: Bearer $BIRD_API_KEY" \
-H "X-Realtime-Key: $BIRD_REALTIME_KEY" \
-H "X-Realtime-Secret: $BIRD_REALTIME_SECRET" \
-H "Content-Type: application/json" \
-d '{
"event": "order-updated",
"channels": ["presence-lobby"],
"data": { "id": 42 },
"include": ["member_count", "connection_count"]
}'member_count works only on presence channels. connection_count requires connection counting on the app. Requesting these attributes counts as one extra message toward usage.
Caps
| Limit | Value |
|---|---|
| Channels per publish | 100 |
| Events per batch | 10 |
| Event payload | 10 KB serialized |
| Channel name | 164 characters, letters, digits, and _ - = @ , . ; |
| Event name | 200 characters |
Exceeding any cap returns a validation error. The API does not truncate the request.
Retrying safely
Retry a publish with the same Idempotency-Key to avoid duplicate delivery. The TypeScript and Go SDKs generate a key and reuse it for automatic retries. If your application retries a request, supply and reuse its own key. See Idempotency.
Next steps
- Authorizing channels is what a private- or presence- channel needs before a client can subscribe.
- Realtime overview explains channels, members, and connections, and where usage shows up.
- Excluding event recipients keeps the acting client from receiving its own change.