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)
# 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 laterNode.js (smpp)
// 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:
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_messagewithesm_classbit0x40set (8-bit or 16-bit reference), or - the
sar_msg_ref_num,sar_total_segmentsandsar_segment_seqnumparameters.
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:
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.