DLR status codes: SMPP message states and what they really assert
Delivery receipts are the closest thing an SMS stack gives you to ground truth—and they are still not ground truth. This article maps SMPP v3.4 message_state values to what each one asserts and does not assert, which states can still change, how receipt text and network error families fit in, and how to handle them in webhooks without over-trusting a single stat field.
SMPP v3.4 message_state values: what each asserts
SMPP v3.4 defines a small set of message_state values used in deliver_sm delivery receipts and in query_sm responses. Treat them as network-reported classifications, not as forensic proof of handset behavior. The same label can arrive from different hops with different confidence. Always pair the state with the receipt text, any network error code, and your own submit/done timestamps.
ENROUTE asserts that the message has been accepted into the delivery path and is not yet in a terminal outcome known to the reporting node. It does not assert that the handset has rung, that the HLR lookup succeeded, or that the message will eventually deliver. DELIVRD or DELIVERED asserts that a downstream node reported successful delivery to the handset or to an accepted store-and-forward endpoint the network treats as delivered. It does not assert user read, app install, or that your content rendered correctly. EXPIRED asserts that the validity period elapsed without a successful delivery report the network considers final. It does not assert the handset was off the whole time—only that time ran out under the path’s rules.
DELETED asserts that the message was removed from a message center or queue before final delivery, typically by an administrative or network purge action. It does not tell you who initiated the delete or whether a handset ever saw the SMS. UNDELIVERABLE asserts that the network concluded delivery cannot complete under current conditions. It does not permanently brand the MSISDN as invalid in every future attempt; conditions change. ACCEPTED asserts that the message was accepted by a downstream entity that will not necessarily return a classical handset delivery—common for certain application endpoints or intermediate acceptance semantics. It is not a synonym for DELIVERED.
UNKNOWN asserts that the reporting node cannot map the outcome to a more specific state. It does not mean the message vanished without logs on your side; it means the receipt you received is uninformative. REJECTED asserts that the message was refused by a network or platform policy before or instead of normal delivery attempt completion. It does not always mean the number is invalid—content, originator, throughput, or barring rules can produce the same state depending on the path.
SMSRoute accounts include free test credits that prove delivery before you pay, and delivery-testing tools are available in the SMSRoute dashboard.
| message_state | Asserts | Does not assert |
|---|---|---|
| ENROUTE | In flight on a reporting path | Handset reached or eventual success |
| DELIVERED | Downstream success report received | Read receipt, UX success, or immutable truth |
| EXPIRED | Validity window ended without success | Permanent handset or number failure |
| DELETED | Removed from MC/queue before final delivery | Actor identity or handset visibility |
| UNDELIVERABLE | Path concluded delivery cannot complete now | Lifetime permanent invalidity |
| ACCEPTED | Accepted by an endpoint with non-classical DLR semantics | Handset delivery equivalent to DELIVERED |
| UNKNOWN | No better classification available | Absence of any system logs |
| REJECTED | Refused under policy or routing rules | Always a bad MSISDN |
Final versus intermediate states
Intermediate states can still change. ENROUTE is the clear intermediate state: a later DELIVERED, EXPIRED, UNDELIVERABLE, REJECTED, DELETED, or even ACCEPTED may follow. UNKNOWN should be treated operationally as non-final unless your upstream contract says otherwise; many integrations see UNKNOWN replaced when a clearer receipt arrives, and some never get an update.
Final states are those your handler should normally stop retrying submit logic for: DELIVERED, EXPIRED, DELETED, UNDELIVERABLE, REJECTED, and typically ACCEPTED when the path uses ACCEPTED as a terminal application acceptance. Final means final for that message identity on that path—not final for the business outcome. A DELIVERED DLR can still be wrong; an EXPIRED can be followed by a user-visible late SMS only in broken or multi-path setups, which you should treat as a defect signal rather than expected SMPP behavior.
Design state machines around message id correlation, not around hope. If you receive ENROUTE, keep the row open. If you receive a final state, close outbound retries for that id, record the state and err fields, and only reopen on an explicit duplicate-id conflict policy you control. Do not flip DELIVERED back to ENROUTE because a second receipt looks friendlier—log the anomaly instead.
Receipt text convention and network error-code families
Many SMSC deliver_sm receipts embed a short text body following a long-standing ESM-style convention. Fields commonly appear as labeled tokens: id (the message identifier as the MC knows it), sub (submit count), dlvrd (delivered count), submit date, done date, stat (textual state such as DELIVRD, EXPIRED, UNDELIV, ACCEPTD, REJECTD), and err (a network or MC error code). Parsing must be defensive: spacing, zero-padding on dates, and stat spellings vary by path. Prefer the structured message_state TLV when present, and use stat as corroboration rather than as the only signal.
The err field is not a universal dictionary. Carriers and hubs compress distinct failures into overlapping codes. Work in qualitative families instead of inventing per-carrier tables. Absent-subscriber family: the network could not place the subscriber—powered off, out of coverage, or temporarily detached, depending on the hop. Handset-memory family: the device or SIM storage path refused or deferred intake. Barring family: originator, destination, content class, or subscriber barring prevented completion. Routing-failure family: no viable route, destination network unreachable from that interconnect, or address formatting rejected before deep delivery. These families guide retries and user messaging; they do not justify hard-coded myths about a single numeric code always meaning one root cause.
Map families to product behavior with restraint. Absent-subscriber and handset-memory outcomes often justify limited, validity-aware retry or a slower follow-up. Barring and many routing failures usually should not be hammered with rapid resubmits of the same payload. When err is missing or zero while stat indicates failure, believe the failure class and retain the raw receipt for support escalation.
SMSRoute's error-code lookup tool covers the same states interactively.
EXPIRED versus REJECTED in practice
EXPIRED and REJECTED are both typically final, but they answer different questions. EXPIRED means the message was allowed to try within a validity period and that window ended without a success the reporting node accepts. Causes cluster around handset unavailability, deferred delivery that never cleared, or path latency beyond the TTL you or the MC set. REJECTED means refusal—policy, screening, invalid or non-routable address handling at the rejecting hop, commercial block, or similar up-front or near-up-front denial—rather than a clean run-out of time.
In handlers, EXPIRED invites inspection of validity period configuration, submit time versus done time, and whether ENROUTE persisted until timeout. REJECTED invites inspection of destination format (E.164 up to 15 digits), originator permissions, content rules, and whether the reject happened immediately after submit. An immediate REJECTED with near-zero latency is a different operational story from a long ENROUTE that becomes EXPIRED at the validity edge. Neither state is a polite synonym for the other; collapsing them into a single failed boolean throws away retry and compliance signal.
Fake-DLR red flags
Because DLRs are reputationally valuable, treat implausible receipts as first-class incidents. Red flags include uniform DELIVERED across large mixed destination sets with no variance in outcome families you know should appear in real traffic, and implausible latency—done dates at or beyond measurement noise of submit with perfect success, or fixed latencies that do not move with destination region or time of day. Another flag is stat text that never disagrees with an always-zero err across failure-prone routes, or message ids that do not correlate with your submit responses.
When red flags appear, freeze automated assumptions that equate DELIVERED with user reachability, retain raw PDUs or webhook payloads, and verify with controlled senders you operate. SMSRoute accounts include free test credits that prove delivery before you pay, and delivery-testing tools are available in the SMSRoute dashboard. Use those to establish a baseline of realistic state mix and timing before you trust a new route in production logic.
The SMSRoute DLR guide explains the webhook mechanics these states arrive through.
How to use these in webhook handlers
Structure the webhook as a correlator, not as a single-field switch. Key on the provider message id returned at submit; store submit timestamps yourself; ingest message_state or stat, err, and done date; then transition your internal record with an explicit state machine. Accept HTTP 200 only after durable persistence of the receipt to avoid provider retries doubling side effects; respond with HTTP 429 or HTTP 500 only when you genuinely need a retry, and keep those paths idempotent.
Normalize inbound states into your own enum that preserves SMPP distinctions—at minimum separate delivered, expired, rejected, undeliverable, accepted, deleted, enroute, and unknown. Attach error family tags qualitatively without claiming carrier-official dictionaries you do not maintain. For ENROUTE and unresolved UNKNOWN, schedule observation, not user-facing success. For DELIVERED, mark transport success and still gate business-critical actions on your own application acknowledgements where the domain requires it.
Finally, log p50 and p95 of submit-to-done latency per route class so fake or degraded DLR patterns show up as distribution shifts rather than anecdotes. Keep payload size discipline in mind at send time—160/153 GSM-7 and 70/67 UCS-2 segmenting rules still shape how splits and partial failures appear downstream—but do not invent delivery percentages. Uncertainty is part of the protocol surface; precise handlers record what was asserted, what was not, and what you will do next.
Frequently asked
- What are the SMPP v3.4 message_state values and which are final?
- The v3.4 set includes ENROUTE, DELIVERED, EXPIRED, DELETED, UNDELIVERABLE, ACCEPTED, UNKNOWN, and REJECTED. ENROUTE is intermediate and can still change; UNKNOWN is operationally non-final unless your upstream contract says otherwise. DELIVERED, EXPIRED, DELETED, UNDELIVERABLE, REJECTED, and typically ACCEPTED are treated as final for that message id on that path.
- How does EXPIRED differ from REJECTED on an SMS DLR?
- EXPIRED means the validity period ran out without a success the reporting node accepts after the message was allowed to attempt delivery. REJECTED means the message was refused under policy, routing, or screening rules rather than timing out. Immediate rejects and long enroute-then-expired paths should not be collapsed into one failed bucket if you care about retries and configuration fixes.
- What fields appear in a classic SMPP delivery receipt text body?
- Common labeled tokens include id, sub, dlvrd, submit date, done date, stat, and err. Use structured message_state when available and treat stat/err as corroboration; err codes should be grouped into qualitative families such as absent subscriber, handset memory, barring, and routing failures rather than trusted as a universal per-carrier table.