SMS

What is a delivery receipt (DLR), and how reliable is it?

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 saysYou get
deliveredsms.delivered, and the message is delivered
expiredsms.expired, carrying the reason's code
rejectedsms.rejected, carrying the reason's code
delivery_failedsms.undelivered or sms.failed, see below
bufferednothing 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. 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 bucketCodeTreated as
unavailable_subscriberunreachabletemporary
received_network_errorprovider_unavailabletemporary
capacity_limit_reachedprovider_unavailabletemporary
unknown_subscriberinvalid_destinationpermanent
opted_outrecipient_opted_outpermanent
carrier_rejectedcontent_rejectedpermanent

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.

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 has the full event catalogue and the payload each one carries, and how to read a message's timeline back.

Développez sur le même réseau.

Une clé API de test est disponible immédiatement. La production est activée dès que vous ajoutez un moyen de paiement et vérifiez un expéditeur.

Commencez avec un seul canal.
Ajoutez les autres quand vous êtes prêt.

Une clé API de test est disponible immédiatement. L'accès production se débloque dès que vous ajoutez un moyen de paiement et vérifiez un expéditeur.

Vous utilisez Claude Code, Cursor ou Codex ? Copiez un prompt de configuration et votre agent installe la CLI Bird et les compétences pour vous. Choisissez le vôtre :

Cursor