> ## Documentation Index
> Fetch the complete documentation index at: https://docs.orbit.devotel.io/llms.txt
> Use this file to discover all available pages before exploring further.

# SMPP wire-level recipes: hex walkthroughs of the bind and submit sequence

> Byte-level worked examples of the SMPP PDUs your client puts on the wire against Orbit's edge — bind, submit_sm, enquire_link, unbind — with raw header and payload hex so you can verify client libraries against the protocol directly.

# SMPP wire-level recipes

The [Connect via SMPP](/guides/smpp) guide covers the bind lifecycle and the runnable client snippets; the [SMPP API reference](/api-reference/endpoints/smpp) 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](/guides/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:

| Octets | Field             | Meaning                                       |
| ------ | ----------------- | --------------------------------------------- |
| 0–3    | `command_length`  | Total PDU length in octets, header included   |
| 4–7    | `command_id`      | Which PDU this is (see the enumeration below) |
| 8–11   | `command_status`  | 0 on requests; on responses, the error code   |
| 12–15  | `sequence_number` | Correlates a response with its request        |

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](/guides/smpp) 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:

| `command_id` | PDU                                      | Direction     |
| ------------ | ---------------------------------------- | ------------- |
| `0x00000009` | `bind_transceiver`                       | client → edge |
| `0x00000001` | `bind_receiver`                          | client → edge |
| `0x00000002` | `bind_transmitter`                       | client → edge |
| `0x00000004` | `submit_sm`                              | client → edge |
| `0x80000004` | `submit_sm_resp`                         | edge → client |
| `0x00000005` | `deliver_sm` (receipts, inbound traffic) | edge → client |
| `0x00000015` | `enquire_link`                           | both ways     |
| `0x80000015` | `enquire_link_resp`                      | both ways     |
| `0x00000006` | `unbind`                                 | client → edge |
| `0x80000006` | `unbind_resp`                            | edge → client |

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.

<CodeGroup>
  ```text full session, hex view theme={null}
  – Sequence 1 … bind (transceiver covers both directions)
  – Sequence 2 … submit_sm (one message)
  – Sequence N … enquire_link keepalive every 30–60 s
  – Sequence N+1 … submit_sm
  – …
  – Sequence N+2 … unbind on shutdown
  ```
</CodeGroup>

## 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`):

```text theme={null}
0000 0030 0000 0009 0000 0000 0000 0001     header: len=48, bind_transceiver, ok, seq=1
736d 7070 5f73 7973 5f61 6263 6431 3200   system_id="smpp_sys_abcd12"
4162 3358 7939 5a71 00                     password="Ab3Xy9Zq"
0000 3400 0000                             system_type="" interface=0x34 ton=0 npi=0
00                                       addr_range=""
```

Response (`bind_transceiver_resp`, `0x80000009`, same sequence):

```text theme={null}
0000 0011 8000 0009 0000 0000 0000 0001     header: len=17, bind_transceiver_resp, ok, seq=1
00                                       system_id=""
```

`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](#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):

```text theme={null}
0000 0042 0000 0004 0000 0000 0000 0002     header: len=66, submit_sm, ok, seq=2
00                                       service_type=""
05                                       source_addr_ton=5 (alphanumeric)
00                                       source_addr_npi=0
4f72 6269 7442 7261 6e64 00              source_addr="OrbitBrand"
01                                       dest_addr_ton=1 (international)
01                                       dest_addr_npi=1 (E.164)
3434 3737 3030 3930 3031 3233 00         destination_addr="447700900123"
00                                       esm_class=0
00                                       protocol_id=0
00                                       priority_flag=0 (normal)
00                                       schedule_delivery_time="" (send now)
00                                       validity_period="" (network default)
01                                       registered_delivery=1 (request a receipt)
00                                       replace_if_present_flag=0
00                                       data_coding=0 (SMSC default alphabet, GSM-7)
00                                       sm_default_msg=0
0b                                       sm_length=11
6865 6c6c 6f20 776f 726c 64             short_message="hello world"
```

Response (`submit_sm_resp`, `0x80000004`, `message_id` for DLR reconciliation):

```text theme={null}
0000 001f 8000 0004 0000 0000 0000 0002     header: len=31, submit_sm_resp, ok, seq=2
6d73 675f 3031 4834 584b 3644 3759 00   message_id="msg_01H4XK6D7Y"
```

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.

## 3. Stay alive: `enquire_link`

Send one every 30–60 seconds. Response expected within your client's timeout (10 s is plenty):

```text theme={null}
0000 0010 0000 0015 0000 0000 0000 0003     header: len=16, enquire_link, ok, seq=3
```

Response:

```text theme={null}
0000 0010 8000 0015 0000 0000 0000 0003     enquire_link_resp, ok, seq=3
```

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](#errors) 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`

```text theme={null}
0000 0010 0000 0006 0000 0000 0000 0004     header: len=16, unbind, ok, seq=4
```

Response:

```text theme={null}
0000 0010 8000 0006 0000 0000 0000 0004     unbind_resp, ok, seq=4
```

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.

<ResponseExample>
  ```text session notation once sequence 2 has closed theme={null}
  seq=2   submit_sm → submit_sm_resp   status=ok   message_id=msg_01H4XK6D
  seq=3   enquire_link → enquire_link_resp            keepalive cycle
  seq=4   unbind → unbind_resp                        drained, closing
  ```
</ResponseExample>

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.

| Symptom                                                                     | Meaning               | Fix                                                                                                                                                                                      |
| --------------------------------------------------------------------------- | --------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Session goes silent, edge tears the bind                                    | Absent `enquire_link` | Send the keepalive every 30–60 s with a 10 s response deadline; rebind with backoff. Silence in a 30-minute window is the default edge idle timeout.                                     |
| `ESME_RALYBND` (`0x0000000D`) on a second bind while the first is alive     | Already bound         | One credential supports one session at a time — move the stale bind's client off, or split traffic across multiple credentials on purpose.                                               |
| `ESME_RTHROTTLED` (`0x00000058`) on `submit_sm`, bursts in a sliding window | Rate limit opening    | The credential's TPS cap counts submissions per second per bind. Back off with jittered retry, or raise `tpsLimit` — the window slides, so a burst in one second doesn't taint the next. |
| `ESME_RMSGQFUL` (`0x00000014`)                                              | Edge queue full       | Slow your enqueue; drain inbound `deliver_sm` receipts before pumping more `submit_sm`.                                                                                                  |
| `ESME_RINVDSTADR` (`0x0000000B`)                                            | Bad destination       | E.164 digit string; `ton=1/npi=1` only when your client demands it.                                                                                                                      |
| `ESME_RINVMSGLEN` (`0x00000004`)                                            | Message too long      | Concatenate with the UDH marker or shorten the payload.                                                                                                                                  |
| Garbage after N octets on a captured frame                                  | Length framing fault  | `command_length` and `sm_length` disagreement is the classic framing slip — assert both fields pack before writing the buffer to the socket.                                             |

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

* [Connect via SMPP](/guides/smpp) — bind credentials, ports, throughput, and the diagnostic ladder
* [SMPP: the no-delivery receipt window](/guides/smpp-receipt-timeout-window) — per-rule timing for the UNDELIV receipt on absorbed traffic
* [SMPP edge model](/concepts/smpp-edge-model) — the relay underneath your bind
* [Send-side DLR model](/concepts/send-side-dlr-model) — the receipt vocabulary that `message_id` reconciliation feeds
* [SMPP API reference](/api-reference/endpoints/smpp) — the REST surface for credentials and carriers
