What is SMPP?
SMPP was originally developed by Aldiscon (later acquired by Logica, now part of IMI Mobile) in the 1990s as an open protocol for exchanging SMS traffic between Short Message Service Centers (SMSCs) and external clients. The spec was handed over to the SMS Forum, which standardized SMPP v3.4 in 1999 — still the dominant version in 2026. A later v5.0 exists but has minimal adoption.
SMPP runs over TCP on port 2775 (or 3550 for TLS). It's a binary, session-oriented protocol: a client (ESME) opens a TCP connection to an SMSC, authenticates with a bind, then exchanges PDUs (Protocol Data Units) until one side unbinds or the connection drops.
Key Terminology
- SMSC — Short Message Service Center. The operator's SMS server. Stores, routes, and delivers SMS.
- ESME — External Short Message Entity. Any SMPP client connecting to an SMSC (your application, an aggregator, another operator).
- MO — Mobile Originated. SMS sent from a mobile subscriber to your application.
- MT — Mobile Terminated. SMS sent from your application to a mobile subscriber.
- PDU — Protocol Data Unit. A single SMPP message (bind, submit, deliver, etc.).
- TLV — Tag-Length-Value. Optional parameters added in v3.4+.
- DLR — Delivery Receipt. A status report confirming whether an MT was delivered.
Session Types (Bind Modes)
An SMPP session has one of three "roles", determined at bind time:
| Bind Type | PDU | Purpose |
|---|---|---|
| Transmitter | bind_transmitter | Send MT only (you submit, SMSC delivers to handsets). No inbound traffic. |
| Receiver | bind_receiver | Receive MO + DLRs only. No outbound traffic. |
| Transceiver | bind_transceiver | Bidirectional — both send and receive on one session. |
Transceiver is the most common in modern deployments because it's simpler and uses one TCP connection. Transmitter+Receiver pair is legacy and rarely used today.
PDU Structure
Every SMPP message has the same basic shape: a 16-byte header followed by a variable body.
┌─────────────────────────────────────────────┐
│ command_length (4 bytes, big-endian) │ Total PDU size
├─────────────────────────────────────────────┤
│ command_id (4 bytes) │ What PDU this is
├─────────────────────────────────────────────┤
│ command_status (4 bytes) │ 0 = OK on responses
├─────────────────────────────────────────────┤
│ sequence_number (4 bytes) │ Matches req ↔ resp
├─────────────────────────────────────────────┤
│ body (variable) │ Depends on command_id
└─────────────────────────────────────────────┘The sequence_number is used to match requests to their responses asynchronously — ESMEs can pipeline many submits before any response arrives.
The PDUs You'll Use 99% of the Time
| Command | Command ID | Purpose |
|---|---|---|
| bind_transceiver | 0x00000009 | Open a session (auth with system_id/password) |
| bind_transceiver_resp | 0x80000009 | SMSC's response |
| submit_sm | 0x00000004 | Send an MT message |
| submit_sm_resp | 0x80000004 | SMSC's ack with message_id |
| deliver_sm | 0x00000005 | Inbound MO or DLR from SMSC |
| deliver_sm_resp | 0x80000005 | Your ack to the SMSC |
| enquire_link | 0x00000015 | Keepalive ping (send every 30–60s) |
| enquire_link_resp | 0x80000015 | Keepalive pong |
| unbind | 0x00000006 | Close session gracefully |
| unbind_resp | 0x80000006 | Ack |
Less common but useful: query_sm, cancel_sm, replace_sm, data_sm, submit_multi.
Sending a Message: submit_sm
The main body of a submit_sm PDU has these key fields:
- source_addr_ton / source_addr_npi — type of number / numbering plan for sender
- source_addr — sender (up to 21 chars for alphanumeric senders)
- dest_addr_ton / dest_addr_npi — type/NPI for recipient
- destination_addr — recipient MSISDN (international format,
+stripped) - esm_class — message type (normal, delivery ack, etc.)
- protocol_id — 0 for normal SMS
- priority_flag — 0 (normal) to 3 (highest)
- schedule_delivery_time — "" for immediate
- validity_period — how long to retry if handset unreachable
- registered_delivery — 1 = request DLR
- data_coding — charset (see below)
- short_message — the actual message bytes, up to 254 octets
Data Coding: GSM-7 vs UCS-2
The data_coding field tells the SMSC how to interpret the message bytes. The three you care about:
| data_coding | Charset | Max chars (single SMS) | Max chars (concatenated) |
|---|---|---|---|
| 0 | GSM 7-bit default alphabet | 160 | 153 per part |
| 3 | ISO-8859-1 (Latin-1) | 140 | 134 per part |
| 8 | UCS-2 (UTF-16 BE, for Unicode) | 70 | 67 per part |
Rule of thumb: if your message is pure Latin with no emojis or accents, use GSM-7 (0). If it contains any non-GSM-7 character (Arabic, Chinese, emoji, em-dash, curly quotes), you must use UCS-2 (8) and you lose 90 characters per SMS. Users are often surprised their "170-character" message suddenly splits into 3 parts because they pasted a curly quote.
Long SMS: Concatenation via UDH
SMS was designed for 160 characters max. To send longer messages, you split into segments and use the User Data Header (UDH) to tell the handset they're parts of the same message so it can reassemble. The UDH occupies 6 bytes at the start of the payload, which is why you drop from 160 to 153 chars per segment.
The UDH for concatenation (IEI 0x00, 8-bit reference):
05 00 03 <ref> <total_parts> <part_number>
↑ ↑ ↑ ↑ ↑ ↑
│ │ │ │ │ └ 1..N (which part)
│ │ │ │ └ total parts
│ │ │ └ reference (same across all parts of this msg)
│ │ └ length of IE data
│ └ IE identifier (0 = 8-bit concat)
└ UDH lengthMany SMPP libraries handle this automatically — you pass a long string and it generates and submits the segments. Otherwise you do it manually via the message_payload TLV or manual UDH in short_message with esm_class = 0x40.
TLVs (Optional Parameters)
SMPP v3.4 added TLVs for carrying optional parameters that didn't fit in the fixed body. Useful ones:
message_payload (0x0424)— alternative toshort_message, supports up to 64 KB payload (useful for WAP push, concatenated messages)receipted_message_id (0x001E)— in a deliver_sm DLR, identifies which submit_sm it refers tomessage_state (0x0427)— state of a receipted message (ENROUTE, DELIVERED, EXPIRED, etc.)user_message_reference (0x0204)— your own reference IDcallback_num (0x0381)— call-back number TLV
Delivery Receipts (DLRs)
If you set registered_delivery = 1 on a submit_sm, the SMSC will eventually send you a deliver_sm PDU containing a DLR. The DLR's short_message field contains a text string in a standard format:
id:2b3c4d5e sub:001 dlvrd:001 submit date:2604121234
done date:2604121234 stat:DELIVRD err:000 text:HelloKey fields:
id— the message_id from submit_sm_resp (how you correlate)stat— final state:DELIVRD,EXPIRED,DELETED,UNDELIV,ACCEPTD,UNKNOWN,REJECTDerr— GSM network error code if failed
Common Error Codes
| Code | Name | Meaning |
|---|---|---|
| 0 | ESME_ROK | Success |
| 1 | ESME_RINVMSGLEN | Message length invalid |
| 8 | ESME_RSYSERR | System error (generic) |
| 10 | ESME_RINVSRCADR | Invalid source address |
| 11 | ESME_RINVDSTADR | Invalid dest address |
| 13 | ESME_RBINDFAIL | Bind failed (auth) |
| 14 | ESME_RINVPASWD | Invalid password |
| 15 | ESME_RINVSYSID | Invalid system_id |
| 20 | ESME_RMSGQFUL | Message queue full |
| 58 | ESME_RX_T_APPN | ESME receiver temporary app error |
| 69 | ESME_RSUBMITFAIL | Submit failed (generic) |
| 88 | ESME_RTHROTTLED | Rate limit exceeded — back off |
| 97 | ESME_RINVDLNAME | Invalid distribution list |
| 192 | ESME_RNOTSUPPRSP | No response from operator |
For the full list see our SMPP Error Codes reference.
Security and Rate Limiting
- TLS — use port 3550 with TLS whenever the aggregator supports it. Plain TCP SMPP should be avoided over the public internet.
- IP whitelisting — most aggregators require you to register source IPs before they'll accept connections.
- Rate limits — SMSCs enforce per-second submit limits (typically 30–200 TPS per bind). Respect
ESME_RTHROTTLEDand back off. - Multiple binds — for higher throughput, open multiple transceiver sessions in parallel rather than trying to push one faster.
- enquire_link — send a ping every 30–60 seconds. If you miss 3 in a row, assume the connection is dead and reconnect.
Best Practices
- Always request DLRs on A2P traffic (
registered_delivery=1) and actually process them — don't fire-and-forget. - Store the
message_idfrom submit_sm_resp so you can correlate DLRs. - Implement retry with exponential backoff on
ESME_RTHROTTLED. - Reconnect automatically on TCP drops; use sequence_number tracking to avoid duplicates.
- Detect charset — if the message contains any non-GSM-7 char, automatically switch to UCS-2.
- Use TLVs (
message_payload) for long messages; don't try to hand-roll UDH unless you must. - Monitor DLR success rate by destination country and route — sudden drops usually mean a carrier issue.
Key Takeaways
- SMPP is the universal SMS exchange protocol — v3.4 dominates in 2026.
- Open a
bind_transceiversession, submit viasubmit_sm, receive viadeliver_sm, keep alive withenquire_link. - Watch the data_coding field — any Unicode forces UCS-2 and cuts character limit from 160 → 70.
- Always request DLRs and correlate by
message_idto measure real delivery rates. - Respect rate limits (
ESME_RTHROTTLED), open multiple binds for throughput, always use TLS over the public internet.