# 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](https://docs.opensips.org/manual/4-0/modules/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](voice.html).

Important

OpenSIPS relays messages and does not create them. A user agent that sends
and receives SIP `MESSAGE` requests is required.
[Part 3](#opensips-sms-user-agent) 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](https://my.didww.com/users/sign_in) or [create a DIDWW account](https://my.didww.com/users/sign_up#/users/sign_up).
- 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](../../sms/sender-id-verifications/index.html#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](../../phone-numbers/buy-numbers/how-to-buy.html).
- The OpenSIPS server must be able to open a TCP connection to the DIDWW SMSC
  on port `2775`, or on port `2776` for [TLS](#opensips-sms-tls).

### Step 1: Create New SMPP ESME trunk

1. In the [DIDWW User Panel](https://my.didww.com/#/sms_trunks), go to
   **SMS > SMS Trunks**.
2. Click **Create New > SMPP ESME**.

![Creating a new SMPP ESME trunk from the Create New menu](../../_images/sms-create-esme.webp)


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](#opensips-sms-define-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](../../_images/sms-esme-settings.webp)


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](../../_images/sms-esme-create.webp)


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](#opensips-sms-define-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](../../_images/sms-esme-credentials.webp)


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](https://my.didww.com/#/user_dids), 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](#opensips-sms-save-trunk).
3. Click **Confirm**.

To assign the trunk to several DIDs at once, see [Assign an SMS
trunk](../../phone-numbers/my-numbers/how-to-guides/assign-sms-trunk.html#user-panel-assign-sms-trunk).

![Assigning the SMPP ESME trunk to a DID number in My Numbers](../../_images/sms-assign-did.webp)


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](#opensips-sms-configure-trunk) 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](#opensips-sms-define-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](#opensips-sms-define-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](#opensips-sms-outbound). |
| `ip` | The IP address of the **Host** from [Step 4](#opensips-sms-copy-credentials). 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](#opensips-sms-copy-credentials). |
| `system_type` | The **System type** from [Step 2](#opensips-sms-trunk-settings). |
| `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](#opensips-sms-define-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](#opensips-sms-send-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](#opensips-sms-load-modules), 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](#opensips-sms-tls). Otherwise, go to [Step 4](#opensips-sms-verify).

### 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](https://www.stunnel.org/) 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](#opensips-sms-define-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](#opensips-sms-user-agent). | The `SMS out` line from [outbound messages](#opensips-sms-outbound), 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](#opensips-sms-assign-trunk). | 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-sms-configure-socket).

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-sms-configure-socket).

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-sms-load-modules).

OpenSIPS exits with no transport protocol loaded

`loadmodule "proto_udp.so"` is missing. See [the module settings](#opensips-sms-load-modules).

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](#opensips-sms-define-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](#opensips-sms-copy-credentials).

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](#opensips-sms-outbound).

---

## 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](#opensips-sms-asterisk-dialplan).

### 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](https://docs.opensips.org/manual/4-0/modules/proto_smpp/) documentation.

On this page
