More sandboxes

On this page

SMS over SMPP

Applications that talk to an SMS gateway over SMPP 3.4 can bind to the SMS sandbox instead of a real SMSC. Testlie acts as the SMSC: every message your application submits is captured in an SMS inbox, exactly like a call to the Twilio-compatible API, and nothing is ever delivered to a phone.

Captured messages appear in the same inbox, API, long-poll endpoint, sms.received webhooks and plan quotas as Twilio API messages. Failure rules apply to SMPP submissions too.

Connection details

Every SMS inbox has its own SMPP credentials. Open SMS, pick the inbox and look at Credentials & SDKs → SMPP.

Setting Value
Host smtp.testlie.com
TLS port (recommended) 3550 (SMPP over TLS 1.2 or 1.3, certificate for smtp.testlie.com)
Plain port 2775 (plain TCP, for clients that cannot use TLS)
system_id tl followed by 12 hex characters, per SMS inbox
password 8 characters (the SMPP 3.4 maximum), per SMS inbox
system_type Anything; ignored
Interface version 3.4 (0x34); older ESMEs work too

Use the TLS port whenever your SMPP client supports TLS: on the plain port the system_id and password cross the internet unencrypted. The examples below use the plain port for brevity; switch to TLS in production-like setups (for example ssmpp://smtp.testlie.com:3550 with the Node smpp package, or a TLS-wrapped socket in other clients). Admins can rotate the password; open binds of the inbox are then unbound and must bind again. Inboxes created before SMPP support get credentials from Generate SMPP credentials (or POST /api/sms-inboxes/<id>/smpp).

Send a message

Python (smpplib)

python
# pip install smpplib
import smpplib.client, smpplib.consts, smpplib.gsm

client = smpplib.client.Client("smtp.testlie.com", 2775, allow_unknown_opt_params=True)
client.set_message_sent_handler(lambda pdu: print("message_id", pdu.message_id.decode()))
client.set_message_received_handler(lambda pdu: print("receipt:", pdu.short_message.decode()))
client.connect()
client.bind_transceiver(system_id=SYSTEM_ID, password=PASSWORD)

# make_parts picks GSM 03.38 or UCS-2 and splits long texts with a UDH.
parts, encoding, esm_class = smpplib.gsm.make_parts("Your code is 123456")
for part in parts:
    client.send_message(
        source_addr_ton=smpplib.consts.SMPP_TON_ALNUM, source_addr="MyApp",
        dest_addr_ton=smpplib.consts.SMPP_TON_INTL, destination_addr="15551234567",
        short_message=part, data_coding=encoding, esm_class=esm_class,
        registered_delivery=True)
client.listen()  # prints the delivery receipt a few seconds later

Node.js (smpp)

js
// npm install smpp
const smpp = require("smpp");

const session = smpp.connect({ url: "smpp://smtp.testlie.com:2775", auto_enquire_link_period: 10000 }, () => {
  session.bind_transceiver({ system_id: SYSTEM_ID, password: PASSWORD }, (pdu) => {
    if (pdu.command_status !== 0) throw new Error("bind failed: " + pdu.command_status);
    session.submit_sm(
      { source_addr: "MyApp", destination_addr: "+15551234567", short_message: "Your code is 123456", registered_delivery: 1 },
      (resp) => console.log("message_id", resp.message_id),
    );
  });
});
session.on("deliver_sm", (pdu) => {
  session.send(pdu.response());
  if (pdu.esm_class & 0x04) console.log("receipt:", pdu.short_message.message);
});

Both snippets were run against the sandbox (smpplib 2.2.4 and smpp 0.6.0-rc.4), including long and Unicode messages. The SMS page shows them with your credentials filled in.

Assert on the capture

The submit_sm_resp message_id is the captured SMS's SID, so a test can fetch it directly, or wait for it like any other SMS:

bash
curl -f -H "Authorization: Bearer $TESTLIE_API_KEY" "https://testlie.com/api/sms/$MESSAGE_ID"
curl -f -H "Authorization: Bearer $TESTLIE_API_KEY" \
  "https://testlie.com/api/sms-inboxes/$SMS_INBOX_ID/messages/wait?to=%2B15551234567&contains=code&timeout=30"

SMPP messages have "channel": "smpp". International numbers (TON 1, or written with +) are stored in E.164 form with a leading +; alphanumeric senders are kept as sent.

Text encodings

data_coding Read as
0 GSM 03.38 default alphabet, one septet per octet, including the extension table (€ [ ] { } \ ^ ~ |)
1 ASCII (IA5)
3 Latin-1 (ISO-8859-1)
8 UCS-2 / UTF-16BE (emoji as surrogate pairs)
2, 4 8-bit binary, shown as [binary] <hex>
6, 7, 5, 10, 14 Cyrillic, Hebrew, Shift-JIS, ISO-2022-JP, KS C 5601
0xF0-0xFF GSM 03.38 or 8-bit, by the message class bits

With data_coding 0 the bytes are GSM 03.38, not ASCII: letters, digits and most punctuation are the same, but @ is 0x00 and _ is 0x11. Use data_coding 1 or 3 if your client sends plain ASCII or Latin-1 bytes.

Long messages can arrive as one submit_sm with the message_payload parameter, or as several parts:

  • a User Data Header in short_message with esm_class bit 0x40 set (8-bit or 16-bit reference), or
  • the sar_msg_ref_num, sar_total_segments and sar_segment_seqnum parameters.

Parts may arrive in any order and over different binds of the same inbox. They are joined into one captured SMS that keeps the segment count. Each part's submit_sm_resp returns its own message_id: the SID for the first part and <SID>-<n> for the others. Parts still missing after 60 seconds are stored as they are, with [missing segment n/total] in place of the gaps.

Delivery receipts

Set registered_delivery to 1 (receipt on success or failure) or 2 (failures only). When the simulated status becomes final, a receipt goes to one of the inbox's receiver or transceiver binds as a deliver_sm with esm_class 0x04:

text
id:SM1f0c… sub:001 dlvr:001 submit date:2610161205 done date:2610161205 stat:DELIVRD err:000 text:Your code is 123456
Field Meaning
id The message_id from submit_sm_resp (the first part's, for concatenated messages)
sub, dlvr Segments submitted and delivered
stat DELIVRD, UNDELIV (async 30xxx failure rule) or REJECTD (async 21xxx failure rule)
err Last three digits of the Twilio error code, e.g. 003 for 30003
text First 20 characters of the message

The receipt also carries the receipted_message_id and message_state parameters. A concatenated message gets a single receipt. Receipts wait until a receiver or transceiver is bound, and are sent again if the client does not acknowledge them with deliver_sm_resp.

Failure simulation

The inbox's failure rules match the destination number. A sync rule rejects the submit_sm (every part, for concatenated messages): 21211 answers ESME_RINVDSTADR, other codes ESME_RSUBMITFAIL. An async rule accepts the message and reports the failure in the receipt.

Supported operations

PDU Behaviour
bind_transmitter, bind_receiver, bind_transceiver Authenticate with the inbox's system_id and password
submit_sm, data_sm Capture a message (or one part of one)
submit_multi Capture one SMS per number; distribution lists are reported as unsuccessful (ESME_RINVDLNAME)
query_sm Current state: ENROUTE, DELIVERED, UNDELIVERABLE or REJECTED
enquire_link, unbind Answered in any bound state; the server also sends enquire_link after 60 seconds of silence and unbinds after 120
cancel_sm, replace_sm Answered with ESME_RCANCELFAIL / ESME_RREPLACEFAIL: messages are final within seconds
Anything else generic_nack with ESME_RINVCMDID

Errors and limits

command_status When
ESME_RBINDFAIL (0x0D) Wrong password or unknown system_id (the same answer for both), too many binds for the inbox (10) or team (50), or too many failed binds from your address
ESME_RALYBND (0x05), ESME_RINVBNDSTS (0x04) Bind while bound; operation not allowed before binding or in this bind mode
ESME_RTHROTTLED (0x58) More than 50 submissions per second for the inbox, or 200 for the team
ESME_RMSGQFUL (0x14) The team's monthly SMS quota is used up
ESME_RINVDSTADR (0x0B) Destination is not a phone number, or a sync 21211 failure rule matched
ESME_RINVCMDLEN (0x02) A PDU shorter than its fields, or a command_length over 64 KB (answered with generic_nack, then the connection is closed)
ESME_RINVMSGLEN, ESME_RINVESMCLASS, ESME_RINVREGDLVFLG, ESME_RINVSCHED, ESME_RINVEXPIRY, ESME_ROPTPARNOTALLWD, ESME_RINVPARLEN, ESME_RMISSINGOPTPARAM, ESME_RINVOPTPARAMVAL Malformed message fields, UDH or optional parameters

Binds that do not happen within 30 seconds of connecting are disconnected. Each connection processes up to 64 outstanding requests before it stops reading (TCP back-pressure). schedule_delivery_time and validity_period are validated but not honoured: messages are captured immediately. Plan limits are described in Plans and limits; a concatenated message counts as one SMS.