Server

SMPP: Bind Types and Message Flow

By Prabath Thalangama· October 24, 2026· 3 min read
#smpp#sms#messaging

Introduction

SMPP (Short Message Peer-to-Peer) is the TCP protocol an application (an ESME — External Short Messaging Entity) uses to send and receive SMS through an SMSC. Version 3.4 is near-universal; 5.0 exists but adoption is thin.

Session lifecycle

connect TCP (default port 2775)
  -> bind_transmitter / bind_receiver / bind_transceiver
  <- bind_*_resp  (command_status 0 = OK)
  ... enquire_link / enquire_link_resp every ~30s (keepalive) ...
  ... submit_sm / deliver_sm traffic ...
  -> unbind
  <- unbind_resp
close TCP

The three bind types

Bind Can send (submit_sm) Can receive (deliver_sm: MO + DLR)
transmitter yes no
receiver no yes
transceiver yes yes
  • transceiver (bind_transceiver) is the modern default — one connection, both directions. Use it unless the SMSC requires separate binds.
  • Older setups use a transmitter + receiver pair (two TCP connections, same system_id), often because the SMSC routes MO/DLR by the receiver bind.

Bind parameters: system_id (username), password, system_type (often blank or a category string), addr_ton/addr_npi, and address_range.

Sending: submit_sm

submit_sm:
  service_type       = ""
  source_addr_ton=5 (alphanumeric) source_addr = "MyBrand"
  dest_addr_ton=1 (international) dest_addr = "94771234567"
  esm_class          = 0x00
  data_coding        = 0 (GSM7) | 8 (UCS2 for non-Latin)
  registered_delivery= 1  (I want a delivery receipt)
  short_message      = "Your code is 123456"

submit_sm_resp returns a message_id — keep it; the DLR references it.

Delivery receipts (DLR)

If registered_delivery=1, the SMSC later sends a deliver_sm with esm_class bit 0x04 set, and short_message containing a receipt string:

id:1234567890 sub:001 dlvrd:001 submit date:2608271432 done date:2608271433
 stat:DELIVRD err:000 text:Your code is...

stat values: DELIVRD, EXPIRED, DELETED, UNDELIV, ACCEPTD, UNKNOWN, REJECTD. Match id against the message_id from submit_sm_resp (watch for decimal vs hex representation differences between vendors).

MO (mobile-originated, a real inbound SMS from a handset) also arrives as deliver_sm, without the receipt esm_class bit.

Sequence numbers and windowing

  • Every PDU has a sequence_number; the response echoes it. That's how you correlate submit_sm ↔ submit_sm_resp on a pipelined connection.
  • Window = how many submit_sm you send before waiting for responses. Window 1 = strictly synchronous (slow). A window of 10–100 pipelines for throughput. The SMSC advertises or enforces a max.
  • Throughput is also capped by a TPS limit (submit_sm per second) in your SMSC contract. Exceed it → ESME_RTHROTTLED (0x00000058); back off.

Concatenation (long SMS)

160 GSM-7 chars (or 70 UCS-2) splits into segments of 153 / 67. Either:

  • UDH in short_message with esm_class=0x40 — a 6-byte header 05 00 03 <ref> <total> <seq>, or
  • the sar_msg_ref_num / sar_total_segments / sar_segment_seqnum TLVs.

Pick one per SMSC; sending both, or mixing, causes garbled multi-part messages.

Verification and troubleshooting

# quick interactive test
smppcli   # from smpplib / or use "smpp" tools, jsmpp test client, Kannel's tools
tcpdump -i any -s0 -A 'port 2775'      # PDUs are binary but headers/strings show
  • bind_*_resp status 0x0000000D (ESME_RBINDFAIL) / 0x0E (ESME_RINVPASWD) / 0x0F (ESME_RINVSYSID) — credentials, or the source IP isn't allow-listed on the SMSC, or you tried a second bind with the same system_id while one is still open.
  • Bound but submit_sm returns ESME_RSUBMITFAIL / ESME_RINVDSTADR — destination number format (TON/NPI), or ESME_RINVSRCADR for a sender ID that's not registered/permitted (alphanumeric sender rules vary by country).
  • No DLRs arriving — registered_delivery was 0, you're on a transmitter-only bind (DLRs come on the receiver/transceiver), or the SMSC routes DLRs to a different bind/connection than you're listening on.
  • DLR id doesn't match your stored message_id — hex vs decimal, or the SMSC truncates to a different length. Store both forms.
  • Connection drops every ~60s — enquire_link not being sent/answered; a firewall idle timeout. Send enquire_link every 30s and respond promptly.
  • ESME_RTHROTTLED — over your TPS. Implement a token-bucket rate limiter at your contracted rate; queue and retry with backoff.
  • Garbled non-Latin text — data_coding should be 8 (UCS-2/UTF-16BE) and the payload encoded accordingly; segment at 67 chars, not 153.
  • Duplicate messages — you resubmitted on a timeout but the original succeeded. Use the window/sequence tracking; don't blind-retry submit_sm.
PrabathStuck on something this site can't fix?Reach out to Prabath directly on LinkedIn.