SMS delivery reports (DLRs): states, webhooks and retry design

SMS delivery reports (DLRs) are SMSC-originated receipts that report how far a message got after submit. They expose a small set of standard states, often as short tokens, and usually arrive on your webhook. This article covers those states, what “delivered” can and cannot mean, and how to design idempotent, order-tolerant webhook handling with sensible retries and validity windows.

What an SMS DLR is

A delivery report, commonly called a DLR, is not generated by your application and is not a handset push in the HTTP sense. After you submit an MT message to an SMSC (or to a gateway that speaks to one), the SMSC may emit a delivery receipt describing the message’s progress or terminal outcome. That receipt is SMSC-originated: it reflects what the network path reported back into the SMSC’s store-and-forward machinery, then forwarded to you over SMPP-style delivery_sm semantics or, more often in modern stacks, translated into an HTTP webhook payload.

DLRs exist because SMS is store-and-forward. Submission success only means the first SMSC accepted the PDU. Downstream hops, handset reachability, roaming, handset storage, and operator policy all sit after that accept. The DLR is the asynchronous signal that something later happened—or that the SMSC gave up. Not every route supplies rich intermediate states; some only ever emit a terminal outcome, and some emit nothing useful at all. Your design must treat the DLR as best-effort signalling layered on top of messaging, not as a transactional commit from the handset.

In practice you correlate a DLR to the original submit using a provider or SMSC message identifier returned at submit time. Multipart messages (for example bodies that exceed the single-segment limits of 160 septets or 140 bytes depending on encoding, or the lower per-segment user-data sizes when UDH is present such as 153 septets or 67 UCS-2 characters) may produce per-segment network behaviour, but applications usually want a single logical message outcome. How segments are rolled up into one DLR is implementation-defined on the path; plan for one webhook per logical message id unless your integration explicitly documents otherwise.

The free DLR/SMPP error-code lookup on these SMSRoute guides covers every SMPP 3.4 command_status value referenced below.

Standard states and short stat tokens

SMPP-oriented delivery receipt vocabulary is the closest thing the industry has to a shared state set. You will see long names in documentation and short stat tokens inside classic receipt text. Treat the short forms as wire abbreviations of the same ideas, not as a second taxonomy. Carriers and aggregators differ in which states they ever emit; unknown or sparse reporting is normal.

Use the table below as a plain-language map. Terminal does not mean universally reliable—only that the SMSC considers the attempt finished from its point of view. Intermediate states may be skipped entirely.

State / tokenPlain meaning
ENROUTEAccepted into the network path; not yet terminal. May be the only non-final signal you ever see.
DELIVERED / DELIVRDSMSC believes the message reached the delivery endpoint it trusts—often the handset, sometimes only a downstream SMSC.
EXPIREDValidity period ended before a successful delivery confirmation arrived.
DELETEDRemoved from SMSC retry or storage before delivery completed.
UNDELIVERABLE / UNDELIVPath concluded delivery is not possible (invalid destination, barred, permanent failure class).
ACCEPTEDHanded to a downstream entity that took responsibility; not proof of handset display.
UNKNOWNOutcome not classifiable with the information the SMSC has.
REJECTED / REJECTDRejected by the SMSC or a downstream node after submit-time acceptance semantics, or reported as refused.

Why delivered is a partial truth

Developers naturally map DELIVERED to “the user saw the SMS.” That mapping is stronger than the protocol guarantees. On some routes, delivered means the handset acknowledged receipt at the radio/NAS layer the operator uses for SMS. On others, delivered means a downstream SMSC or intermediate gateway accepted the message and will not report further. Roaming, number portability, and handset power state all change what confirmation is available. Two carriers can both emit DELIVRD for materially different underlying events.

ACCEPTED is even easier to misread. It often signals successful hand-off, not end-user receipt. UNDELIVERABLE and REJECTED collapse many failure classes into coarse buckets; they are actionable for suppression and support workflows, but they rarely encode a precise reason code you can show a customer without additional context. EXPIRED ties to validity period configuration more than to handset behaviour alone: if your validity window is short, you will see more expiry relative to the same handset population.

Honest product language follows from this ambiguity. Surface DLR state to internal tools and support, drive retries and suppression from terminal failure classes where appropriate, and avoid promising handset-visible delivery solely because a delivered token arrived. When compliance or user experience depends on awareness, pair SMS with in-app or other channels rather than overloading the DLR.

SMSRoute reports delivery state per message; the retry patterns here apply to any provider that emits standard DLRs.

Webhook handling: idempotency, order, retries

Webhook delivery of DLRs fails in ordinary distributed ways: at-least-once retries from the sender, duplicate posts, delayed posts, and reordering relative to your submit acknowledgement. Design the consumer as an idempotent state machine keyed by message id plus reported state (and, if present, a receipt timestamp or provider event id). Deduplicate on that composite key before side effects. If the same message id moves ENROUTE then DELIVERED, store both transitions; if DELIVERED arrives twice, acknowledge twice to the HTTP client but apply business logic once.

Out-of-order arrival is common when intermediate and terminal receipts take different paths or when sender retry queues reorder. Never assume monotonic time of receipt equals monotonic state progress. Prefer a small precedence model: terminal states replace intermediate ones; conflicting terminals for one message id should be logged for investigation and resolved with a documented rule (for example, first terminal wins, or last-writer wins with audit). Do not flip a message from DELIVERED back to ENROUTE because a late duplicate intermediate arrived.

Your HTTP handler should answer quickly after durable accept—persist the event, then process asynchronously if fan-out is heavy. Return success only when the event is safely stored; transient storage failures should yield a retryable status so the sender’s backoff can work. On your outbound side toward customer systems, use exponential backoff with jitter, cap attempts qualitatively rather than hammering forever, and keep a dead-letter path for poison payloads. Retries exist to absorb blips; they are not a substitute for idempotent handlers.

Checklist for a robust DLR webhook path: (1) correlate on stable message id from submit; (2) idempotency key = message id + state (+ event id if supplied); (3) durable write before final 2xx; (4) allow at-least-once duplicates without double side effects; (5) apply explicit precedence for out-of-order states; (6) async fan-out after accept; (7) retryable status codes for transient faults only; (8) structured logs linking submit, DLR, and downstream actions; (9) quarantine unparsable bodies; (10) never block the request thread on slow third-party calls.

Validity periods, timeouts, and silence

Validity period is the SMSC-facing lifetime of the message attempt. When it lapses, EXPIRED is the honest terminal state if the SMSC still bothers to tell you. Timeout design in your application should align with that window plus a cushion for late receipts, not with an arbitrary UI spinner. If you mark a message failed in your database simply because your own short timer fired, you will disagree with later DLRs and confuse support. Prefer “pending until validity cushion elapses,” then “no terminal DLR observed,” as a distinct outcome from UNDELIVERABLE.

Absence of a DLR means nothing definitive by itself. Many routes under-report; some failures never produce a receipt; webhook endpoints that were down may exhaust the sender’s retry budget; filtering or account configuration may disable receipts. Silence is not delivered, not undelivered, and not proof of fraud or success. Operationally, track submit success, optional intermediate states, terminal states, and a separate “DLR not observed before watch window” bucket. Use the last bucket for route quality review, not for automatic customer-facing claims.

Put together: model DLR states explicitly, treat delivered as path-dependent confirmation, build webhooks for duplicates and reordering, and let validity period bound how long you wait before declaring observation finished. That combination stays accurate under carrier variance and keeps your messaging layer debuggable when production traffic misbehaves.

The SMSRoute deliverability checklist pairs with this guide when a DLR says delivered but the handset shows nothing.

Frequently asked

What does an SMS DLR DELIVRD or DELIVERED status actually mean?
It means the SMSC reported a delivered outcome for that message id. Depending on the route, that may reflect handset-level confirmation or only acceptance by a downstream SMSC. It is not a universal guarantee that the user read or even displayed the message.
How should I handle duplicate or out-of-order SMS delivery report webhooks?
Deduplicate on message id plus state (and event id when present), persist before returning success, and apply a precedence rule so terminal states are not overwritten by late intermediate ones. Assume at-least-once delivery and design side effects to run once.
If I never receive a DLR, was the SMS undelivered?
No. Missing DLRs are common: under-reporting routes, exhausted webhook retries, or disabled receipts all produce silence. Treat “no DLR within the validity-related watch window” as its own observation state, separate from UNDELIVERABLE or EXPIRED.

Related