Skip to main content

SMPP wire-level recipes

The Connect via SMPP guide covers the bind lifecycle and the runnable client snippets; the SMPP API reference covers the REST surface. This page is the layer under both: the literal bytes. Each PDU below appears as a complete hex listing — the 16-octet header plus the payload — annotated field by field, so you can benchmark your client library, debug a byte offset, or translate the listing straight into a hex test fixture. The PDUs shown are the full session any sender runs: bind, submit_sm, and the keepalive loop. Field values match the worked parameters on the Connect via SMPP page: alphanumeric sender OrbitBrand, destination 447700900123, payload hello world, plain-ASCII with data_coding=0x00.

Reading a hex listing

Every SMPP PDU is a 16-octet big-endian header followed by a payload: Two wire-level rules matter on every PDU:
  • C-strings. system_id, password, source/destination addresses, and message_id are NUL-terminated octet strings — the value’s bytes plus one 0x00 octet. A lettered span in a listing below such as 4f 72 ... reads as ASCII.
  • Digits only on destinations. dest_addr_ton values above 1 are legacy detail; set ton=1 (international) with an E.164 digit string, ton=5 (alphanumeric) with digits anyway, and the bound is upheld — number normalization is edge-side, so don’t depend on it.

The PDU enumeration

The complete set your integration can legitimately send or expect on a session, in call order: In responses the high bit is set: a request goes out with 0x0000xxxx, its response comes back with 0x8000xxxx. Every _resp PDU below carries the same sequence_number as its request, even on error — reuse that to line up through your own operation table.

1. Open the session: bind_transceiver

Pick a transceiver bind unless you operate a send-only/discipline split deliberately — receipts and (with the credential’s dlrMode on bind or both) inbound deliver_sm traffic come down the same session. Request (sequence 0x00000001):
Response (bind_transceiver_resp, 0x80000009, same sequence):
command_status=0x00000000 means the session is open and submit_sm is accepted from now on. A nonzero status in the response — 0x0000000E (ESME_RINVPASWD, bad password), 0x0000000F (ESME_RINVSYSID, unknown system_id), 0x00000015 (ESME_RINVBNDFMT, source IP outside the credential’s allowedCidrs), or 0x0000000D (ESME_RALYBND — see Errors) — rejects in the same envelope; read the status from the response before assuming the network ate it.

2. Send and poll the response: submit_sm

Request (sequence 0x00000002, length 0x42 = 66):
Response (submit_sm_resp, 0x80000004, message_id for DLR reconciliation):
Capturing the returned message_id against your sequence_number closing out the send. An inbound deliver_sm receipt on the same bind embeds that same string in its receipted_message_id field, which is the join between your operation table and the delivery world. Send body notes:
  • data_coding=0x08 is UCS-2 (Unicode); 0x00 is the SMSC default alphabet (GSM-7). Set the right one or you’re sending mojibake.
  • esm_class=0x40 flags UDHI for concatenated segments when you breach a single segment’s 140-octet limit; short segments link with a UDH at the payload head.
  • The submit_sm PDU has no batch mode: pipeline many in flight on the bind and let the remote window drain them.
  • sm_length is a single octet of payload length right ahead of short_message; when you’re debugging capture, a mismatched length there is the frame’s framing fault.
Send one every 30–60 seconds. Response expected within your client’s timeout (10 s is plenty):
Response:
The full 16-octet header and nothing else — both directions. The traffic on your session subscribes to the same keepalive discipline as any SMPP peer: no enquire_link within the edge’s idle window tears the session down, and a session that re-binds on every send is the bind-abuse pattern the edge throttles.

4. Close cleanly: unbind

Response:
Drain in-flight submit_sm before the unbind — receipts sent in the window between the TCP close and the session teardown don’t re-queue.
Whatever shape you keep the session state in, the only tables are: a counter for sequence_number, a map from sequence → pending operation (each submit_sm goes in until the submit_sm_resp arrives), and the returned message_id keyed for DLR joins. Rebinds reset nothing for the edge — it correlates on the number alone, and re-use of sequence values across sessions is fine.

Errors

Work these top to bottom — the first row that matches is the fix. Compliance controls (opt-out suppression, tenant quiet hours, consent) apply to every submit_sm the same way they do on the REST path, per your tenant’s own configuration — the controls are tenant-owned and apply the same on the SMPP wire. A rejection there arrives as a nonzero command_status in submit_sm_resp, not as a silent drop, and the Developer → SMPP Compliance tab shows you how they apply. The Devotel softswitch carries the MT leg; your bind is the wire into it.

See also