Skip to main content

Troubleshooting: SMPP bind rejects and command_status codes

Two failure classes arrive on the wire itself when you run a raw SMPP client against Orbit’s edge (smpp.orbit.devotel.io:2775 plain TCP, :3550 TLS): a bind reject — the bind_transceiver / bind_transmitter / bind_receiver response carries a non-zero command_status — and a submit reject — the submit_sm_resp nack carries one of the ESME_* status codes. This page decodes both, then covers the two receipt-path failure classes (a DLR that never arrives, and receiver-side MO handling). Work the relevant table top to bottom — the first row that matches is the fix. For the full connect/SDK walkthrough see Connect via SMPP; for the model underneath the bind (reconciliation, credential lifecycle, BYO carriers) see SMPP edge model.

1. Bind rejects: bind_*_resp with a non-zero command_status

A bind reject comes back in the bind_*_resp PDU as a non-zero command_status; read the status from that PDU before assuming the network ate it. Three non-decoder causes account for the “bind returns nothing at all” symptom — work them before assuming a wire-level reject:
  1. Port misadvice (TLS vs plain-TCP). Both ports speak the same protocol and take the same credentials; the difference is transport only. A client pointed at 3550 without TLS (or at 2775 with TLS enabled) can complete the TCP connect and then stall on the handshake — it looks exactly like a hang, not a reject. Check the client library’s TLS/use_tls flag matches the port, and verify against the smpp.orbit.devotel.io hostname.
  2. A blocked port drops, not refuses. Only 2775 and 3550 are open; every other port is closed to the internet and a wrong-port connect sits open until your client gives up. Set an explicit connect timeout (5–10 s) so a wrong port fails in seconds instead of hanging.
  3. Credential status flip hasn’t reconciled. A suspended credential rejects new binds and drops existing ones; a revoked one removes the system_id from the relay entirely. Flipping the status in the dashboard takes effect on the wire within one reconciliation pass — budget a minute. If the dashboard says active but binds still reject after that, escalate with the credential id.

2. ESME_* submit rejects: command_status decoder for submit_sm_resp

These arrive on the submit_sm_resp nack — the bind is up; the edge refused the message. A submit that is accepted (command_status=0) on the bind and then never produces an outcome is the receipt class below, not a reject — move on.

3. DLR never arrives after an accepted submit

An accepted submit_sm returns message_id in the submit_sm_resp — the row exists. A receipt that never lands has three canonical owners, in order of how often they are the cause:
  1. dlrMode points the receipt elsewhere. On the credential, dlrMode = "bind" (the default) streams the receipt back as a deliver_sm on the live session; if the bind that is asking is closed, the receipt waits for the next bind. dlrMode = "webhook" has nothing to do with the session — it POSTs to dlrWebhookUrl. Point the mode where the listener is.
  2. The row was absorbed onto another channel, and the window closed. When a termination rule absorbs your submit_sm onto another channel (WhatsApp OTP is the common case), no carrier emits a receipt — Orbit synthesizes one and the deadline is per-rule. If you see stat:UNDELIV with a reason like no delivery within 45 s of acceptance, that is the armed window firing, not a missing receipt; tune it per SMPP: the no-delivery receipt window.
  3. The receipt arrived but your handler dropped it. A client that nacks or ignores deliver_sm PDUs because its handler crashed loses the receipt the same way a missing receipt reads. Log every inbound PDU at debug before assuming the edge held it.
For the platform-side receipt model — ACCEPTDDELIVRDUNDELIV mapped to canonical sent/delivered/failed states, err tokens 81/88/89/00 — see SMPP edge model.

4. Receiver-side (MO) path: inbox bind and opt-in

Traffic received on your SMPP bind — pass-through submit_sm PDUs your own ESME clients and partner aggregators send you — lands in one of two Inbox lanes by tenant choice:
  • The default (off) records each bind-received submit as a Messages row only — no Inbox conversation, no inbox_new_conversation notification. If agents do not work bind free-text in the Inbox, leave it off.
  • Turn the toggle on when a two-way aggregator workflow means agents genuinely reply from the Inbox; each bind submit then upserts a conversation per send and rings the new-conversation bell.
True carrier-side MO — a recipient’s reply to a message you sent — threads on the normal inbound path either way; the toggle gates only the conversation creation tied to traffic that arrived on your bind. If “replies on my bind do not appear in the Inbox” is the symptom, check the toggle first — with it on the default false, there is no conversation to open, and it does not retro-open conversations for traffic recorded while it was off. See SMPP Inbox opt-in for the dashboard and API paths (GET/PUT /messages/smpp-bind-inbox).

Fixes you should NOT try

  • Do not rebind in a tight loop after a credential reject. A rejected bind answered in milliseconds means the edge refuses the credential; more attempts only trip the throttled-rebind gate. Fix the credential, then rebind with backoff.
  • Do not treat ESME_RTHROTTLED as a transient. A second submit at the same rate re-fails; back off with jitter or raise tpsLimit.
  • Do not open a fresh bind per message or per batch. Each bind costs a full handshake; the edge treats a rapid rebind loop the same way every SMPP server does — throttled and flagged. One long-lived bind per sending process.
  • Do not send ESME_RINVDSTADR retries at the same destination. The destination is byte-rejected; only ever fix the address, never resend.
  • Do not point dlrMode at both and then ignore one leg. A bind+webhook receipt delivered to a webhook endpoint your stack drops is still delivered; pick one leg and drain it.

When to escalate

Open a support ticket when one of these holds:
  • The dashboard shows the credential as active but binds still reject with ESME_RINVSYSID or ESME_RINVPASWD after one reconciliation pass (about a minute) — that is reconciler-side, not credential-side.
  • submit_sm nacks with a command_status not in the decoder table.
  • A submit_sm accepted on the bind never produces either a receipt or a stat:UNDELIV past the armed no-delivery window for that rule, and dlrMode already points at the live leg.
Include all three of these so support can trace the row without a back-and-forth:
  • Your tenant ID (Settings → Organization, or organizationId from GET /api/v1/me).
  • The credential id (smpp_…) and the system_id (smpp_sys_…) returned at creation.
  • A failing PDU trace: the command_status value and the PDU type (bind_*_resp or submit_sm_resp), plus the message_id from the submit_sm_resp if the submit was accepted.

See also