← Back to blog

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:

  1. The synchronous response — what you get back immediately from the POST. It means the platform accepted the request and queued it.
  2. The 1st callback (send request report) — whether the request passed validation and reached the carrier. This tier decides billing.
  3. 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):

StatusMeaning
SMDA valid send — submitted to the carrier for delivery

Failure (rc = -100, not charged):

StatusMeaningUsually caused by
IMIncorrect country code or mobile numberNumbers not cleaned — a +, spaces, or a prefix
OFCAOn the Do-not-call Register and filtered (Hong Kong)The recipient opted out and you have no consent
IULOn the unsend list — not sentThe number previously unsubscribed
IPInsufficient pointsThe account balance ran out
ISInvalid or unregistered sender nameThe 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.

StatusMeaningWhat to do
DELIVRDDelivered to the recipient deviceThis is what “delivered” means
EXPIREDValidity period elapsed without deliveryConfirm the number is still live before retrying
UNDELIVUnreachable destination, network issue, or device offRetry later — success rate is decent
REJECTDInvalid content, blacklisted, or policy restrictionDo not resend as-is — fix content or check sender setup
UNKNWNCarrier cannot determine the final statusDo 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 SMDthis 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.