SMS events
Every message moves through a lifecycle, and Bird emits an event at each step. This page is the full event vocabulary; how events are delivered to your endpoint (signatures, retries, replay) is covered in the Webhooks guide.
The delivery lifecycle, as a path through the event types:
- sms.accepted: Bird has the message and is preparing to hand it to a carrier.
- sms.sent: Bird handed the message to the carrier and is awaiting a delivery receipt.
- One terminal event:
- sms.delivered: The carrier confirmed delivery to the handset.
- sms.undelivered: The carrier reported a temporary non-delivery, such as an unavailable handset.
- sms.failed: A permanent failure stopped delivery.
- sms.expired: The carrier stopped trying and reported the message as expired.
The terminal events surface the carrier's delivery receipt, what SMS platforms call a delivery report or DLR.
The exception is sms.rejected: the message was refused (by a policy check, a charge that could not complete, or a carrier that turned it away) rather than attempted and lost. A message rejected during processing carries sms.rejected as its only event.
Bird also receives replies. When a subscriber texts one of your numbers, Bird stores the message and emits sms.received, so you can act without polling. The payload carries the body, segment breakdown, both numbers, and the operator when the carrier reports one.
Bird evaluates the reply against the keyword rules for that number. A supported stop keyword such as STOP records a sender-and-subscriber suppression and still emits sms.received.
The event type is an open enum: Bird may add new event types over time, so treat an unrecognized type as a future event rather than an error. Match the types you handle and ignore the rest.
The event envelope
Events arrive at your webhook endpoint in the Standard Webhooks nested envelope described in the Webhooks guide: three fields, type, timestamp, and a type-specific data object. The event's identity is not in the body: it rides in the webhook-id HTTP header, which is stable across retries of the same delivery and is your deduplication key.
| Field | Description |
|---|---|
| type | One of the event types on this page, such as sms.delivered |
| timestamp | When the event occurred (RFC 3339); sort by this, never by arrival order, since deliveries are not ordered |
| data | Event-specific payload |
Every SMS event's data contains sms_id, workspace_id, and the to and from addresses. It also echoes the send's tags and metadata so you can route and correlate events without another lookup. Each is null when the send carried none.
The same object carries cost, the message's charge as of that event, split into transaction_amount and passthrough_amount with their sum in amount. It is null on an event that priced nothing. Because deliveries are not ordered, merge cost one component at a time instead of replacing the whole object: for each component, keep the value from the event with the latest timestamp. An amount sums only the components in its own payload, so read it as the charge so far rather than a settled total. Cost and billing covers what each component means.
Code example
{
"type": "sms.delivered",
"timestamp": "2026-06-10T14:30:00Z",
"data": {
"sms_id": "sms_01krdgeqcxet5s7t44vh8rt9mg",
"workspace_id": "ws_01krdgeqcxet5s7t44vh8rt9mg",
"to": "+15551234567",
"from": "+12025550188",
"carrier": "Example Wireless",
"mcc_mnc": "310260",
"cost": {
"amount": "0.00990",
"currency_code": "USD",
"transaction_amount": "0.00790",
"passthrough_amount": "0.00200"
},
"tags": [{ "name": "campaign", "value": "spring-2026" }],
"metadata": { "order_id": "ord_123" }
}
}Lifecycle events
sms.accepted
Fires when Bird accepts the send and begins preparing to hand it to a carrier. Payload adds segments, the breakdown Bird counted at accept time; its count is what the send is billed on.
sms.sent
Fires when Bird has handed the message to the carrier and is awaiting a delivery receipt. Payload adds carrier and mcc_mnc (the handling network and its mobile country/network code). Each is absent rather than null when the carrier does not report it. To measure processing latency, compare this event's timestamp with sms.accepted's.
sms.delivered
The carrier confirmed the message reached the handset. Payload adds carrier and mcc_mnc, each absent when the receipt didn't identify them.
Failure events
Each failure event's payload adds an error object: a Bird-stable code (for example unreachable or blocked_by_carrier), a human-readable description, the raw carrier_error_code when one was supplied, and occurred_at.
sms.undelivered
A non-permanent non-delivery: the handset was off or unreachable.
sms.failed
A permanent delivery failure stopped the message.
sms.rejected
The message was refused by Bird's checks during processing, a charge that could not complete, or a carrier that turned it away. A rejection stops the message before a delivery attempt succeeds. An exhausted wallet ends here with the error code insufficient_balance, and a message whose charge could not complete is not billed.
sms.expired
The carrier stopped trying to deliver and reported the message as expired. Expiry comes from the carrier's delivery receipt: Bird sets no validity window of its own and runs no timer that ends a message. The error describes why the message was still undelivered when the carrier gave up, usually unreachable: the handset stayed off or out of coverage throughout.
Suppression events
Beyond the per-message lifecycle, one event reports a change to the workspace's suppression list: sms_suppression.created fires when a suppression opens, whether a subscriber texted a stop keyword, the carrier reported an opt-out, or someone added one manually. The payload carries the suppression_id, the subscriber's number as destination, the originator the block binds (an SMS suppression is the exact sender-and-subscriber pair), the reason, and the workspace_id, so your own system can mirror the list without polling:
Code example
{
"type": "sms_suppression.created",
"timestamp": "2026-08-25T14:52:03.192524705Z",
"data": {
"suppression_id": "ssu_01krdgeqcxet5s7t44vh8rt9mg",
"destination": "+15550001234",
"originator": "+15557654321",
"reason": "keyword_stop",
"workspace_id": "ws_01ky7m21hjffh9s8kq76gyb1xf"
}
}A workspace-wide opt-out recorded on the Preferences tab is a stated preference rather than a suppression, and does not fire this event.
Reading a message's timeline
Webhooks deliver events to your systems. For a one-off review, the SMS log renders the same stream as a timeline with timestamps, carrier details, and errors. To retrieve the timeline programmatically, call GET /v1/sms/messages/{message_id}/events. To read only the latest state, call GET /v1/sms/messages/{message_id}.
Next steps
- Webhooks & events: set up an endpoint, verify signatures, and handle retries and replay.
- SMS log: inspect the per-message timeline these events drive.
- Sending SMS: set the tags and metadata echoed on every event.