Home / Docs / SMPP Gateway
Developer Guide · SMPP

SMPP gateway for high-volume SMS

When you need to push 100k+ SMS per day with tight latency — OTPs, transactional alerts, mass campaigns — REST is the wrong protocol. MsgHub exposes a fully-featured SMPP 3.4 gateway. Persistent TCP, sliding-window submit, real-time DLR, India DLT-compliant. This guide covers everything from bind types to throughput tuning to working code in Java, Python and Node.

SMPP 3.4 Persistent TCP DLT-compliant Last updated: 2026-05-14

Overview

SMPP (Short Message Peer-to-Peer) is the binary protocol that runs underneath every Indian SMS aggregator and telco. Instead of one HTTP request per SMS, you open a single TCP connection, bind once, and stream submit_sm PDUs. The gateway responds with submit_sm_resp and pushes back deliver_sm PDUs for delivery receipts. Latency drops, throughput multiplies, and you can handle backpressure properly.

MsgHub's SMPP layer is built on Jasmin. It sits between your account on MsgHub and your DLT-registered SMS vendor. There are two ways SMPP shows up in the platform:

This guide covers both flows. Most readers will care about Flow A. Flow B is for resellers / agency tenants.

When to use SMPP vs REST

Use caseRecommended
OTP / 2FA at > 50/sec sustainedSMPP
Transactional alerts < 50/secREST
Marketing broadcasts (50k+ recipients)SMPP (or REST batch)
One-off transactional with rich payloadREST (simpler)
Need delivery receipt within 2 secondsSMPP (push) vs REST + webhook (push too — equivalent)
Mobile / web app with no backendREST

Rule of thumb: if your SMS load can be served by a single thread of REST calls, use REST. If you're already worrying about HTTP keep-alive and concurrent connections, switch to SMPP.

Prerequisites

Flow A · Configure vendor SMPP (most readers)

In MsgHub, open Settings (bottom of sidebar) → scroll to the SMPP (High-Volume SMS) section.

  1. Toggle Enable SMPP on. ("Route SMS campaigns through SMPP instead of the HTTP vendor API.")
  2. Fill the fields from your vendor's dashboard:
    • Vendor SMPP Host (e.g. smpp.kaleyra.com)
    • Port (typically 2775)
    • System ID (your login)
    • Password
  3. Set the TPS Target slider — 10 to 1000 TPS. MsgHub provisions one Jasmin connector per 100 TPS automatically.
  4. Set Jasmin User Password — used internally by Jasmin to authenticate the per-tenant user named t-{your-tenant-id}.
  5. Click Save SMPP Settings.
  6. Click Apply to Jasmin — this provisions the Jasmin connector on the backend. Wait for the success confirmation.

After this, all SMS dispatched from your account — REST API, dashboard sends, campaigns, chatbot flows — route through Jasmin to your vendor over SMPP. Nothing changes from the API caller's perspective.

You're done if you only need Flow A. Skip to the "Bind types", "submit_sm", "encoding" sections below if you want the protocol-level details — they describe what Jasmin sends to your vendor on your behalf.

Flow B · Expose SMPP to your enterprise clients

This is for tenants who resell SMS access. After completing Flow A, MsgHub also exposes its own SMPP server (Jasmin's port 2775) to enterprise clients. Each client gets their own bind credentials managed via the SMPP Accounts page in the sidebar.

Create an enterprise client account

  1. Sidebar → SMPP Accounts+ New Account.
  2. Fill:
    • Account Label — friendly name for tracking
    • Password — what the client uses to bind
    • Max Bindings — concurrent connections allowed (default 2)
    • TPS Limit — per-client throughput cap
    • Allowed Sender IDs — whitelist of headers this client may use
    • Enabled — toggle
  3. Save.

Connection details to share with the client

After Jasmin is provisioned (Flow A above), Settings shows an Enterprise SMPP Server URL. Copy it. Share with the client:

SMPP HostShown as Enterprise SMPP Server URL in your tenant's Settings — depends on your MsgHub deployment
SMPP Port2775
SMPP version3.4 (interface_version = 0x34)
System IDThe account label / username from the SMPP Accounts page
PasswordThe password you set when creating the account
Bind TypeTransceiver (recommended) or Transmitter

SMPP port 2775 is plaintext. If your enterprise clients aren't on the same private network as the MsgHub deployment, run the SMPP connection through a TLS tunnel (stunnel, ssh -L) or a VPN. There is no native TLS port currently.

Bind types

SMPP defines three bind types. Pick based on what you need to do:

TypeSendsReceivesUse when
Transmitter (TX) Send only — DLR via webhook instead of SMPP
Receiver (RX) DLR-only or inbound-SMS-only listener
Transceiver (TRX) Default for most apps. Single connection does both.

TRX is what 90% of production setups use. The other two exist for cases where you want different processes handling send and receive (e.g. sender pod and DLR-consumer pod scaled independently).

Sending: submit_sm PDU

The workhorse PDU is submit_sm. Key fields:

FieldNotes
source_addrYour DLT-registered Header (6 chars, e.g. MSGHUB)
destination_addrRecipient phone in international format without +, e.g. 919876543210
source_addr_ton5 (alphanumeric) for Header sender
source_addr_npi0
dest_addr_ton1 (international)
dest_addr_npi1 (E.164)
short_messageUp to 140 bytes (GSM 7-bit ~ 160 chars / UCS-2 ~ 70 chars)
data_coding0 = GSM 7-bit, 8 = UCS-2 (Unicode)
registered_delivery1 to request DLR

DLT-specific TLVs

For DLT compliance, include the approved Template ID in an optional TLV parameter:

0x1402 · entity_id (PE ID)Optional; defaults to tenant's primary PE
0x1403 · template_idRequired — your DLT-approved 18-19 digit template ID

MsgHub validates these on every submit. Wrong / missing template_id → submit_sm_resp with status 0xFF (custom: DLT template mismatch).

Encoding & message length

SMS is constrained to 140 bytes per PDU. That maps to different character counts depending on encoding:

data_codingEncodingSingle SMSPer-segment in concat
0GSM 7-bit (default)160 chars153 chars
8UCS-2 (Hindi, Gujarati, emoji)70 chars67 chars

Pure-ASCII message containing a single non-GSM char (e.g. ' typographic apostrophe instead of ') drops you to UCS-2 — and from 160 to 70 chars. Watch for smart-quote substitution in templates copied from Word. Strip or sanitise before submit.

Concatenated (multi-part) SMS

Messages longer than the single-SMS limit must be split into segments. Two standard methods:

Method A · UDH (User Data Header)

Set esm_class = 0x40 and prepend each segment with a 6-byte UDH:

# UDH layout for concatenation
05 00 03 {ref_num} {total_parts} {part_number}
#  │  │  │      │            │             └─ 1-indexed (1, 2, 3...)
#  │  │  │      │            └──────────────── total segment count
#  │  │  │      └─────────────────────────────── arbitrary ID, same for all segments of one message
#  │  │  └────────────────────────────────────── IEI length (3)
#  │  └───────────────────────────────────────── IEI: concatenated SMS, 8-bit ref
#  └──────────────────────────────────────────── UDH length (5)

Method B · message_payload TLV (recommended)

Use the 0x0424 TLV to send up to 1024 bytes in a single PDU. The gateway handles UDH segmentation downstream. Simpler client code, identical result on the handset.

Most SMPP libraries do this for you automatically — pass the full text, the library splits + UDH-tags appropriately.

Delivery receipts (DLR)

When registered_delivery = 1 on submit_sm, MsgHub pushes a deliver_sm PDU back when the telco confirms delivery (or failure).

DLR short_message follows the standard format:

id:a3b8c9 sub:001 dlvrd:001
submit date:2605141032 done date:2605141033
stat:DELIVRD err:000 text:first 20 chars of message

Key fields:

Respond with deliver_sm_resp within 30 seconds or MsgHub considers the DLR unacknowledged and may retry.

Throughput tuning

Window size

SMPP sliding window controls how many in-flight submits can be unacknowledged. Default: 10. Tune up to 100 if your latency requirements are tight and your client handles ordering properly.

# jSMPP example
session.setMaxUnboundedTime(60000);
session.setMaxUnboundedTime(60000);
session.setEnquireLinkTimer(30000);
session.setTransactionTimer(60000);
session.setSubmitTimer(60000);
session.setMaxUnboundedTime(60000);
# Set window size via session.getConnection().setRequestTimeout(...)

Multiple binds

One bind = ~100 SMS/sec under load. Need more? Open more binds. MsgHub allows up to 10 simultaneous TRX binds per system_id (higher on request). Distribute submits across them (round-robin or sticky-by-route).

Enquire_link

Send enquire_link every 30 seconds to keep the connection alive through NAT / firewall idle timeouts. Most libraries do this for you — just configure the interval.

Reconnect strategy

TCP connections die. Plan for it.

  1. Catch unbind / disconnect events.
  2. Wait with exponential backoff: 1s → 2s → 5s → 15s → 60s → cap at 5 min.
  3. Reconnect, rebind, resubmit anything that was in-flight (use unique IDs to dedupe on gateway side).
  4. Don't reconnect tighter than 1s — you'll get auth-rate-limited.

For HA, run two binds from different boxes and route by primary/secondary. When primary dies, route all new submits through secondary while primary reconnects. This pattern gives sub-second failover compared to ~5–10s of pure reconnect-driven downtime.

Code examples

Java · jSMPP

import org.jsmpp.bean.*;
import org.jsmpp.session.*;

SMPPSession session = new SMPPSession();
String systemId = System.getenv("MSGHUB_SMPP_SYSTEM_ID");
String password = System.getenv("MSGHUB_SMPP_PASSWORD");

// MSGHUB_SMPP_HOST comes from your tenant's Settings → SMPP → Enterprise SMPP Server URL
session.connectAndBind(
    System.getenv("MSGHUB_SMPP_HOST"), 2775,
    new BindParameter(
        BindType.BIND_TRX,
        systemId, password,
        "",                 // system_type
        TypeOfNumber.UNKNOWN,
        NumberingPlanIndicator.UNKNOWN,
        null             // address range
    )
);

// Listen for DLRs
session.setMessageReceiverListener(new MessageReceiverListener() {
    public void onAcceptDeliverSm(DeliverSm dlr) throws ProcessRequestException {
        DeliveryReceipt r = dlr.getShortMessageAsDeliveryReceipt();
        System.out.println("DLR for " + r.getId() + ": " + r.getFinalStatus());
    }
    // alertNotification, onAcceptDataSm omitted
});

// Send
OptionalParameter templateId = new OptionalParameter.OctetString(
    (short) 0x1403,
    "1707XXXXXXXXXXXXXX".getBytes()
);

String messageId = session.submitShortMessage(
    "",                                // service_type
    TypeOfNumber.ALPHANUMERIC,
    NumberingPlanIndicator.UNKNOWN,
    "MSGHUB",                          // source = DLT header
    TypeOfNumber.INTERNATIONAL,
    NumberingPlanIndicator.ISDN,
    "919876543210",                    // destination
    new ESMClass(),
    (byte) 0,                          // protocol_id
    (byte) 0,                          // priority
    "",                                // schedule_delivery
    "",                                // validity_period
    new RegisteredDelivery(SMSCDeliveryReceipt.SUCCESS_FAILURE),
    (byte) 0,                          // replace_if_present
    new GeneralDataCoding(Alphabet.ALPHA_DEFAULT),
    (byte) 0,                          // sm_default_msg_id
    "Your OTP is 482910. Valid for 5 minutes.".getBytes(),
    templateId
);
System.out.println("submitted: " + messageId);

Python · smpplib

import os, smpplib.client, smpplib.consts, smpplib.gsm

# Host comes from your tenant's Settings → SMPP → Enterprise SMPP Server URL
client = smpplib.client.Client(os.environ["MSGHUB_SMPP_HOST"], 2775)

# DLR handler
def on_deliver(pdu):
    print("DLR:", pdu.short_message.decode())

client.set_message_received_handler(on_deliver)

client.connect()
client.bind_transceiver(
    system_id=os.environ["MSGHUB_SMPP_SYSTEM_ID"],
    password=os.environ["MSGHUB_SMPP_PASSWORD"],
)

parts, encoding_flag, msg_type_flag = smpplib.gsm.make_parts(
    "Your OTP is 482910. Valid for 5 minutes."
)

for part in parts:
    pdu = client.send_message(
        source_addr_ton=smpplib.consts.SMPP_TON_ALNUM,
        source_addr="MSGHUB",
        dest_addr_ton=smpplib.consts.SMPP_TON_INTL,
        destination_addr="919876543210",
        short_message=part,
        data_coding=encoding_flag,
        esm_class=msg_type_flag,
        registered_delivery=True,
        # DLT template_id TLV
        optional_parameters={0x1403: b"1707XXXXXXXXXXXXXX"},
    )

client.listen()  # blocks; runs DLR handler

Node.js · smpp package

const smpp = require('smpp');

// Host comes from your tenant's Settings → SMPP → Enterprise SMPP Server URL
const session = smpp.connect({
  url: `smpp://${process.env.MSGHUB_SMPP_HOST}:2775`,
  auto_enquire_link_period: 30000,
});

session.bind_transceiver({
  system_id: process.env.MSGHUB_SMPP_SYSTEM_ID,
  password: process.env.MSGHUB_SMPP_PASSWORD,
}, (pdu) => {
  if (pdu.command_status !== 0) {
    console.error('Bind failed:', pdu.command_status);
    return;
  }
  console.log('Bound. Sending...');

  session.submit_sm({
    source_addr:      'MSGHUB',
    source_addr_ton:  5,
    destination_addr: '919876543210',
    dest_addr_ton:    1,
    dest_addr_npi:    1,
    short_message:    'Your OTP is 482910. Valid 5 min.',
    registered_delivery: 1,
    data_coding:      0,
    // DLT TLVs
    '0x1403': Buffer.from('1707XXXXXXXXXXXXXX'),
  }, (resp) => {
    console.log('submit_sm_resp:', resp.message_id);
  });
});

// DLR listener
session.on('deliver_sm', (pdu) => {
  console.log('DLR:', pdu.short_message.message);
  session.deliver_sm_resp({ sequence_number: pdu.sequence_number });
});

SMPP error codes (command_status)

CodeNameMeaning
0x000ESME_ROKSuccess
0x00DESME_RBINDFAILWrong system_id / password
0x00EESME_RINVPASWDInvalid password
0x00FESME_RINVSYSIDInvalid system_id
0x033ESME_RBINDFAILAlready bound — your client opened too many sessions
0x058ESME_RTHROTTLEDYou exceeded the throughput cap. Slow down or open more binds.
0x065ESME_RINVSCHEDInvalid schedule_delivery_time
0x0FF(custom: DLT template mismatch)Template ID missing or doesn't match content

Troubleshooting

"Bind failed" with ESME_RINVPASWD

submit_sm_resp always returns 0xFF

Connection drops every ~5 minutes

DLRs never arrive

Throughput plateau at ~30 SMS/sec

FAQ

Should I use SMPP or REST?

REST is fine up to ~50 SMS/sec per tenant. Beyond that — OTP-scale workloads sending 100k to millions per day with tight latency — SMPP shines because the persistent connection eliminates HTTP overhead. Rule of thumb: REST for transactional, SMPP for high-volume OTP.

What SMPP version does MsgHub support?

SMPP 3.4 (via Jasmin). SMPP 3.3 clients work in compatibility mode. SMPP 5.0 is not supported.

What's the max throughput?

The TPS Target slider in Settings → SMPP goes up to 1,000 TPS per tenant. MsgHub provisions one Jasmin connector per 100 TPS, so 1,000 TPS means 10 connectors fan-out to your vendor. Your vendor's contract typically has its own TPS cap — your effective max is the lower of the two.

Why am I getting ESME_RTHROTTLED on a client bind (Flow B)?

The client exceeded the TPS Limit set on their SMPP Account. Either raise the limit for that client in the SMPP Accounts page, or have the client slow their submit rate. MsgHub doesn't retry the dropped submit — back-pressure is the signal.

Can I send Hindi / Gujarati / regional language SMS?

Yes. Use data_coding = 8 (UCS-2). Note that UCS-2 caps the message at 70 characters per segment (vs 160 for GSM 7-bit), and most Indian aggregators charge per-segment.

How do I send a flash SMS (Class 0)?

Set data_coding low nibble bit 0 — i.e. data_coding = 0x10 for GSM 7-bit Class 0. Not supported on all Indian operators; test before relying on it.

Can I use SMPP for inbound (two-way) SMS?

Yes if your sender Header supports two-way (short codes in particular, some long codes). Inbound MO messages arrive as deliver_sm PDUs without DLR fields — message text is in short_message. Bind as RX or TRX to receive.

What if my vendor's SMPP support is different?

The fields in Settings → SMPP (host, port, system ID, password, TPS) cover the standard SMPP 3.4 bind parameters. If your vendor needs non-standard auth (signed bind, mTLS, IP whitelisting) contact us — Jasmin's connector layer can usually accommodate, but it needs a one-time wiring per vendor.

What's next

DLT registration (prerequisite)
SMPP submits without an approved DLT template will be rejected. Start at DLT setup if you haven't already.
REST API as a fallback
For low-volume transactional sends — or when SMPP is overkill — the REST API covers the same ground.
Need help wiring a specific vendor?
If your aggregator's SMPP differs from standard 3.4 (custom auth, mTLS, non-standard TLVs) we'll add a connector for you.