All posts

SMPP Error Codes Explained: Complete Reference for SMS Delivery Failures

A few years back, I got pulled into a billing review for an enterprise client who swore their OTP delivery was fine. Their dashboard showed 98% success.

June 20, 202613 min read
smpp-error-codes

A few years back, I got pulled into a billing review for an enterprise client who swore their OTP delivery was fine. Their dashboard showed 98% success. Their customers were calling support because codes weren't arriving. Both things were true at the same time, and the gap between them lived entirely in a column most people never look at: the SMPP error code returned at the protocol level.

smpp-error-codes

That gap is where a lot of money and trust quietly disappear.

The Number That Lied

Most teams treat SMPP error codes as plumbing. The SMS either went out or it didn't, and if it didn't, somebody pastes a hex value into a search engine and moves on. The problem is that SMPP errors don't operate on a single layer. There's the submit response you get back when you hand the message to the SMSC, and there's the delivery receipt that arrives later telling you what actually happened on the network. People conflate the two constantly.

A message can return ESME_ROK accepted, no error, and still never reach the handset. The acceptance only means the SMSC took custody. What happens downstream, across interworking agreements, foreign operators, and number portability lookups, is reported through delivery receipts with their own status fields and their own error codes that have nothing to do with the original submit. If your monitoring only watches submit responses, your success rate measures whether you successfully spoke to a server, not whether a human read a message. That's the 98% I mentioned. Submit-time success. Useless on its own.

Two Codes, Two Different Stories

SMPP error codes come in two flavors that get muddled. The first is the command_status field in the protocol PDU, the SMSC's immediate verdict on your submission. The second is the error reason inside a delivery receipt, which is frequently a vendor-specific or operator-specific code mapped loosely onto the standard set, or not mapped at all.

Here's the part nobody tells you: the standard SMPP v3.4 spec defines a fixed range of ESME_* values, but every gateway, aggregator, and SMSC vendor extends that range. So when you see error 0x0451, that's outside the standard table, which means somebody upstream invented it. Two providers can return the same numeric code for completely different failures. I've seen 0x0058 mean "throttling" on one route and "destination blocked" on another. If you build alerting on raw codes without knowing whose codes they are, you'll chase ghosts.

Following a Message Through Its Lifecycle

When you submit a message, the flow looks roughly like this from where I sit reading logs.

Your application sends a submit_sm PDU. The SMSC responds with submit_sm_resp carrying a command_status. If that status is 0x00000000 (ESME_ROK), you got a message ID back, and the SMSC owns the message now. Anything non-zero at this stage is a submit rejection; the message never entered the delivery pipeline.

The common submission rejections worth memorizing:

  • 0x0000000B — ESME_RINVDSTADR: invalid destination address. Often, a malformed MSISDN or a missing country code.

  • 0x0000000A — ESME_RINVSRCADR: invalid source address. Frequently, an alphanumeric sender ID that the route doesn't permit.

  • 0x00000058 — ESME_RTHROTTLED: You're exceeding your allowed throughput. The SMSC is telling you to slow down.

  • 0x00000014 — ESME_RMSGQFUL: message queue full. Capacity problem upstream, usually transient.

  • 0x0000000D — ESME_RBINDFAIL: your bind failed. Credentials, IP allowlist, or session limits.

  • 0x00000045 — ESME_RSUBMITFAIL: generic submit failure, which is the SMSC's way of saying "no" without telling you why.

If the submission succeeds, the message travels, and later a deliver_sm PDU arrives carrying the delivery receipt. That's where the real story is. The receipt has a stat field, DELIVRD, UNDELIV, EXPIRED, REJECTD, UNKNOWN, and an err field with the actual failure code. UNDELIV with an err of 000 tells you almost nothing. UNDELIV with a meaningful operator error tells you whether the number is portable-and-misrouted, the handset was off until expiry, or the destination operator blocked the sender.

The discipline that separates teams who understand their traffic from teams who don't: reconcile, submit responses against delivery receipts per message ID, and aggregate the err values, not just the stat values.

A Working Reference to the SMPP Error Codes That Matter

I'm not going to hand you the spec sorted by hex value, because that's not how you meet these codes. You meet them in clusters, depending on what's broken. So here's how I actually group them after years of triaging the things.

Session and bind problems: these show up the moment you connect. If you're seeing these, stop looking at your messages and look at your connection logic.

  • 0x0D ESME_RBINDFAIL: bind rejected; Most commonly caused by credential issues, IP restrictions, or account provisioning problems.

  • 0x0E ESME_RINVPASWD: invalid password.

  • 0x0F ESME_RINVSYSID: invalid system ID.

  • 0x05 ESME_RALYBND: already bound; usually a connection-pool bug on your side.

  • 0x04 ESME_RINVBNDSTS: wrong command for your bind type, like submitting on a receiver-only bind.

Address problems — where most submit rejections actually live.

  • 0x0B ESME_RINVDSTADR — invalid destination; the one you'll see most, almost always a number never normalized to E.164.

  • 0x0A ESME_RINVSRCADR — invalid source; on international routes, usually an alphanumeric sender ID the operator won't accept.

  • 0x48 ESME_RINVSRCTON:— invalid source TON.

  • 0x49 ESME_RINVSRCNPI:— invalid source NPI.

  • 0x50 ESME_RINVDSTTON:— invalid destination TON.

  • 0x51 ESME_RINVDSTNPI: — invalid destination NPI. (These four trip people up constantly. The address looks fine, but the type-of-number or numbering-plan flags don't match. One wrong TON can silently null-route a whole campaign.)

Throttling and capacity: the codes that mean slow down.

  • 0x58 ESME_RTHROTTLED: You're over throughput; Some providers apply additional defensive throttling below contractual limits.

  • 0x14 ESME_RMSGQFUL: queue full upstream; transient, clears with sane retry and backoff.

Malformed PDU and bad flags: The SMSC is rejecting the shape of what you sent.

  • 0x01 ESME_RINVMSGLEN: invalid message length.

  • 0x02 ESME_RINVCMDLEN: invalid command length.

  • 0x03 ESME_RINVCMDID: unrecognized command.

  • 0x06 ESME_RINVPRTFLG: invalid priority flag.

  • 0x07 ESME_RINVREGDLVFLG: invalid registered-delivery flag.

  • 0x43 ESME_RINVESMCLASS: invalid esm_class.

  • 0x54 ESME_RINVREPFLAG: invalid replace-if-present flag.

Optional-parameter (TLV) errors: surface when you're doing anything fancy with optional tags, and TLV support doesn't match.

  • 0xC0 ESME_RINVOPTPARSTREAM: error in the optional part of the PDU body.

  • 0xC1 ESME_ROPTPARNOTALLWD: optional parameter not allowed.

  • 0xC2 ESME_RINVPARLEN: invalid parameter length.

  • 0xC3 ESME_RMISSINGOPTPARAM: expected optional parameter missing.

  • 0xC4 ESME_RINVOPTPARAMVAL: invalid optional parameter value.

Service, scheduling, and message-handling: operational odds and ends.

  • 0x15 ESME_RINVSERTYP: invalid service type.

  • 0x53 ESME_RINVSYSTYP: invalid system type.

  • 0x61 ESME_RINVSCHED: bad scheduled-delivery time.

  • 0x62 ESME_RINVEXPIRY: bad validity period. (Both are common when a time format gets hardcoded wrong.)

  • 0x63 ESME_RINVDFTMSGID: predefined message doesn't exist.

  • 0x0C ESME_RINVMSGID: message ID is garbage.

  • 0x11 ESME_RCANCELFAIL: cancel failed.

  • 0x13 ESME_RREPLACEFAIL: replace failed.

  • 0x67 ESME_RQUERYFAIL: query failed. (The last three fire when you operate on a message ID the SMSC can't find or won't touch.)

Receiver-app codes: relevant if you're on the terminating side.

  • 0x64 ESME_RX_T_APPN: temporary app error; retry.

  • 0x65 ESME_RX_P_APPN: permanent app error; don't retry.

  • 0x66 ESME_RX_R_APPN: outright reject.

Distribution-list codes — you'll rarely touch these unless you're doing multi-recipient submits, which most modern stacks avoid.

  • 0x33 ESME_RINVNUMDESTS: invalid number of destinations.

  • 0x34 ESME_RINVDLNAME: invalid distribution-list name.

  • 0x40 ESME_RINVDESTFLAG: invalid destination flag.

  • 0x42 ESME_RINVSUBREP: invalid submit-with-replace.

  • 0x44 ESME_RCNTSUBDL: cannot submit to distribution list.

  • 0x55 ESME_RINVNUMMSGS: invalid number of messages.

The ones you hope you never think hard about.

  • 0x08 ESME_RSYSERR: generic SMSC fault; their problem, not yours.

  • 0x45 ESME_RSUBMITFAIL: the infamous "no" with no explanation.

  • 0xFE ESME_RDELIVERYFAILURE: delivery failure, mostly on data_sm responses.

  • 0xFF ESME_RUNKNOWNERR: even the SMSC doesn't know.

  • 0x00 ESME_ROK: the one everyone celebrates, which only ever meant the SMSC took the message off your hands, nothing about whether it arrived.

That's the standard v3.4 set. The codes that'll actually keep you up at night are the ones above this range that your specific aggregators invented, and the only reference that helps there is the one you build yourself from your own traffic.

The delivery-receipt stat values are a separate vocabulary you'll pair with all of the above: DELIVRD, EXPIRED, DELETED, UNDELIV, ACCEPTD, UNKNOWN, and REJECTD. The receipt's own err field is where operator-specific failure reasons land, and those rarely follow this list.

When a Delivered Receipt Is a Lie

The assumption that bites people is that a delivered receipt equals a delivered message. On a lot of international routes, especially grey routes and some SS7-based interworking, the delivery receipt is fabricated. The intermediate party returns DELIVRD regardless of what happened on the terminating network because their commercial incentive is to look reliable. You're paying for delivery confirmations that are essentially fiction.

I learned to spot this by correlating receipt timing. Genuine delivery receipts on a given operator cluster around a characteristic latency distribution. When you see a route returning DELIVRD in a suspiciously uniform sub-second window across thousands of messages, that's not great network performance. That's a receipt generator. The error codes look perfect precisely because nobody on the real network is allowed to report a failure.

This is why error-code analysis isn't just troubleshooting, it's route quality intelligence. Your error distribution is a fingerprint of who you're actually connected to.

Where the Money and Trust Leak Out

The costs stack up in places that don't show on the obvious dashboard.

OTP and verification flows are where this hurts most. A user who doesn't receive a code retries, which generates more billable submits, which inflates your messaging spend while your conversion drops. Repeated EXPIRED receipts on time-sensitive traffic are a direct signal that your route latency is killing the use case, even when the eventual stat reads are delivered.

Then there's analytics distortion. If your ESME_ROK submit rate is feeding a "deliverability" metric, leadership is making routing and vendor decisions on a number that doesn't describe reality. I've watched companies renew contracts with the worst-performing route in their stack because that route's submission acceptance was flawless; it accepted everything and delivered a fraction.

Security is the quiet one. Error-code patterns are an early indicator of abuse. A sudden spike in ESME_RINVDSTADR across sequential number ranges is somebody enumerating MSISDNs through your platform. A climb in throttling errors paired with traffic to unusual destinations can signal artificially inflated traffic being pushed through your account. The codes were telling the story months before the billing review caught it, nobody was reading them as a security feed.


The Patterns Worth Watching

From experience, these are the patterns worth wiring into monitoring:

  • A widening gap between submission success and delivery-receipt success. The two curves should track loosely; divergence means downstream failure you're not seeing at submit time.

  • Uniform, near-instant DELIVRD receipts on international routes. Almost always, synthetic confirmations.

  • Rising EXPIRED rates on OTP traffic point to latency or terminating-network congestion regardless of final status.

  • Clusters of ESME_RINVDSTADR or ESME_RINVSRCADR that correlate with specific sender IDs or destination prefixes — often a configuration drift or a blocked sender ID nobody updated.

  • Throttling errors (ESME_RTHROTTLED) are appearing well below your contracted throughput. That's the provider quietly capping you, and it changes your capacity planning.

  • Vendor-specific error codes outside the standard range that suddenly change meaning after a route or vendor switch.

What I Tell Teams to Do

Stop trusting a single success number. Build your reporting on the reconciliation between the submit response and the delivery receipt, keyed by message ID, so you can see exactly where in the lifecycle a message died.

Maintain a per-provider error-code dictionary. Standard SMPP codes are only the baseline; the codes that matter for your traffic are the extended ones your specific routes return. Document what each one actually means in practice, not what the spec implies, because the spec and reality diverge the moment you leave your own SMSC.

Watch error distributions as a route-quality signal, not just an incident trigger. Two routes to the same operator with identical pricing can have completely different error fingerprints, and the one with honest failure reporting is usually the better route even if its headline numbers look worse.

For verification traffic specifically, treat EXPIRED and high-latency DELIVRD as failures in your own metrics even when the protocol calls them delivered. The user experience is what you're optimizing, not the protocol's definition of success.

And keep a baseline. You can't recognize an anomalous error pattern if you never characterized normal. The teams that catch fraud and route degradation early are the ones who know their typical error distribution well enough to notice when it shifts.

Where This Is All Heading

The direction of travel is toward less visibility, not more, unless you actively fight for it. As more traffic moves through aggregation layers, RCS, and over-the-top verification channels, the clean SMPP error contract gets abstracted away behind APIs that return their own simplified status codes. Convenient, until you need to know why a message failed, and the abstraction won't tell you.

The operators and platforms that will hold an advantage are the ones investing in delivery intelligence correlating protocol-level errors with real outcomes, detecting synthetic receipts, and treating their error telemetry as both a quality and a security dataset. Regulatory pressure around sender ID registration and traffic legitimacy is also going to make accurate error attribution a compliance matter, not just an operational nicety. The error code that is an annoyance today becomes tomorrow's audit trail.

The Lesson That Stuck

The thing I keep coming back to is that an SMPP error code is rarely just a technical fault. It's a small, honest report from a system that has no reason to lie to you unless somebody in the path has a reason to make it lie. Learn to tell the difference between an error that's telling you the truth and a success that isn't, and you'll understand your messaging network better than the people who only ever look at the green number on the dashboard. The failures were always documented. Someone just had to read them.

Quick Answers: SMPP Error Codes Explained

What are SMPP error codes?

SMPP error codes are status values returned during SMS transmission that indicate whether a message was accepted, rejected, or failed. They appear both in the submit response (command_status) and in delivery receipts, describing failures at different points in the message lifecycle.

How does an SMPP error code differ from a delivery receipt status?

The submit-time error code tells you whether the SMSC accepted your message. The delivery receipt status (DELIVRD, UNDELIV, EXPIRED) and its error field tell you what happened on the actual network afterward. Acceptance is not delivery.

Why is monitoring SMPP error codes important?

Because submit success alone hides downstream failures, fabricated delivery receipts, and abuse patterns. Reading error codes across the full lifecycle reveals real deliverability, route quality, and security signals that headline success rates conceal.

What is the difference between ESME_ROK and a successful delivery?

ESME_ROK means the SMSC accepted the message for processing, nothing more. The message can still fail downstream and return UNDELIV or EXPIRED in its delivery receipt. Treat ESME_ROK as a custody transfer, not a confirmation.

What are the common SMPP submit error codes?

Frequent ones include ESME_RINVDSTADR (invalid destination), ESME_RINVSRCADR (invalid sender), ESME_RTHROTTLED (rate limited), ESME_RMSGQFUL (queue full), and ESME_RSUBMITFAIL (generic rejection).

What are the risks of ignoring SMPP error codes?

You overpay for retries on failed OTP delivery, make routing decisions on misleading metrics, miss fabricated delivery receipts on grey routes, and overlook early indicators of fraud such as MSISDN enumeration or artificially inflated traffic.

How can businesses improve SMS delivery using error codes?

Reconcile submit responses against delivery receipts per message ID, maintain a per-provider error-code dictionary, treat error distributions as route-quality intelligence, and establish a baseline so anomalous failure patterns stand out early.

Share this post