SMS DLR status codes explained — what SMD, DELIVRD, UNDELIV and REJECTD actually mean
Anyone who has integrated SMS has hit this: the API returned rc: 100, the report says success, and the customer says nothing arrived.
The problem sits in a distinction many systems collapse. “We accepted it”, “we submitted it” and “the handset received it” are three different events — and plenty of reporting treats them as one.
Here is how SMS reporting actually works.
Three moments, not one
Between your server and the customer’s phone there are at least three observable states:
- The synchronous response — what you get back immediately from the POST. It means the platform accepted the request and queued it.
- The 1st callback (send request report) — whether the request passed validation and reached the carrier. This tier decides billing.
- The 2nd callback (delivery report / DLR) — the carrier’s report on the final handset state.
These can be seconds or hours apart. If your reporting only records the first, it is essentially telling you how many HTTP requests you issued.
The synchronous response: accepted, nothing more
{ "rc": 100, "rm": "OK", "ref_id": "145737475" }
rc: 100 means the request was well-formed and accepted. ref_id identifies it — both callbacks echo the same ref_id, which is the key that joins the whole chain together.
If the request itself is malformed (a missing required field, say), you get rc: -100 here with the reason in rm.
The 1st callback: submission
This tells you whether the request actually reached the carrier.
Success (rc = 100, charged):
| Status | Meaning |
|---|---|
SMD | A valid send — submitted to the carrier for delivery |
Failure (rc = -100, not charged):
| Status | Meaning | Usually caused by |
|---|---|---|
IM | Incorrect country code or mobile number | Numbers not cleaned — a +, spaces, or a prefix |
OFCA | On the Do-not-call Register and filtered (Hong Kong) | The recipient opted out and you have no consent |
IUL | On the unsend list — not sent | The number previously unsubscribed |
IP | Insufficient points | The account balance ran out |
IS | Invalid or unregistered sender name | The Sender ID is not attached to your account |
All five failures share one property: they are avoidable on your side. Number hygiene, unsubscribe management, balance monitoring, Sender ID binding — all within your control.
IP deserves special attention. If nothing monitors your balance, a large batch that runs out mid-flight fails silently for the remainder. Call /v1/acct/get-points-balance before a big send.
The 2nd callback: the real delivery report
This one only arrives when the carrier reports back.
| Status | Meaning | What to do |
|---|---|---|
DELIVRD | Delivered to the recipient device | This is what “delivered” means |
EXPIRED | Validity period elapsed without delivery | Confirm the number is still live before retrying |
UNDELIV | Unreachable destination, network issue, or device off | Retry later — success rate is decent |
REJECTD | Invalid content, blacklisted, or policy restriction | Do not resend as-is — fix content or check sender setup |
UNKNWN | Carrier cannot determine the final status | Do not treat as failure and blindly resend; watch the rate |
The three most commonly mishandled
UNDELIV treated as permanent failure. It is usually transient — phone off, in a tunnel, roaming abroad. Flagging the number as dead throws away list value. Retry once or twice after an interval.
REJECTD fed into an automatic retry. This is the most wasteful pattern there is. A content or policy problem does not resolve by sending again; it just gets rejected again. This status should page a human, not enter a retry queue.
UNKNWN counted as a failure. That makes your delivery rate look worse than it is. Track it as its own bucket instead — a route with a conspicuously high UNKNWN share is itself a signal worth acting on.
Reconciling the numbers
Join three counts on ref_id:
- Requests you sent
- 1st callbacks returning
SMD← this is the billing basis - 2nd callbacks returning
DELIVRD
The gaps between them are the interesting part. You can also pull sms_count and points_used for a date range from /v1/sms/get-usage-report and cross-check against your own records.
If a provider gives you a single “sent successfully” number, you have no way to see what happened in between — which is why we pass every failure code through untouched rather than packaging it into a flattering percentage. See Honest Delivery.
Two implementation notes
Make callback handling idempotent. Key on ref_id + status. The same callback can be delivered more than once, and if your handler decrements stock or sends email, running twice is a real bug.
Set the Callback URL in the dashboard first. Without it neither tier fires at all — plenty of people debug their own endpoint for a day before discovering nothing was ever being sent.
Full field definitions and example payloads are in the SMS API integration guide.