March 28, 2026 MOBITELSMS Engineering 14 min read

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:

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:

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:

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:

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:

  1. Mobile-originated messages -- An SMS sent by a handset to your number (short code, long code, or keyword). The short_message field contains the message text.
  2. Delivery receipts (DLRs) -- Status reports for previously submitted messages. The esm_class field is set to 0x04 to 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.

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:

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:

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:

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

  1. Use bind_transceiver for bidirectional communication on a single connection
  2. Implement enquire_link keepalives every 30-60 seconds
  3. Handle windowing with proper sequence number tracking
  4. Set data_coding correctly: 0 for GSM-7, 8 for UCS-2
  5. Request DLRs with registered_delivery = 1
  6. Parse DLRs using the receipted_message_id TLV when available
  7. Implement automatic reconnection with exponential backoff
  8. Handle ESME_RTHROTTLED with rate reduction
  9. Use TLS (port 2777) for production connections
  10. Test against SMPPSim before connecting to production
  11. Monitor connection health and PDU error rates in production
  12. 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.