WhatsApp events
Every WhatsApp message emits events as it moves through its lifecycle. Each event is a stamped fact about the message: when Bird accepted it, when it reached WhatsApp, when it was delivered, when the recipient read it, or that it failed. Events are how you follow what happened to a message after the send returns 202.
The event envelope
Events reach you in two shapes that carry the same lifecycle facts. A webhook delivery is the standard envelope described in the Webhooks guide: a type, a timestamp, and a type-specific data object.
Code example
{
"data": {
"direction": "outbound",
"from": { "phone_number": "+13124495569" },
"metadata": { "session_id": "sess_4821" },
"tags": [{ "name": "flow", "value": "login-otp" }],
"to": { "phone_number": "+14155550100" },
"whatsapp_id": "wam_01ky7qbvswf3fvyaw3az90391c",
"workspace_id": "ws_01ky7m235keycbnwyajabe1a6b"
},
"timestamp": "2026-07-23T14:51:39.913Z",
"type": "whatsapp.delivered"
}Every whatsapp.* payload's data carries the identity base: whatsapp_id, workspace_id, the message direction (outbound for messages you send), the from and to addresses (each a phone_number in E.164 and/or a Meta business-scoped user ID bsuid), and the tags and metadata from the send request (both null when the message carried none).
The events API returns the same lifecycle as leaner records: each event is an id, a type, and the occurred_at timestamp, since the message identity is already fixed by the request URL.
Lifecycle events
A message emits events in order as it progresses. Not every message emits every type: a send can stop at whatsapp.failed or whatsapp.rejected, and whatsapp.read only appears if the recipient opens the message.
| Event | Meaning |
|---|---|
| whatsapp.accepted | Bird accepted the send request. This is what the 202 reported. |
| whatsapp.sent | Handed to the WhatsApp network. |
| whatsapp.delivered | Delivery confirmed to the recipient's device. |
| whatsapp.read | The recipient opened the message. |
| whatsapp.failed | WhatsApp would not deliver the message. |
| whatsapp.rejected | Bird refused the message before sending it. You were not charged. |
One thing about whatsapp.read: it does not change the message status. A delivered message stays delivered, and the read is recorded as a read_at timestamp on the message alongside the event.
The event type list is open: new types may be added over time, so treat an unrecognized value as a future event rather than an error.
Failure events
whatsapp.failed and whatsapp.rejected are both terminal: the message will not be delivered, and no further events follow. They differ in whether Bird ever took the message on. A rejection is Bird declining it up front, so it was neither sent nor charged: the recipient is on your workspace's suppression list, the wallet could not cover the send, or the destination has no configured price. A failure means Bird took the message on and WhatsApp would not deliver it: nearly always WhatsApp refusing the message when Bird submits it, or reporting later that it could not be delivered. The meta_error_code carries WhatsApp's own code for the refusal when it gave one.
Both carry an error object with the detail: a Bird-stable code (for example undeliverable, insufficient_balance, or recipient_suppressed), a human-readable description, an optional raw meta_error_code from the WhatsApp Cloud API, and the occurred_at. It appears on the API record and in the webhook payload, and on no other event type.
Reading events from the API
GET /v1/whatsapp/messages/{message_id}/events returns a message's events in chronological order. The timeline is bounded and returned in full, so the list is not paginated. Reading events needs an API key with the whatsapp scope:
const { data } = await bird.whatsapp.listEvents("wa_abc123");
for (const event of data) console.log(event.type, event.occurred_at);events = client.whatsapp.list_events("wa_abc123")
for event in events.data:
print(event.type, event.occurred_at)package main
import (
"context"
"fmt"
"log"
"os"
bird "github.com/messagebird/bird-sdk-go"
"github.com/messagebird/bird-sdk-go/option"
)
func main() {
client, err := bird.NewClient(option.WithAPIKey(os.Getenv("BIRD_API_KEY")))
if err != nil {
log.Fatal(err)
}
events, err := client.Whatsapp.ListEvents(context.Background(), "wam_01krdgeqcxet5s7t44vh8rt9mg", bird.WhatsappListEventsParams{})
if err != nil {
log.Fatal(err)
}
for _, e := range events.Data {
fmt.Println(e.Id, e.Type)
}
}$events = $bird->whatsapp->listEvents('wamid_01krdgeqcxet5s7t44vh8rt9mg');
foreach ($events->getData() ?? [] as $event) {
echo $event->getType(), ' ', $event->getId(), "\n";
}bird whatsapp list-events <message-id>curl https://us1.platform.bird.com/v1/whatsapp/messages/wam_.../events \
-H "Authorization: Bearer $BIRD_API_KEY"A message that was accepted, sent, delivered, and read returns four events:
Code example
{
"data": [
{
"id": "ev_01ky7q6a1fejfbvs0myn41hj41",
"occurred_at": "2026-07-23T14:48:34.71Z",
"type": "whatsapp.accepted"
},
{
"id": "ev_01ky7q6a2denvtd6jg1vqwmg13",
"occurred_at": "2026-07-23T14:48:35.671Z",
"type": "whatsapp.sent"
},
{
"id": "ev_01ky7q6a2zff9r2qm74mmg1g6z",
"occurred_at": "2026-07-23T14:48:36.642Z",
"type": "whatsapp.delivered"
},
{
"id": "ev_01ky7q6c21frssf0vj8h50qysw",
"occurred_at": "2026-07-23T14:48:38.65Z",
"type": "whatsapp.read"
}
]
}To keep only one type, pass the type query parameter (for example ?type=whatsapp.failed); omit it for the full timeline.
The same timeline is what the WhatsApp log page renders when you open a message.

Webhooks
The same lifecycle events are delivered to your endpoints as webhooks, so you can react to a message the moment it moves instead of polling the events API. Subscribe an endpoint to any whatsapp.* type from the Webhooks page in the dashboard or through the webhooks API; each delivery uses the event envelope with the identity base and, on whatsapp.failed and whatsapp.rejected, the error object. The Webhooks guide covers creating endpoints, verifying signatures, and retries.
Next steps
- WhatsApp log: the per-message view that renders this timeline
- Sending WhatsApp messages: where a message's lifecycle begins
- Webhooks guide: endpoints, signatures, retries, and the full event catalog