Overview
SMPP is a telecoms-industry protocol, first published by Aldiscon/Logica in the late 1990s and now maintained by the SMPP developers’ forum. It defines how two peers exchange discrete units called PDUs (Protocol Data Units) over a single TCP connection: the application binds, submits messages, receives receipts, keeps the link alive, and unbinds. Because it is binary and connection-oriented, it carries steady, million-message-a-day workloads with far less overhead than opening an HTTP request per message. Read this page top to bottom and you will understand every field in a real SMPP session.
What SMPP is & where it fits
An SMPP session always has two roles. The ESME (External Short Messaging Entity) is your application — a bulk-SMS platform, an OTP service, an alerting system. The SMSC (Short Message Service Centre), called an MC (Message Centre) in v5.0, is the operator or aggregator node that store-and-forwards messages to and from handsets over the mobile network. The ESME connects to the SMSC; the SMSC connects to the wider SS7 / mobile core.
SMPP is the backbone of A2P (Application-to-Person) messaging — verification codes, receipts, alerts, marketing — as opposed to P2P (Person-to-Person) texting between two handsets. A2P traffic is high-volume, machine-generated and delivery-receipt driven, which is exactly what SMPP was designed to carry.
Why a binary TCP protocol instead of HTTP? A single SMPP bind is a long-lived socket. Once it is open you avoid a TCP and TLS handshake per message, you can have many messages in flight at once (asynchronous windowing), and the SMSC pushes delivery receipts and inbound messages straight back down the same connection. At sustained high throughput this is dramatically more efficient than a request/response HTTP API — which is why operators and large senders standardise on it.
Version history
- SMPP v3.3 — the first widely deployed version; introduced the core PDU set but predates TLVs (optional parameters) and the transceiver bind.
- SMPP v3.4 — the dominant version in production today. Added the transceiver bind, optional TLV parameters, message_payload, and data_sm. Almost every real-world integration you will build speaks v3.4.
- SMPP v5.0 — the newest version. Renames SMSC to MC, adds broadcast PDUs (broadcast_sm), congestion-state signalling and more standardised TLVs. Adoption is limited; support v3.4 first and treat v5.0 as opt-in.
Architecture & bind types
The topology is simple: the ESME opens a TCP connection to the SMSC/MC and authenticates with a system_id and password. The direction the session is allowed to carry traffic is fixed at bind time by choosing one of three bind modes. Each mode is a different bind PDU.
Submit only. The ESME can send submit_sm but cannot receive deliver_sm on this session. Receipts must come over a separate receiver bind.
Receive only. The SMSC delivers deliver_sm (inbound messages and DLRs) to the ESME, which cannot submit on this session.
Both directions over one session — submit_sm out, deliver_sm in. Introduced in v3.4 and the recommended default for new integrations.
A fourth actor is routing: the SMSC decides, per message, which downstream operator route or interconnect a submitted message takes toward the destination handset, and which bound ESME an inbound (mobile-originated) message belongs to.
On the wire, SMPP runs over TCP. The IANA-registered default port is 2775 for plain connections. SMPPS — SMPP wrapped in TLS — has no single standard port; providers expose it on a port of their choosing (3550, 8775 and 2776 are all seen in practice). Always use the encrypted port your provider assigns for production.
The session lifecycle
Every SMPP session follows the same arc: open TCP, bind, stay alive, exchange messages and receipts, then unbind and close. The diagram below traces a full transceiver session — each row is one PDU, blue flowing from the ESME to the SMSC, green flowing back.
Two important details. First, submit_sm_resp returns immediately and only means “accepted for delivery” — the real outcome arrives later as a deliver_sm receipt. Second, every request PDU has a matching _resp that echoes its sequence_number, which is how asynchronous responses are correlated back to requests.
PDU anatomy
Everything on an SMPP connection is a PDU. Every PDU begins with a fixed 16-byte header of four 4-byte (32-bit, big-endian) integers, followed by a command-specific body. Read the header first and you know exactly how to read the rest.
| Offset | Field | Size | Meaning |
|---|---|---|---|
| 0–3 | command_length | 4 bytes | Total length of the whole PDU in octets, header + body included. |
| 4–7 | command_id | 4 bytes | Which operation this is (e.g. submit_sm = 0x00000004). Responses set the top bit (0x80000000). |
| 8–11 | command_status | 4 bytes | 0 in requests; an ESME_R* result code in responses (0 = ESME_ROK = success). |
| 12–15 | sequence_number | 4 bytes | Caller-assigned 1…0x7FFFFFFF id; the matching _resp echoes it to correlate them. |
| 16… | body | variable | Mandatory parameters for this command, then any optional TLV parameters. |
The body ends with any number of TLVs — optional parameters encoded as Tag (2 bytes), Length (2 bytes), Value (Length bytes). TLVs are how v3.4 extended the protocol without breaking older peers: message_payload, receipted_message_id, sar_* concatenation fields and many more all ride as TLVs.
The sequence_number is the key to windowing. Because SMPP is asynchronous, an ESME can send many submit_sm PDUs — each with an incrementing sequence_number — before any responses arrive. When submit_sm_resp PDUs come back, their sequence_number tells you which submission each one answers. The number of unacknowledged PDUs allowed in flight is the window size.
Core PDUs reference
These are the operations you will actually use. Each request has a response PDU whose command_id is the request’s id with the high bit set (0x80000000), so submit_sm 0x00000004 pairs with submit_sm_resp 0x80000004.
| PDU | command_id | Purpose |
|---|---|---|
| bind_receiver | 0x00000001 | Open a receive-only session. |
| bind_transmitter | 0x00000002 | Open a submit-only session. |
| bind_transceiver | 0x00000009 | Open a bidirectional session (v3.4). |
| submit_sm | 0x00000004 | Submit a single message to one destination. |
| submit_multi | 0x00000021 | Submit one message to up to 254 destinations. |
| deliver_sm | 0x00000005 | SMSC → ESME: a delivery receipt or inbound (MO) message. |
| data_sm | 0x00000103 | TLV-only, bidirectional alternative to submit_sm/deliver_sm. |
| query_sm | 0x00000003 | Ask the SMSC for the current state of a submitted message. |
| cancel_sm | 0x00000008 | Cancel a previously submitted, not-yet-delivered message. |
| replace_sm | 0x00000007 | Replace the content/schedule of a pending message. |
| enquire_link | 0x00000015 | Keepalive — confirm the session is still alive. |
| unbind | 0x00000006 | Gracefully close the SMPP session. |
| generic_nack | 0x80000000 | Negative acknowledgement for an unparseable/invalid PDU. |
submit_sm field-by-field
submit_sm is the PDU you will send most. Its body is a sequence of mandatory parameters, then optional TLVs. These are the fields that matter in practice.
| Field | What it does |
|---|---|
| service_type | Optional SMS application category (e.g. blank, or a value your SMSC uses for routing/billing classes). |
| source_addr_ton / _npi | Type of Number and Numbering Plan Indicator for the sender. TON 5 = alphanumeric (a Sender ID); TON 1 = international E.164. |
| source_addr | The sender address — a short code, long number, or up to 11-char alphanumeric Sender ID. |
| dest_addr_ton / _npi | TON/NPI for the destination. Usually TON 1 (international), NPI 1 (ISDN/E.164). |
| destination_addr | The recipient MSISDN, normally in international format without a leading +. |
| esm_class | Messaging mode/type bitmask — e.g. request a delivery receipt is handled by registered_delivery, while the UDHI bit (0x40) says short_message begins with a UDH. |
| data_coding | The DCS: 0 = GSM 7-bit (default), 3 = Latin-1, 8 = UCS-2 (Unicode). Drives the character limit. |
| registered_delivery | Set to 1 to request a delivery receipt (deliver_sm) for this message; 0 for fire-and-forget. |
| sm_length / short_message | The payload length (0–254) and the message octets themselves, encoded per data_coding. |
| message_payload (TLV 0x0424) | Optional TLV carrying the message body instead of short_message — used for long content beyond 254 octets. |
| schedule_delivery_time / validity_period | Optional absolute/relative time to schedule delivery and how long the SMSC should keep retrying. |
Data coding & concatenation
The data_coding field (the Data Coding Scheme, DCS) tells the handset how to interpret the bytes, and it decides how many characters fit in one SMS.
| data_coding | Encoding | Single | Concatenated |
|---|---|---|---|
| 0 | GSM 7-bit default alphabet | 160 chars | 153 chars / part |
| 3 | Latin-1 (ISO-8859-1), 8-bit | 140 chars | 134 chars / part |
| 8 | UCS-2 (UTF-16BE) — Unicode | 70 chars | 67 chars / part |
A message longer than one segment is split and reassembled on the handset using a UDH (User Data Header) prepended to each part. The concatenation element uses IEI 0x00 (8-bit reference) — a 6-octet header carrying a reference number, the total part count and this part’s index. Those 6 octets are why the per-part limits drop to 153 (GSM 7-bit) and 67 (UCS-2). When you send a UDH inside short_message you must also set the UDHI bit (0x40) in esm_class.
There are two ways to send long content: build the UDH-segmented parts yourself (each a submit_sm with UDHI set), or hand the whole body to the SMSC in the message_payload TLV and let it segment. Some SMSCs instead accept the sar_msg_ref_num / sar_total_segments / sar_segment_seqnum TLVs to describe concatenation without a UDH.
Delivery receipts (DLR)
When you set registered_delivery = 1, the SMSC later reports the message’s fate by sending you a deliver_sm with the delivery-receipt bit set in esm_class (0x04). The receipt text lives in the short_message field as a well-known keyword string:
id:1a2b3c4d5e sub:001 dlvrd:001 submit date:2607251432 done date:2607251432 stat:DELIVRD err:000 text:Your code is 8The id matches the message_id you got back in submit_sm_resp (also echoed in the receipted_message_id TLV 0x001E). The stat field carries the final state:
| stat | Meaning |
|---|---|
| DELIVRD | Delivered to the handset. The success state. |
| EXPIRED | Validity period elapsed before delivery — the SMSC stopped retrying. |
| DELETED | The message was deleted at the SMSC. |
| UNDELIV | Undeliverable — no route or the handset could not be reached. |
| ACCEPTD | Accepted (e.g. manually read on the SMSC) — an intermediate state. |
| UNKNOWN | The SMSC has no state information for the message. |
| REJECTD | Rejected by the SMSC or a downstream network (e.g. blocked sender). |
Always answer a deliver_sm with a deliver_sm_resp; if you do not, the SMSC will re-send the receipt and may throttle or drop the bind. Note that receipt formatting is not perfectly standardised — providers vary slightly in field order and the err code map.
Error / command_status codes
Every response PDU carries a command_status in its header — an ESME_R* code. Zero means success; anything else describes why the request was refused. These are the ones you will meet most.
| Code | Value | Meaning |
|---|---|---|
| ESME_ROK | 0x00000000 | No error — success. |
| ESME_RINVMSGLEN | 0x00000001 | Invalid message length (sm_length / payload problem). |
| ESME_RINVCMDLEN | 0x00000002 | Invalid command_length — the PDU framing is wrong. |
| ESME_RINVCMDID | 0x00000003 | Unknown or unsupported command_id. |
| ESME_RINVBNDSTS | 0x00000004 | Command not valid in the current bind state (e.g. submit on an RX bind). |
| ESME_RALYBND | 0x00000005 | ESME is already bound. |
| ESME_RINVSRCADR | 0x0000000A | Invalid source address (Sender ID). |
| ESME_RINVDSTADR | 0x0000000B | Invalid destination address. |
| ESME_RBINDFAIL | 0x0000000D | Bind failed — bad system_id / password. |
| ESME_RINVPASWD | 0x0000000E | Invalid password. |
| ESME_RTHROTTLED | 0x00000058 | Throttling error — you exceeded the allowed message rate. |
| ESME_RSUBMITFAIL | 0x00000045 | submit_sm / submit_multi failed. |
| ESME_RSYSERR | 0x00000008 | SMSC system error. |
The full registry — including provider-specific vendor ranges — is searchable in the SMPP error-code reference.
Throughput, windowing & throttling
Throughput in SMPP is measured in TPS — transactions (submit_sm) per second — and it is governed by two things: the rate the SMSC permits, and how many messages you keep in flight. Because SMPP is asynchronous you do not have to wait for each submit_sm_resp before sending the next PDU. The number of unacknowledged submissions you allow at once is your window size (commonly 10–100). A larger window hides round-trip latency and raises effective TPS — up to the SMSC’s ceiling.
- Respect the throttle. When the SMSC returns ESME_RTHROTTLED (0x58), back off — pause and retry with exponential backoff rather than hammering the bind, which can get you disconnected.
- Keepalive on a cadence. Send enquire_link roughly every 30 seconds while idle; if a few go unanswered, assume the link is dead.
- Reconnect deliberately. On disconnect, reconnect and re-bind with backoff and jitter; do not tight-loop. Track outstanding sequence_numbers so you can reconcile receipts after a reconnect.
- Scale with binds, carefully. Higher aggregate TPS usually comes from multiple binds — but only up to the limit your provider authorises per account.
Code sample
An illustrative connect → bind → submit flow in Node-style pseudocode. Real SMPP libraries (for example node-smpp, jSMPP for Java, or python-smpplib) handle PDU encoding for you; this shows the shape of the session, not a specific library’s API.
import smpp from "smpp";
// 1. Open a TCP (or TLS) session to the SMSC.
const session = smpp.connect({
// Host, port and bind credentials come with your test access.
// Plain SMPP conventionally uses 2775; the TLS port is provided with your account.
url: process.env.SMPP_URL!, // e.g. smpp://<host>:2775 (plain) or smpps://<host>:<tls-port>
auto_enquire_link_period: 30000, // keepalive every 30s
});
// 2. Bind as a transceiver.
session.bind_transceiver(
{
system_id: "YOUR_SYSTEM_ID",
password: "YOUR_PASSWORD",
system_type: "",
},
(pdu) => {
if (pdu.command_status !== 0) {
throw new Error("bind failed: 0x" + pdu.command_status.toString(16));
}
// 3. Submit a Unicode message, asking for a delivery receipt.
session.submit_sm(
{
source_addr_ton: 5, // alphanumeric Sender ID
source_addr: "BroadNet",
dest_addr_ton: 1, // international E.164
destination_addr: "447700900123",
registered_delivery: 1, // request a DLR
data_coding: 8, // UCS-2 (Unicode)
short_message: "Your code is 8123",
},
(submitResp) => {
// submit_sm_resp returns the SMSC message_id.
console.log("accepted, message_id =", submitResp.message_id);
}
);
}
);
// 4. Receive delivery receipts on the same session.
session.on("deliver_sm", (pdu) => {
if ((pdu.esm_class & 0x04) === 0x04) { // delivery-receipt bit
console.log("DLR:", pdu.short_message.message);
}
session.send(pdu.response()); // ALWAYS deliver_sm_resp
});BroadNet SMPP
BroadNet operates a carrier-grade SMSC gateway that accepts SMPP v3.4 binds over TLS. You get transceiver binds, high TPS with async windowing, full delivery receipts, Unicode and concatenation, and an IP-allowlisted, credentialled connection to reach operators worldwide. It is the same protocol taught on this page — pointed at real routes.
Request a bind for your account and our sales engineering team will provision your system_id, TLS endpoint, TPS and IP allowlist. Or read how the gateway is built.
Frequently asked questions
What port does SMPP use?
The IANA-registered default is TCP port 2775 for plain SMPP. There is no single standard TLS port — SMPPS (SMPP over TLS) commonly runs on a provider-specific port such as 3550, 8775 or 2776, so always use the port your SMSC operator assigns. BroadNet publishes its bind host, plain port and TLS port with your credentials.
What is the difference between submit_sm and data_sm?
submit_sm is the primary PDU an ESME uses to submit a single short message; it carries the classic short_message field (up to 254 octets) plus optional TLVs. data_sm is a streamlined, symmetric alternative introduced in SMPP v3.4 that always carries the content in the message_payload TLV and can flow in either direction (ESME→SMSC or SMSC→ESME). Most A2P traffic uses submit_sm; data_sm is used where its TLV-only, bidirectional shape is preferred.
How does SMPP handle Unicode?
Unicode is sent by setting data_coding to 8 (UCS-2 / UTF-16BE big-endian). A single UCS-2 segment holds 70 characters; concatenated UCS-2 parts hold 67 characters each because 6 octets are consumed by the UDH concatenation header. GSM 7-bit (data_coding 0) is the default for basic Latin text at 160/153 characters.
What is a transceiver bind?
A transceiver (TRX) bind is a single SMPP session, opened with bind_transceiver, that can both submit messages (submit_sm) and receive messages and delivery receipts (deliver_sm) over the same TCP connection. It replaces the older pattern of opening a separate transmitter bind and receiver bind, and is the recommended mode for most modern integrations.
Why use SMPP over an HTTP API?
SMPP is a persistent, binary TCP protocol built for sustained high throughput. A long-lived bind avoids per-message TLS/TCP handshakes, supports asynchronous windowing (many outstanding submit_sm before an ack), and delivers real-time deliver_sm receipts and mobile-originated messages back over the same socket. For very high, steady A2P volumes it is more efficient than request/response HTTP; HTTP REST is simpler for lower volumes and richer channels.
What is enquire_link?
enquire_link is a lightweight keepalive PDU that either peer sends to confirm the SMPP session and underlying TCP connection are still alive; the other side answers with enquire_link_resp. It is typically sent every 30 seconds during idle periods. If several enquire_links go unanswered the peer treats the link as dead and reconnects.
What does command_length in a PDU header mean?
command_length is the first 4-byte field of every SMPP PDU header and holds the total length of the entire PDU in octets, including the 16-byte header itself and the whole body. A receiver reads these 4 bytes first to know exactly how many more bytes to read for the complete PDU before parsing command_id, command_status and sequence_number.
What is a delivery receipt (DLR) in SMPP?
A delivery receipt is the SMSC telling the ESME the final fate of a submitted message. It arrives as a deliver_sm PDU with the esm_class delivery-receipt bit set (0x04). The short_message contains a keyword receipt string — id, sub, dlvrd, submit date, done date, stat and err — where stat carries the final state such as DELIVRD, EXPIRED, UNDELIV or REJECTD.