SMS over SMPP#

Use OpenSIPS 4.0 with a DIDWW SMPP ESME trunk to send and receive SMS. Relay SIP MESSAGE requests from a user agent as outbound SMS, and deliver incoming SMS and delivery receipts to the user agent as SIP MESSAGE requests.

OpenSIPS binds to the DIDWW SMSC as an SMPP client using the proto_smpp module. All required modules are part of a stock OpenSIPS installation, and no database server is required. For inbound and outbound calling, see Voice over SIP.

Important

OpenSIPS relays messages and does not create them. A user agent that sends and receives SIP MESSAGE requests is required. Part 3 uses Asterisk as an example.


1. Configure SMPP ESME trunk#

Configure an SMPP ESME trunk in the DIDWW User Panel so that OpenSIPS can bind to the DIDWW SMSC.

Before you begin#

  • An active DIDWW account is required. Sign in to DIDWW or create a DIDWW account.

  • To send SMS, a sender is required. Use a DID number with Outbound P2P SMS enabled for P2P messaging, or a verified sender ID for A2P messaging. See Sender ID Verifications.

  • To receive SMS, a DID number that supports inbound SMS is required. If you do not have one, see How to buy numbers.

  • The OpenSIPS server must be able to open a TCP connection to the DIDWW SMSC on port 2775, or on port 2776 for TLS.

Step 1: Create New SMPP ESME trunk#

  1. In the DIDWW User Panel, go to SMS > SMS Trunks.

  2. Click Create New > SMPP ESME.

Creating a new SMPP ESME trunk from the Create New menu

Creating a new SMPP ESME trunk.#

Step 2: Configure general trunk settings#

In the Create SMPP ESME Trunk form:

  1. Enter a Friendly name for the trunk (e.g., OpenSIPS).

  2. Enter a System type (e.g., OpenSIPS). The db_text table requires a non-empty value. See Define the SMSC.

  3. Select the Server location: US, NY or NL, AMS. It determines the host OpenSIPS connects to.

  4. Optionally, enter the public IP address of the OpenSIPS server in Allowed IP addresses. If the field is empty, any address can connect with the trunk credentials.

  5. Under Source Address Settings, keep Allow any DID(s) for SMS OUT enabled, or turn it off and select the DIDs allowed as senders.

SMPP ESME trunk settings with friendly name, system type, server location, and source address settings

SMPP ESME trunk settings.#

Step 3: Click Create and save the SMPP ESME trunk#

When all required fields in the Create SMPP ESME Trunk form are filled, click Create to save the trunk. The trunk appears in the SMS Trunks list.

Create button in the Create SMPP ESME Trunk form

Saving the SMPP ESME trunk.#

Step 4: Copy the trunk credentials#

  1. In the SMS Trunks list, click the key icon in the Credentials column of the trunk.

  2. Copy the System ID, the Password (click the eye icon to show it), and the Host. You will use them to define the SMSC.

Note

OpenSIPS truncates a password longer than 9 characters and a System ID longer than 16 characters, and logs this once at startup. Check the length of the credentials before you configure them.

SMPP ESME trunk credentials showing the System ID, password, and host

SMPP ESME trunk credentials.#

Step 5: Assign the trunk to your DID numbers#

This step is required only to receive SMS.

  1. In the DIDWW User Panel, go to Phone Numbers > My Numbers.

  2. In the Trunk column of the DID, click the SMS: entry and select the trunk created in Step 3.

  3. Click Confirm.

To assign the trunk to several DIDs at once, see Assign an SMS trunk.

Assigning the SMPP ESME trunk to a DID number in My Numbers

Assigning the SMS trunk to a DID.#


2. Configure OpenSIPS#

Configure OpenSIPS to bind to the DIDWW SMSC and relay messages between DIDWW and one user agent. The examples use /etc/opensips/opensips.cfg for the script and /etc/opensips/dbtext/ for the db_text tables. Replace the uppercase placeholders with the values for your deployment.

Before you begin#

  • OpenSIPS 4.0 is required. The proto_smpp and db_text modules used in this part are included in the opensips package.

  • Have the System ID, Password, Host, and System type from Part 1 at hand.

Step 1: Configure the OpenSIPS script#

The following blocks build /etc/opensips/opensips.cfg in order. Back up the current file, then replace its contents with the blocks below.

1.1 Configure the sockets and the connect timeout#

Begin the file with the global parameters:

log_level=3
stderror_enabled=yes
syslog_enabled=no
udp_workers=2
tcp_workers=2
tcp_connect_timeout=2000

socket=udp:0.0.0.0:5060
socket=smpp:OPENSIPS_IP:2775

The UDP socket receives MESSAGE requests from the user agent. The smpp socket is required. proto_smpp does not start without one, because the connection to the SMSC is opened from it. Set OPENSIPS_IP to an IP address of the OpenSIPS server, not the DIDWW host. The DIDWW host is set when you define the SMSC.

Important

tcp_connect_timeout defaults to 100 ms, which can be shorter than the round-trip time to the DIDWW SMSC. The connection then fails with connect timed out in the log, and OpenSIPS keeps retrying.

1.2 Load the required modules#

Add the module block below the global parameters:

mpath="/usr/lib/x86_64-linux-gnu/opensips/modules/"

loadmodule "proto_udp.so"
loadmodule "signaling.so"
loadmodule "sl.so"
loadmodule "tm.so"
loadmodule "maxfwd.so"
loadmodule "sipmsgops.so"

loadmodule "db_text.so"
modparam("db_text", "db_mode", 0)

loadmodule "proto_smpp.so"
modparam("proto_smpp", "db_url", "text:///etc/opensips/dbtext")
modparam("proto_smpp", "outbound_uri", "sip:USER_AGENT_IP:5060")

mpath is the module directory of an x86-64 build. Other architectures use their own directory name.

db_url points to the directory with the db_text tables from Define the SMSC. outbound_uri is the address of the user agent. OpenSIPS sends every incoming SMS and delivery receipt there as a SIP MESSAGE, without running the routing script. proto_smpp requires tm.

1.3 Define the SMSC#

proto_smpp reads the SMSC from a table named smpp. With db_text, the table is a file in the db_url directory, next to a version file. Create both.

/etc/opensips/dbtext/version:

table_name(str) table_version(int)
smpp:1

/etc/opensips/dbtext/smpp:

id(int,auto) name(string) ip(string) port(int) system_id(string) password(string) system_type(string) src_ton(int) src_npi(int) dst_ton(int) dst_npi(int) session_type(int)
1:DIDWW:SMSC_IP:2775:SYSTEM_ID:PASSWORD:SYSTEM_TYPE:1:1:1:1:1

Column

Value

name

DIDWW, the name used in outbound messages.

ip

The IP address of the Host from Step 4. A hostname is not accepted. Resolve it first, for example with dig +short us.sms-out.didww.com.

port

2775

system_id, password

The System ID and Password from Step 4.

system_type

The System type from Step 2.

src_ton, src_npi, dst_ton, dst_npi

1, 1, 1, 1: international numbers, E.164 numbering plan.

session_type

1: transceiver. One connection sends and receives.

Every field must be non-empty. db_text reads an empty field as NULL and rejects the table. A : inside a value must be escaped as \:.

Note

The table is read at startup. Restart OpenSIPS after changing it.

1.4 Configure outbound messages#

Add the routing logic below the module block:

route {

    if (!mf_process_maxfwd_header(10)) {
        send_reply(483, "Too Many Hops");
        exit;
    }

    if (is_method("OPTIONS")) {
        send_reply(200, "OK");
        exit;
    }

    if (!is_method("MESSAGE")) {
        send_reply(405, "Method Not Allowed");
        exit;
    }

    if ($si != "USER_AGENT_IP") {
        send_reply(403, "Forbidden");
        exit;
    }

    xlog("L_INFO", "SMS out: from=$fU to=$rU callid=$ci\n");

    $var(utf16) = 0;
    if ($hdr(X-SMS-Encoding) =~ "UTF-16")
        $var(utf16) = 1;

    if (send_smpp_message("DIDWW", $fU, $rU, $rb, $var(utf16), 1)) {
        send_reply(202, "Accepted");
    } else {
        xlog("L_ERR", "send_smpp_message failed rc=$rc callid=$ci\n");
        send_reply(500, "SMPP send failed");
    }
    exit;
}

The route accepts MESSAGE requests from the user agent only. send_smpp_message() sends each one to the SMSC named DIDWW in Define the SMSC:

  • The sender is the user part of the From ($fU). It must be a DID with Outbound P2P SMS or a verified sender ID. Enter a DID in international format without +.

  • The recipient is the user part of the Request-URI ($rU), in the same format.

  • The text is the message body ($rb).

  • The last argument, 1, requests a delivery receipt.

A positive return value means the message was sent to the SMSC, not that it was accepted. The SMSC’s answer appears only in the log: Successfully sent message "<id>", or Error in submit_sm_resp <code> with an SMPP status code.

Note

OpenSIPS does not convert text between encodings. To send non-Latin text, the user agent sends the body as UTF-16BE written in hexadecimal digits and adds the header X-SMS-Encoding: UTF-16. The script then passes 1 as the UTF-16 argument. See Send a message.

1.5 Configure inbound messages#

Inbound SMS and delivery receipts need no routing logic. For each one, proto_smpp builds a SIP MESSAGE and sends it to the outbound_uri from the module settings, without running the script:

  • The user part of the Request-URI is the receiving DID.

  • The user part of the From is the sender’s number.

  • The body is the text in UTF-8, with Content-Type: text/plain. A UCS-2 message arrives as UTF-16 in hexadecimal digits, with Content-Type: text/plain; charset=UTF-16.

A delivery receipt arrives the same way, with the receipt as the body:

id:00000000-0000-0000-0000-000000000000 sub:001 dlvrd:001 submit date:2609161429 done date:2609161429 stat:DELIVRD err:0 text:-

Important

stat:DELIVRD is the SMSC’s report. The destination network can still drop the message without notice, for example when its content filter treats the text as spam.

Step 2: Load the configuration#

Check that the configuration parses:

opensips -C -f /etc/opensips/opensips.cfg

Important

config file ok, exiting... confirms only that the script parses. The check does not read the smpp table or connect to the SMSC. Start OpenSIPS and review the startup output as well.

Start OpenSIPS in the foreground:

opensips -F -f /etc/opensips/opensips.cfg

Leave OpenSIPS running. To encrypt the SMPP connection, continue with Step 3. Otherwise, go to Step 4.

Step 3: Carry the SMPP connection over TLS (optional)#

proto_smpp has no TLS support. To encrypt the connection, run a TLS client such as stunnel on the OpenSIPS server. OpenSIPS connects to stunnel on the loopback interface, and stunnel connects to the DIDWW SMSC on port 2776.

  1. Create /etc/stunnel/smpp-didww.conf:

    [smpp-didww]
    client = yes
    accept = 127.0.0.1:2775
    connect = SMSC_IP:2776
    verifyChain = yes
    CAfile = /etc/ssl/certs/ca-certificates.crt
    checkHost = sms-out.didww.com
    sni = sms-out.didww.com
    

    SMSC_IP is the address from Define the SMSC. CAfile is the system CA bundle on Debian.

  2. Start stunnel.

  3. In the smpp table, replace SMSC_IP with 127.0.0.1, then restart OpenSIPS.

Important

Keep checkHost set to sms-out.didww.com for every Host. The certificate on port 2776 is issued for this name only, so verification against us.sms-out.didww.com or eu.sms-out.didww.com fails.

Step 4: Verify the configuration#

Check

Action

Expected result

SMPP socket

Review the startup output.

A Listening on block includes smpp: OPENSIPS_IP [OPENSIPS_IP]:2775.

SMSC table

Review the startup output.

Loaded 1 SMSc servers

Bind

Review the startup output.

Successfully bound transceiver within a few seconds.

Bind status in DIDWW

In SMS > SMS Trunks, open the trunk’s actions menu and select Check Status.

Connected

Outbound SMS

Send a MESSAGE from the user agent. See Part 3.

The SMS out line from outbound messages, then Successfully sent message. The message appears with the status Success in Logs & Analytics > SMS Logs, outbound.

Inbound SMS

Send an SMS to a DID assigned in Step 5.

The user agent receives a MESSAGE with the DID in the Request-URI.

With TLS, the stunnel log also shows Certificate accepted at depth=0: CN=sms-out.didww.com.

Common issues#

connect timed out repeats in the log and the bind never succeeds

tcp_connect_timeout is too low. See the sockets and the connect timeout.

OpenSIPS exits with at least one listener is mandatory for using the SMPP module

The smpp socket is missing. See the sockets and the connect timeout.

OpenSIPS exits with missing modparam: ‘smpp_outbound_uri’

outbound_uri is not set. The error uses a different name for the same parameter. See the module settings.

OpenSIPS exits with no transport protocol loaded

loadmodule "proto_udp.so" is missing. See the module settings.

error while querying database at startup, and every message fails with rc=-2

No SMSC was loaded, for example because a field in the smpp table is empty. See Define the SMSC.

The bind fails with Error in bind_transceiver_resp 0000000d

The SMSC rejected the bind. Check the System ID and Password. See Step 4.

With stunnel, Connection refused on 127.0.0.1:2775, then a successful bind

OpenSIPS started before stunnel. The module retries the connection. Start stunnel first to avoid the error.

The SMSC answers Error in submit_sm_resp 00000001

The message body was empty. Check what the user agent sends.

Non-Latin text arrives as a string of hexadecimal digits

The body was UTF-16 in hexadecimal digits, but the X-SMS-Encoding: UTF-16 header did not reach OpenSIPS, so the digits were sent as text. See outbound messages.


3. Send and receive messages (Asterisk example)#

OpenSIPS does not create messages, so a SIP user agent has to send and receive them. It sends each outbound SMS to OpenSIPS as a MESSAGE request, and receives inbound SMS and delivery receipts from OpenSIPS the same way. This part uses Asterisk 22 with PJSIP as an example. Any user agent that sends and receives SIP MESSAGE requests can be used instead.

Important

Run Asterisk on a different host from OpenSIPS. Both listen on UDP port 5060.

Step 1: Configure the endpoint#

In /etc/asterisk/pjsip.conf, define OpenSIPS as an endpoint:

[transport-udp]
type=transport
protocol=udp
bind=0.0.0.0:5060

[opensips]
type=endpoint
context=sms-in
message_context=sms-in
disallow=all
allow=ulaw
aors=opensips

[opensips]
type=aor
contact=sip:OPENSIPS_IP:5060
qualify_frequency=0

[opensips]
type=identify
endpoint=opensips
match=OPENSIPS_IP

The identify section matches requests from OpenSIPS to this endpoint. message_context sends incoming MESSAGE requests to the sms-in context. See Step 2.

Step 2: Configure the dialplan#

Add two contexts to /etc/asterisk/extensions.conf:

[sms-in]
exten => _X.,1,NoOp(SMS in: from=${MESSAGE(from)} to=${MESSAGE(to)})
 same => n,Verbose(1,SMS in body: ${MESSAGE(body)})
 same => n,Hangup()

[sms-out]
exten => _X.,1,Set(MESSAGE(body)=${SMS_BODY})
 same => n,ExecIf($["${SMS_ENCODING}" != ""]?Set(MESSAGE_DATA(X-SMS-Encoding)=${SMS_ENCODING}))
 same => n,MessageSend(pjsip:opensips/sip:${EXTEN}@OPENSIPS_IP:5060,sip:${SMS_SOURCE}@USER_AGENT_IP)
 same => n,NoOp(MessageSend status: ${MESSAGE_SEND_STATUS})
 same => n,Hangup()

sms-in receives inbound SMS and delivery receipts. The extension is the receiving DID, the sender is in MESSAGE(from), and the text is in MESSAGE(body).

sms-out sends one SMS. The recipient is the extension, the sender is SMS_SOURCE (the DID, in international format without +), and the text is SMS_BODY.

Note

Asterisk sends a MESSAGE with Content-Type: text/plain and replaces a value set with MESSAGE_DATA(Content-Type). This is why the UTF-16 marker is the custom X-SMS-Encoding header, which Asterisk passes unchanged.

Step 3: Send a message#

Create a call file that runs sms-out, then move it into the outgoing spool directory:

cat > /var/spool/asterisk/tmp/sms.call <<'EOF'
Channel: Local/RECIPIENT@sms-out
Application: Wait
Data: 1
MaxRetries: 0
Set: SMS_SOURCE=SENDER_DID
Set: SMS_BODY=Hi, I will be in the office tomorrow morning
EOF
mv /var/spool/asterisk/tmp/sms.call /var/spool/asterisk/outgoing/

RECIPIENT and SENDER_DID are numbers in international format without +. Create the file outside the outgoing directory, on the same filesystem, so that Asterisk does not read a partial file.

For non-Latin text, convert the body to UTF-16BE in hexadecimal digits and set SMS_ENCODING:

printf %s "你好" | iconv -f UTF-8 -t UTF-16BE | od -An -v -tx1 | tr -d ' \n'
Set: SMS_BODY=4f60597d
Set: SMS_ENCODING=UTF-16

For more information about the SMPP module, see the official proto_smpp documentation.