# 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](/docs/guides/webhooks).

The delivery lifecycle, as a path through the event types:

1. `sms.accepted`: Bird has the message and is preparing to hand it to a carrier
2. `sms.sent`: the message was handed to the carrier; awaiting a delivery receipt
3. One terminal event:
   - `sms.delivered`: the carrier confirmed delivery to the handset
   - `sms.undelivered`: a non-permanent non-delivery (handset off, unreachable)
   - `sms.failed`: a permanent failure; the message will not be delivered
   - `sms.expired`: the message's validity window elapsed before a terminal receipt arrived

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 SMS is outbound-only today. Replies to your messages are not yet received or captured, so no inbound event type exists.

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](https://www.standardwebhooks.com) nested envelope described in the [Webhooks guide](/docs/guides/webhooks): 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, e.g. `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 (fields below)                                                                      |

Every SMS event's `data` carries the identity base: `sms_id`, `workspace_id`, the `to` and `from` addresses, and the `tags` and `metadata` you set on the send, echoed on every event so you can route and correlate against your own records without an extra lookup (each is `null` when the send carried none).

```json
{
  "type": "sms.delivered",
  "timestamp": "2026-06-10T14:30:00Z",
  "data": {
    "sms_id": "sms_01krdgeqcxet5s7t44vh8rt9mg",
    "workspace_id": "ws_01krdgeqcxet5s7t44vh8rt9mg",
    "to": "+15551234567",
    "from": "Bird",
    "carrier": "Example Wireless",
    "mcc_mnc": "310260",
    "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: the identity base only.

### 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 `null` when not yet known. 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 `null` 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; the message will not be delivered.

### sms.rejected

The message was refused: by Bird's checks during processing, by a charge that could not complete, or by a carrier that turned it away. A rejection means the message was stopped, not attempted and lost in delivery. 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 message's validity window elapsed before a terminal delivery receipt arrived. Payload: the identity base only.

## Reading a message's timeline

Webhooks are how events reach your systems. For a one-off look at a single message, the [SMS log](/docs/guides/sms/sms-log) in the dashboard renders the same stream as a per-message timeline: every event with its timestamp, the carrier on delivery events, and the error detail on failures. A per-message events endpoint is not part of the public API today; to reconcile programmatically, read the message's current state with [`GET /v1/sms/messages/{message_id}`](/docs/api/reference/get-sms-message), whose `status`, `sent_at`, `delivered_at`, and `last_error` fields reflect the latest event.

## Next steps

- [Webhooks & events](/docs/guides/webhooks): endpoint setup, signature verification, retries, and replay
- [SMS log](/docs/guides/sms/sms-log): the per-message timeline these events drive
- [Sending SMS](/docs/guides/sms/sending-sms): the send payload whose `tags` and `metadata` echo on every event