Anatomy of an SMPP delivery receipt
When you submit an SMS with registered_delivery set to 1, the SMSC later sends a deliver_sm PDU back on a receiver or transceiver bind. Bit 0x04 in its esm_class marks it as a delivery receipt rather than an inbound message, and its short_message carries a text receipt. Appendix B of the SMPP v3.4 specification gives this format as an example, and nearly every SMSC follows it: id:IIIIIIIIII sub:SSS dlvrd:DDD submit date:YYMMDDhhmm done date:YYMMDDhhmm stat:DDDDDDD err:E text:….
Because Appendix B is an example rather than a strict rule, real receipts vary: lowercase keys, extra spaces, submit_date with an underscore, 12-digit dates with seconds, or missing text. This decoder accepts all of those and tells you which fields are missing.
- id: the message ID the SMSC returned in submit_sm_resp. Use it to match the receipt to your message. The spec example shows 10 characters, but many SMSCs use longer IDs, sometimes in hex.
- sub and dlvrd: the number of messages originally submitted and delivered, as 3 digits. They matter mainly for distribution lists; for a single SMS they are typically 001 and 001 (or 000).
- submit date and done date: when the message was submitted and when it reached its final state, in the SMSC's clock.
- stat: the final or intermediate state, as a 7-letter code such as DELIVRD.
- err: a 3-character error code, network or SMSC specific; 000 means no error.
- text: the first 20 characters of the original message, often truncated or blank.
- Optional TLVs can repeat this data in a cleaner form: receipted_message_id (0x001E), message_state (0x0427) and network_error_code (0x0423).
DLR stat codes and what they mean
SMPP v3.4 defines eight message states. ENROUTE and ACCEPTD are intermediate; the others are final. Our API normalises every channel to six webhook statuses (SUBMITTED, SENT, DELIVERED, READ, FAILED, REJECTED), and the decoder shows the closest match for each stat value. Treat that mapping as indicative: the exact rule for your account is confirmed during onboarding.
- DELIVRD (message_state 2): delivered to the handset. Closest webhook status: DELIVERED.
- ACCEPTD (6): formally 'read manually on behalf of the subscriber by customer service', but most SMSCs send it as an intermediate 'accepted by the network' receipt. Closest: SENT.
- ENROUTE (1): still in transit, for example queued for a retry. Closest: SENT.
- UNDELIV (5): undeliverable, for example an invalid or inactive number. Closest: FAILED.
- EXPIRED (3): the validity period ended before the handset could be reached, often because it was off or out of coverage. Closest: FAILED.
- DELETED (4): deleted before delivery, for example by cancel_sm. Closest: FAILED.
- REJECTD (8): refused by the network or a firewall, for example an unregistered sender ID or blocked content. Closest: REJECTED.
- UNKNOWN (7): the SMSC has no final state. Treat it as unconfirmed, not as a proven failure.
err codes: why there is no universal list
The err field is three characters wide and the SMPP specification does not define its values. 000 always means no error, but non-zero values come from the SMSC vendor, the operator or an aggregator in between. Some SMSCs pass through GSM MAP error causes (for example, MAP error 1 is unknown subscriber and 27 is absent subscriber), while others map everything to their own table.
If the receipt carries the network_error_code TLV (0x0423), it is more reliable: its first octet gives the network type (3 means GSM) and the next two the error code. A DELIVRD receipt with a non-zero err is inconsistent and worth raising with your provider; the decoder flags it.
command_status errors versus delivery receipts
SMPP reports problems at two moments. The command_status in submit_sm_resp arrives within milliseconds and says whether the SMSC accepted your request; 0x00000000 (ESME_ROK) means accepted, not delivered. The delivery receipt arrives seconds to days later and carries the final outcome.
command_status values are defined in section 5.1.3 of SMPP v3.4. The most common in production are 0x00000058 ESME_RTHROTTLED (you exceeded your agreed messages per second: slow down and retry with exponential backoff), 0x00000014 ESME_RMSGQFUL (queue full: back off), 0x0000000A and 0x0000000B (invalid source or destination address) and 0x0000000D or 0x0000000E (bind failed or wrong password). Values 0x00000400 to 0x000004FF are reserved for SMSC vendor-specific errors, so ask your provider about those.
Dates, time zones and delivery latency
Receipt dates use YYMMDDhhmm, and many SMSCs append seconds (YYMMDDhhmmss). The receipt carries no time zone: the time is whatever the SMSC clock uses, usually UTC. The decoder shows the dates exactly as written and computes latency as done date minus submit date, which is unaffected by the time zone as long as both come from the same clock.
Latency is a useful quality signal. Healthy OTP traffic to a reachable handset usually completes within seconds. Long latencies followed by EXPIRED point to handsets that are switched off or roaming, and a done date earlier than the submit date means the two clocks disagree. Minute-level dates cannot measure sub-minute latency, so prefer SMSCs that send seconds.
Handling receipts reliably in production
Answer every deliver_sm with deliver_sm_resp, even if your processing fails, otherwise the SMSC resends it and may throttle the bind. Store the raw receipt, then process it asynchronously and idempotently by message ID, because receipts can arrive twice or out of order: a late ENROUTE must never overwrite DELIVRD.
If you would rather not parse receipts at all, our REST SMS API delivers the same information as normalised webhook statuses, and missed receipts can be re-pushed with POST /sms/resendDlr. SMPP binds over TLS are available for high-volume senders and resellers.
