SMS DLR status codes explained — what SMD, DELIVRD, UNDELIV and REJECTD actually mean

On this page
  1. Three moments, not one
  2. The synchronous response: accepted, nothing more
  3. The 1st callback: submission
  4. The 2nd callback: the real delivery report
  5. Reconciling the numbers
  6. Two implementation notes

Key takeaways

  • A synchronous rc: 100 response only means the platform accepted and queued the request, not that the handset received it.
  • A 1st callback of SMD means submitted to the carrier and charged; only a 2nd callback with delivery_status of SS (that is, DELIVRD) means delivered.
  • The five 1st-callback failure codes (rc = -100) are not charged and are all avoidable on the sender’s side.
  • UNDELIV is usually transient, so retry once or twice after an interval instead of treating it as permanent failure.
  • Do not resend REJECTD as-is; page a human and fix the content or check the sender setup.

Anyone who has integrated SMS has hit this: the API returned rc: 100, the report says success, and the customer says nothing arrived. For the usual reasons behind it, see SMS not getting delivered? The surprising reasons behind it.

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. On registering the Sender ID in the first place, see Registering a # Sender ID in Hong Kong — a practical guide to the OFCA scheme.

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. Its fields differ from the 1st callback: the result is in delivery_status, where SS means delivered — the DELIVRD row below — and SF means not delivered, with an extra desc_code holding one of the other codes in the table.

{ "ref_id": "145737475", "rc": 100, "rm": "Carrier has successfully delivered to the recipient's device", "delivery_status": "SS" }

{ "ref_id": "145737477", "rc": 100, "rm": "Undelivered due to unreachable destination, network issues, or recipient", "delivery_status": "SF", "desc_code": "UNDELIV" }
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. For why route quality differs, see Grey routes versus direct carrier connections — why cheap SMS costs more.

Reconciling the numbers

Join three counts on ref_id:

  • Requests you sent
  • 1st callbacks returning SMD ← this is the billing basis
  • 2nd callbacks returning delivery_status: SS (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. For how messages are counted into billable segments, see How Hong Kong SMS billing works — characters, Unicode and segmentation.

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 plus the status (status on the 1st callback, delivery_status on the 2nd). 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. For the full path from creating an API key to receiving a first callback, see Taiwan SMS API quickstart — from sign-up to your first SMS to +886 (cURL, Node.js, Python, PHP).

Full field definitions and example payloads are in the SMS API integration guide.