SMPP (Short Message Peer-to-Peer) is the standard protocol for exchanging SMS messages between applications and message centers. If you are building an SMS platform, integrating with a carrier gateway, or connecting to an aggregator for bulk messaging, you will almost certainly be working with SMPP. This guide covers the protocol from a developer's perspective -- what you need to know to build a reliable SMPP integration, from connection setup through production deployment.
SMPP Overview: ESME, SMSC, and the Connection Model
SMPP defines communication between two parties:
- ESME (External Short Messaging Entity) -- Your application. The system that sends and/or receives SMS messages. This could be a web application, a marketing platform, an OTP service, or any system that generates or consumes SMS traffic.
- SMSC (Short Message Service Center) -- The message center operated by the carrier or gateway provider. The SMSC accepts messages from ESMEs, routes them through the mobile network, and delivers them to handsets. It also accepts messages from handsets (mobile-originated) and delivers them to ESMEs.
SMPP uses persistent TCP connections. Your ESME establishes a TCP connection to the SMSC (typically on port 2775, or 2777 for TLS), then sends a bind request to authenticate. Once bound, the connection remains open and both sides can send PDUs (Protocol Data Units) at any time. The connection stays alive until one side sends an unbind request or the TCP connection drops.
This persistent connection model is a key advantage of SMPP over HTTP-based APIs. With HTTP, each message requires a new request-response cycle (or at least a new HTTP transaction). With SMPP, the connection is established once and can handle thousands of messages per second without the overhead of repeated authentication or connection setup.
Bind Types
Before you can send or receive messages, you must bind (authenticate) your connection. SMPP defines three bind types:
- Bind Transmitter (
bind_transmitter, command_id: 0x00000002) -- Your ESME can only send messages (submit_sm). The SMSC will not deliver messages or DLRs to you on this connection. Use this if you only need to send outbound messages and will receive DLRs through a separate mechanism (webhook, separate receiver bind). - Bind Receiver (
bind_receiver, command_id: 0x00000001) -- Your ESME can only receive messages (deliver_sm). You cannot send messages on this connection. Use this for receiving mobile-originated messages or DLRs on a dedicated connection. - Bind Transceiver (
bind_transceiver, command_id: 0x00000009) -- Your ESME can both send and receive on the same connection. This is the most common bind type in modern implementations. It simplifies connection management (one connection instead of two) and is supported by virtually all SMSCs.
The bind request includes your credentials: system_id (username), password, and system_type (optional, identifies your application type). The SMSC responds with a bind response containing the SMSC's system_id and a command_status indicating success or failure.
# Bind Transceiver PDU (simplified)
Command ID: 0x00000009 (bind_transceiver)
Sequence Num: 1
System ID: "myapp" # Your username
Password: "s3cret" # Your password
System Type: "OTP" # Application type (optional)
Interface Ver: 0x34 # SMPP v3.4
Addr TON: 0x01 # International
Addr NPI: 0x01 # E.164
Address Range: "" # Accept all (or restrict to specific range)
PDU Structure
Every SMPP message is a PDU (Protocol Data Unit) with a standard header and a variable body. The header is 16 bytes:
- Command Length (4 bytes) -- Total PDU size including the header.
- Command ID (4 bytes) -- Identifies the PDU type (bind_transceiver, submit_sm, deliver_sm, etc.).
- Command Status (4 bytes) -- Result code for response PDUs.
0x00000000= ESME_ROK (success). Non-zero values indicate errors. - Sequence Number (4 bytes) -- Matches requests to responses. Your ESME assigns a unique sequence number to each request; the SMSC echoes it in the response. This is how you correlate asynchronous responses.
The body varies by PDU type. For a submit_sm (sending a message), the body includes source address, destination address, data coding, message text, and optional TLV (Tag-Length-Value) parameters.
Key PDUs
submit_sm (Send a Message)
The most important PDU for outbound messaging. Key fields:
- source_addr_ton / source_addr_npi / source_addr -- The sender address (your originating number or alphanumeric sender ID). TON (Type of Number) = 0x01 for international, 0x05 for alphanumeric. NPI (Numbering Plan Indicator) = 0x01 for E.164, 0x00 for alphanumeric.
- dest_addr_ton / dest_addr_npi / destination_addr -- The recipient's phone number. Typically TON=0x01, NPI=0x01 for international E.164 numbers.
- data_coding -- The character encoding of the message body.
0x00= GSM-7 (default, 160 chars per segment),0x08= UCS-2 (Unicode, 70 chars per segment). See the data coding section below. - short_message -- The message text, encoded according to data_coding. Maximum 254 bytes in the short_message field. For longer messages, use the
message_payloadTLV or UDH-based concatenation. - registered_delivery -- Set to
0x01to request a delivery receipt (DLR). The SMSC will send a deliver_sm when the message is delivered (or fails). - validity_period -- How long the SMSC should attempt delivery before expiring the message. Format: absolute (YYMMDDhhmmss) or relative (000000hhmmss000R).
The SMSC responds with submit_sm_resp, containing the message_id -- a unique identifier for the submitted message. You need this ID to correlate with the delivery receipt.
deliver_sm (Receive a Message or DLR)
The SMSC sends deliver_sm to your ESME for two purposes:
- Mobile-originated messages -- An SMS sent by a handset to your number (short code, long code, or keyword). The
short_messagefield contains the message text. - Delivery receipts (DLRs) -- Status reports for previously submitted messages. The
esm_classfield is set to0x04to indicate this is a DLR rather than an MO message. The DLR contains the original message_id, delivery status, and timestamps.
DLR format varies between SMSCs, but the most common is a text string in the short_message field:
id:0123456789 sub:001 dlvrd:001 submit date:2603281430 done date:2603281431 stat:DELIVRD err:000 text:Your verific
Key DLR fields: id = message_id from submit_sm_resp, stat = delivery status (DELIVRD, UNDELIV, EXPIRED, REJECTD, ACCEPTD). Some SMSCs also provide the message_id in the receipted_message_id TLV, which is more reliable than parsing the text format.
Your ESME must respond to every deliver_sm with a deliver_sm_resp containing command_status = ESME_ROK. If you do not respond, the SMSC may redeliver the same PDU or close the connection.
enquire_link (Keepalive)
SMPP connections can go idle during periods of low traffic. Without keepalives, intermediate firewalls and load balancers may close the idle TCP connection. The enquire_link PDU serves as a heartbeat: either side can send it, and the other side must respond with enquire_link_resp.
Best practice: send an enquire_link every 30-60 seconds. If you do not receive a response within 15 seconds, assume the connection is dead and reconnect. Most SMSCs will close your connection if they do not receive any PDUs (including enquire_link) within their inactivity timeout (typically 60-120 seconds).
unbind (Graceful Disconnect)
When you want to close the connection cleanly, send an unbind PDU. Wait for unbind_resp, then close the TCP connection. This tells the SMSC that you are intentionally disconnecting, not crashing. The SMSC can clean up resources and will not attempt to redeliver queued messages to your connection.
Data Coding: GSM-7 vs. UCS-2
The data_coding field determines how the message text is encoded. Getting this wrong is one of the most common integration mistakes.
- data_coding = 0 (GSM-7 Default Alphabet) -- 7-bit encoding that supports the GSM default character set: basic Latin letters, digits, common punctuation. One character = 7 bits. Maximum 160 characters per SMS segment (1120 bits / 7 = 160). For concatenated messages, the UDH header uses 48 bits, leaving 153 characters per segment.
- data_coding = 8 (UCS-2) -- 16-bit Unicode encoding. Supports all Unicode characters including Chinese, Arabic, Cyrillic, emoji, and any character not in the GSM-7 alphabet. One character = 16 bits. Maximum 70 characters per segment. Concatenated: 67 characters per segment.
The critical rule: if your message contains any character that is not in the GSM-7 alphabet, you must use UCS-2 for the entire message. Mixing is not possible. Characters that force UCS-2 include accented characters not in the GSM extension table (e.g., certain diacritics), Chinese/Japanese/Korean/Arabic characters, and emoji.
Some SMSCs support data_coding = 3 (Latin-1/ISO-8859-1) or other encodings, but GSM-7 (0) and UCS-2 (8) are the only universally supported values. For a deeper discussion of segment calculation, see our article on UCS-2 vs GSM-7 segment counting.
Windowing: Multiple Outstanding PDUs
One of SMPP's performance advantages is windowing -- the ability to send multiple PDUs without waiting for responses. The window size defines how many unacknowledged PDUs can be outstanding at any time.
With a window size of 1, your ESME sends a submit_sm and waits for submit_sm_resp before sending the next message. This limits throughput to one message per round-trip time (RTT). With a 50ms RTT, that is only 20 messages per second.
With a window size of 10, you can have 10 unacknowledged submit_sm PDUs in flight simultaneously. This effectively multiplies your throughput by the window size: 10 * 20 = 200 messages per second with the same 50ms RTT.
Typical window sizes for production SMPP connections range from 10 to 100. The SMSC may enforce a maximum window size -- check with your provider. You need to track sequence numbers carefully to match responses to requests when multiple PDUs are in flight.
# Pseudocode: SMPP windowing with async response handling
window_size = 10
pending = {} # sequence_number → message_data
def send_message(destination, text):
while len(pending) >= window_size:
process_responses() # Read and handle any available responses
seq = next_sequence_number()
pdu = build_submit_sm(seq, destination, text)
socket.send(pdu)
pending[seq] = {'destination': destination, 'sent_at': time.time()}
def process_responses():
while data_available(socket):
pdu = read_pdu(socket)
if pdu.command_id == SUBMIT_SM_RESP:
original = pending.pop(pdu.sequence_number)
if pdu.command_status == ESME_ROK:
log_success(original, pdu.message_id)
else:
log_failure(original, pdu.command_status)
Error Handling
SMPP defines a comprehensive set of error codes in the command_status field of response PDUs. The most common ones you will encounter:
0x00000000ESME_ROK -- Success. The message was accepted.0x00000001ESME_RINVMSGLEN -- Message length is invalid. Check your short_message field length.0x00000005ESME_RALYBND -- Already bound. You sent a bind request on an already-bound connection.0x0000000DESME_RINVDFTMSGID -- Invalid default message ID. Usually means the message_id in a query or cancel request does not exist.0x0000000EESME_RX_T_APPN -- Temporary system error. Retry the message after a delay.0x00000045ESME_RSUBMITFAIL -- Submit failed. A generic failure that can mean many things -- check SMSC logs or contact your provider.0x00000058ESME_RTHROTTLED -- Throttled. You are exceeding the SMSC's rate limit for your account. Back off and reduce your submission rate.0x00000061ESME_RINVDCS -- Invalid data coding scheme. Your data_coding value is not supported.
Best practice for error handling: treat ESME_ROK as success, ESME_RTHROTTLED as a signal to reduce rate (implement exponential backoff), temporary errors (0x0000000E, 0x00000058) as retriable, and all other non-zero status codes as permanent failures that should be logged and investigated.
TLV Parameters
SMPP v3.4 introduced TLV (Tag-Length-Value) optional parameters that extend PDU capabilities. Important TLVs:
0x0424message_payload -- Alternative to short_message for messages longer than 254 bytes. Supports up to 64KB (though practical limits depend on the SMSC).0x001Ereceipted_message_id -- In deliver_sm DLRs, contains the original message_id. More reliable than parsing the DLR text.0x0427message_state -- In DLRs, the message state as an integer (1=ENROUTE, 2=DELIVERED, 5=UNDELIVERABLE, 6=ACCEPTED, 8=REJECTED).0x0204network_error_code -- Carrier-specific error code for failed deliveries.0x020Csar_msg_ref_num /0x020Esar_total_segments /0x020Fsar_segment_seqnum -- SAR (Segmentation and Reassembly) TLVs for concatenated messages, as an alternative to UDH-based concatenation.
SMPP over TLS
Standard SMPP transmits credentials and message content in plaintext over TCP. For production deployments, especially those carrying sensitive content (OTP codes, financial alerts), SMPP over TLS is strongly recommended.
The implementation is straightforward: wrap the TCP connection in a TLS layer before sending the bind request. The standard TLS port for SMPP is 2777 (compared to 2775 for plain TCP), though this is not universally standardized -- confirm with your provider.
The MOBITELSMS SMPP gateway supports TLS 1.2 and TLS 1.3 on port 2777, with optional mutual TLS (client certificate authentication) for enhanced security.
Connection Management for Production
A production SMPP integration needs robust connection management:
- Automatic reconnection -- If the connection drops (TCP reset, SMSC restart, network failure), your client should automatically reconnect with exponential backoff (1s, 2s, 4s, 8s, max 60s).
- Enquire link monitoring -- Send enquire_link every 30s. If no response within 15s, close and reconnect.
- Multiple connections -- For high throughput, open multiple parallel SMPP connections. Each connection can independently send and receive. Typical production setups use 2-10 connections per SMSC.
- Connection pooling -- Manage connections in a pool. Route submit_sm requests to the least-loaded connection. Handle connection failures by removing the dead connection from the pool and reconnecting in the background.
- Graceful shutdown -- On application shutdown, send unbind on all connections, wait for unbind_resp (with a timeout), then close TCP connections.
- Sequence number management -- Sequence numbers are 32-bit unsigned integers. They wrap around after 0xFFFFFFFF. Ensure your implementation handles wraparound correctly. Use per-connection sequence counters, not global ones.
Testing with SMPPSim
Before connecting to a production SMSC, test your integration against a simulator. SMPPSim is the most widely used SMPP testing tool. It is a Java application that emulates an SMSC, accepting binds, receiving submit_sm PDUs, generating deliver_sm DLRs, and logging all traffic.
# Install and run SMPPSim
wget https://github.com/seleniumQuery/smppsim/releases/download/v2.6.11/SMPPSim.tar.gz
tar xzf SMPPSim.tar.gz
cd SMPPSim
# Edit conf/smppsim.props to set port, credentials, etc.
java -jar smppsim.jar
SMPPSim configuration allows you to simulate various scenarios: successful delivery, delivery failure, DLR delays, throttling, and connection drops. Test all of these scenarios before going to production.
For integration with the MOBITELSMS SMPP platform, we provide a staging SMSC endpoint that behaves identically to production but does not deliver real messages. Contact our team for staging credentials.
PHP SMPP Client Example
Here is a simplified PHP example demonstrating the core SMPP operations using raw sockets. In production, use an established SMPP library, but understanding the raw protocol helps debug issues:
<?php
// Simplified SMPP client -- for illustration only
// Production code should use a proper SMPP library
function smpp_connect($host, $port, $system_id, $password) {
$socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
socket_connect($socket, $host, $port);
// Build bind_transceiver PDU
$body = pack_cstring($system_id) // system_id
. pack_cstring($password) // password
. pack_cstring('') // system_type
. pack('C', 0x34) // interface_version (3.4)
. pack('C', 0x01) // addr_ton (international)
. pack('C', 0x01) // addr_npi (E.164)
. pack_cstring(''); // address_range
$pdu = smpp_pack_header(0x00000009, 0, 1, strlen($body)) . $body;
socket_write($socket, $pdu);
// Read bind_transceiver_resp
$resp = smpp_read_pdu($socket);
if ($resp['command_status'] !== 0) {
throw new Exception("Bind failed: status " . $resp['command_status']);
}
return $socket;
}
function smpp_submit($socket, $src, $dst, $message, $seq) {
$body = pack_cstring('') // service_type
. pack('C', 0x05) // source_addr_ton (alphanumeric)
. pack('C', 0x00) // source_addr_npi
. pack_cstring($src) // source_addr
. pack('C', 0x01) // dest_addr_ton (international)
. pack('C', 0x01) // dest_addr_npi
. pack_cstring($dst) // destination_addr
. pack('C', 0x00) // esm_class
. pack('C', 0x00) // protocol_id
. pack('C', 0x00) // priority_flag
. pack_cstring('') // schedule_delivery_time
. pack_cstring('') // validity_period
. pack('C', 0x01) // registered_delivery (request DLR)
. pack('C', 0x00) // replace_if_present
. pack('C', 0x00) // data_coding (GSM-7)
. pack('C', 0x00) // sm_default_msg_id
. pack('C', strlen($message)) // sm_length
. $message; // short_message
$pdu = smpp_pack_header(0x00000004, 0, $seq, strlen($body)) . $body;
socket_write($socket, $pdu);
}
?>
Integration Checklist
- Use bind_transceiver for bidirectional communication on a single connection
- Implement enquire_link keepalives every 30-60 seconds
- Handle windowing with proper sequence number tracking
- Set data_coding correctly: 0 for GSM-7, 8 for UCS-2
- Request DLRs with registered_delivery = 1
- Parse DLRs using the receipted_message_id TLV when available
- Implement automatic reconnection with exponential backoff
- Handle ESME_RTHROTTLED with rate reduction
- Use TLS (port 2777) for production connections
- Test against SMPPSim before connecting to production
- Monitor connection health and PDU error rates in production
- Log all PDUs (with message content masked for privacy) for debugging
SMPP is a mature, battle-tested protocol that handles the majority of the world's A2P SMS traffic. Getting the integration right takes attention to detail -- data coding, windowing, error handling, and connection management all matter. But once your SMPP client is solid, it provides the highest-performance, lowest-latency path for SMS delivery at scale.