A delivery receipt is the carrier's report on what became of a message after you handed it over. It is the only visibility anyone has into that stretch, which is exactly why its limits are worth knowing: it is a claim made by a third party about their own network, and both what it says and whether you see it at all are narrower than people expect.

## What does a delivery receipt actually tell me?

That the network believes it got the message to the handset. No more.

It is not read confirmation, and no part of the SMS chain offers one. A `delivered` receipt means the device acknowledged receipt to the network. The phone may be in a drawer. It is also not proof anyone saw the sender, or that the message rendered as you wrote it.

What arrives at Bird is a status plus a reason, and the two are read together: the status says what shape the outcome took, and the reason says why. That pairing is what decides which event you get, so neither half is enough on its own.

## Which events can a receipt produce?

Five outcomes, and one of them you will never see.

| Receipt says      | You get                                       |
| ----------------- | --------------------------------------------- |
| `delivered`       | `sms.delivered`, and the message is delivered |
| `expired`         | `sms.expired`, carrying the reason's code     |
| `rejected`        | `sms.rejected`, carrying the reason's code    |
| `delivery_failed` | `sms.undelivered` or `sms.failed`, see below  |
| `buffered`        | nothing you can observe                       |

**The buffered case is the one to know about.** When a carrier reports that it is holding the message, Bird records that in its own event log and publishes it to nobody: it reaches no status, no timeline, no rollup and no webhook. So a message can be sitting in a carrier's buffer, Bird can know it, and nothing in your view of that message changes. The absence of news is not the absence of receipts.

There is a second silence worth expecting. A receipt whose status Bird does not recognise produces no event at all rather than an unknown one, so it too passes without a trace.

Expiry is the mirror image and worth stating because people assume otherwise: **an `sms.expired` always comes from a carrier saying it gave up**, rather than from anything you set on the send.

You may find `validity_period` and reach for it to bound that. It appears twice and works neither time. On the send request it is a preview field that is not available, and supplying it returns [`422 SMSUnsupportedFeature`](/docs/api/errors/E12002). On the message read it is a preview field that is not returned, so it is empty however the message went out. Do not plan around either half: how long a carrier keeps trying is currently the carrier's decision, and nothing in the record will tell you what that bound was.

## Why did the same failure arrive as two different events?

Because `delivery_failed` splits on the reason, and the split is the difference between "try again later" and "do not try again".

Bird maps the carrier's reason onto its own error code and decides recoverability at the same time:

| Reason bucket            | Code                   | Treated as |
| ------------------------ | ---------------------- | ---------- |
| `unavailable_subscriber` | `unreachable`          | temporary  |
| `received_network_error` | `provider_unavailable` | temporary  |
| `capacity_limit_reached` | `provider_unavailable` | temporary  |
| `unknown_subscriber`     | `invalid_destination`  | permanent  |
| `opted_out`              | `recipient_opted_out`  | permanent  |
| `carrier_rejected`       | `content_rejected`     | permanent  |

A temporary reason becomes `sms.undelivered`. A permanent one becomes `sms.failed`. Same receipt status, different event, decided entirely by the word beside it.

**A reason outside that list gets the `unknown` code and is treated as permanent.** That is the conservative choice and it is the right one, but it means `sms.failed` is not proof that the carrier said anything permanent: it can equally mean the carrier said something Bird has no bucket for. If you retry on `sms.undelivered` and give up on `sms.failed`, you will occasionally give up on something that was worth another attempt.

The codes themselves, including which ones nothing emits, are covered in [why messages get filtered by carriers](/explained/sms/why-are-my-sms-messages-being-filtered-by-carriers).

## So how much should I trust it?

Trust it as one party's account of its own behaviour, and design so that a missing receipt is an expected state rather than an anomaly.

Three habits follow from how the mapping works.

1. **Do not treat silence as failure.** A message with no terminal event may be buffered at the carrier, or its receipt may have been one Bird could not recognise. Neither shows up as a failure, and neither means the message is lost.
2. **Branch on the event, and read the code for the reason.** The event carries the decision your system needs, temporary against permanent. The code tells you what to log, and `unknown` is a real and common value rather than an edge case.
3. **Never build a read metric on `delivered`.** It is a network acknowledgement. Delivery rate is a legitimate metric; open rate is not a thing SMS can give you.

[SMS events](/docs/guides/sms/events) has the full event catalogue and the payload each one carries, and how to read a message's timeline back.