Plan the interoperability tests against real-world SMPP peers before 1.0.0

This commit is contained in:
2026-09-05 17:31:02 +02:00
parent 9ca83ac05c
commit 8a565cf557
5 changed files with 888 additions and 0 deletions
+237
View File
@@ -0,0 +1,237 @@
# Real-world SMPP behaviours & quirks checklist
For `@larvit/smpp` (ESME client + SMSC server, SMPP 3.4 default) pre-1.0 testing. Every item: behaviour, who, source URL, one-line test. Unsourced items are marked **unverified — folklore**.
Legend for repeat sources: Kannel = Kannel User's Guide 1.4.5 (kannel.org); spec = SMPP v3.4 Issue 1.2 (smpp.org/SMPP_v3_4_Issue1_2.pdf, cross-read via sysop.fr HTML mirror where the PDF extractor cut off).
---
## 1. Bind
- interface_version `0x34` = SMPP 3.4; `0x00``0x33` = "3.3 or earlier" — spec. Test: bind with 0x50 (v5) against a 3.4-only server, confirm reject or negotiate via `sc_interface_version`.
- `sc_interface_version` optional TLV in bind_resp reports the SMSC's actual max supported version, independent of what was requested — spec. Test: assert bind_resp TLV reflects server's true version even if client requested something else.
- Kannel: default source-addr TON=2/NPI=1 if left unset in `smsc smpp` group — Kannel User's Guide 1.4.5, https://www.kannel.org/download/1.4.5/userguide-1.4.5/userguide.html. Test: bind without explicit TON/NPI, confirm defaults appear on outgoing PDUs.
- Melrose Labs SMSC Simulator: rejects destination_address <8 chars with `ESME_RINVDSTADR` (0x0B); requires TLS 1.1+ (no SSL/TLS1.0) — https://melroselabs.com/services/smsc-simulator/smsc-simulator-technical-details/. Test: submit_sm with a short dest address, expect rejection; attempt TLS1.0 handshake, expect refusal.
- Sinch (current SMPP product): TLS port 3601, host `<host>.smpp.api.sinch.com`; IP restricted to pre-registered addresses by default; only 2 parallel connections per host/system_id — https://developers.sinch.com/docs/sms/smpp/connectivity. Test: bind a 3rd session on the same system_id, expect rejection.
- Sinch (legacy "Cloud SMPP"): max 3 sessions; TLS on **different ports per encoding** — 8443 standard, 9443 Latin-1 — https://developers.sinch.com/docs/sms/other/sms-other-cloud-smpp. Test: connect TLS to 8443 vs 9443, confirm encoding-dependent routing.
- Infobip: bind as transceiver or receiver to get DLRs (transmitter-only gets none); max 4 sessions by default; only SMPP **3.4** accepted (3.3/5.0 rejected); hosts `smpp3.infobip.com:8888` primary / `smpp1.infobip.com:8888` secondary, TLS `smpp2.infobip.com:8887`; `system_type=HLR` required for Number Lookup bind — https://www.infobip.com/docs/essentials/api-essentials/smpp-specification. Test: bind interface_version 0x33/0x50, expect failure; bind TX-only, confirm no DLR arrives.
- Telnyx: host `smpp.telnyx.com:2775`, TLS required; bind requires `addr_ton=1`/`addr_npi=1`; gated to contracted accounts ($5000/month, 12mo min) — https://support.telnyx.com/en/articles/1667062-short-message-peer-to-peer-set-up-guide. Test: bind with other addr_ton/npi values, expect rejection.
- Vonage: SMPP 3.4; default 2 connections/account — 3rd bind rejected with `command_status=0x00000005` (ESME_RALYBND); recommends clustered host pairs; unique `system_type` per host needed for multi-host inbound; error 13 decimal/`0x0d` (ESME_RBINDFAIL) = wrong zone/host or bad creds — https://api.support.vonage.com/hc/en-us/articles/204015713 , https://api.support.vonage.com/hc/en-us/articles/204015783 , https://api.support.vonage.com/hc/en-us/articles/204591147. Test: open a 3rd bind, assert command_status 0x00000005.
- Bird: server only actually validates `system_id`/`password`/`interface_version` at bind — other fields are ignored; supports 3.3/3.4/5.0 (3.3 loses TLV info in deliver_sm); per-account bind-count/throughput enforced **per SMPP server** (so binding to N servers multiplies the effective cap); plaintext TCP 2775, TLS 2776 (SSLv3+ only) — https://docs.bird.com/connectivity-platform/other-integrations/how-can-i-use-the-short-message-peer-to-peer-smpp-network-protocol. Test: bind with garbage in address_range, confirm still accepted; open the (N+1)th bind on one server and confirm it's rejected while a bind to a second server still works.
- Clickatell: 3.3/3.4 supported; new integrations must pass a **compliance test SMSC** before being switched to production — https://archive.clickatell.com/developers/api-docs/using-the-smpp-api/. Test: model two distinct SMSC endpoints (test/prod) with separate credentials.
- Kaleyra: `interface_version` must be `0x34` for v3.4 features, else silently defaults to 3.3 behaviour; `system_type` should be `smpp`; TRX bind only available when negotiating 3.4 — https://messaging.kaleyra.com/support/solutions/articles/3000091795-introduction-to-smpp-interface. Test: bind at 0x33, confirm TRX-gated features (e.g. TLVs) are withheld vs 0x34.
- tyntec: 4 connection modes — plain SMPP, SSL-SMPP, SMPP-in-VPN, SMPP-with-TLS (on request) — https://www.tyntec.com/helpcenter/docs/faqs/sms/getting-started/what-type-of-connections-does-tyntec-support-for-smpp/. Test: model plain and TLS as distinct configured listeners, not one socket with a flag.
- CM.com: endpoints `smpp_gw.messaging.cm.com:30006` / `smpp_sgw.messaging.cm.com:30007` behind a global load balancer — https://developers.cm.com/messaging/docs/smpp. Test: bind against both, confirm 0x34 accepted on each.
- LINK Mobility (Implementation Guide): interface_version must be 3.4 — anything else fails the bind; `system_type`/`address_range` accepted but ignored; recommends separate TX+RX binds over a single TRX; requires client IP pre-registered (firewalled otherwise) — https://a.storyblok.com/f/151608/x/bb282a50a7/link-mobility-implementation-guide-smpp.pdf. Test: bind with interface_version ≠0x34, expect failure; bind from an unregistered IP, expect connection refused.
- LINK Mobility (SMSC-SMPP User Guide, separate product): TX/RX/TRX all supported; `address_range` **not supported at all**; TLS 1.2/1.3 required (1.0/1.1 discontinued 2020-11-15); TLS port 3601 vs plaintext 3600 — https://www.linkmobility.com/resources/developer/SMSC-SMPP-User-Guide-1.5.pdf. Test: connect on 3600 without TLS vs 3601 with TLS1.2+; attempt TLS1.1, expect rejection.
- Telia: interface_version must be 0x34; `system_type`/`address_range` "not in use" — send NULL; static IP must be ACL-whitelisted; TLS ≥1.2 with SNI required; endpoint `smpp.messaging.teliacompany.com:3550` — https://cdn.messaging.teliacompany.com/documents/developer/index.html. Test: bind with non-null system_type (still succeeds, field ignored); connect from a non-whitelisted IP, expect refusal.
- Telenor: SMSC "supports the most important operations... but does not fully support the protocols" of 3.3/3.4; **OUTBIND, SUBMIT_MULTI and ALERT_NOTIFICATION explicitly not supported** — https://developer.telenor.no/images/97-322719.pdf. Test: send those 3 PDU types to a Telenor-modeled mock, expect no support/rejection.
- Twilio: does offer an SMPP API but with **no self-service onboarding** — enterprise-only, provisioned via account manager for existing SMPP deployments — https://help.twilio.com/articles/31537798746267. Test: not directly testable; document as enterprise-gated rather than a public bind target.
- Telesign: SMPP 3.4/5.0; TX/RX/TRX (persistent TRX preferred); `system_id` max 16 octets, `password` max 9 octets; TLS 1.2 mandatory; Session Init Timer 10s (time to bind after TCP connect); explicitly advises against IP-allowlisting Telesign's own gateway IP (it can change) — https://developer.telesign.com/enterprise/docs/smpp-protocol. Test: assert bind fails if not completed within 10s of TCP connect; reject system_id>16 or password>9 octets.
- Route Mobile: interface_version must be 0x34 to enable v3.4-only fields (long SMS, message_payload) — omitted/0x33 disables them silently rather than erroring; `system_type` unused/must be null; multiple simultaneous binds allowed — https://routemobile.com/pdf_files/developer/api/routemobilesmpp.pdf. Test: bind at 0x33, then attempt message_payload/UDH long-SMS, confirm treated as unavailable (not rejected loudly).
- Bandwidth: bind mode is one of Transceiver (recommended, "for proper DLR delivery")/Sender Only/Receiver Only — DLR delivery is tied to the chosen mode, not forced-TRX; IP allowlisting via priority-weighted groups, highest weight used first — https://www.bandwidth.com/support/en/articles/12823933. Test: bind Receiver Only, submit_sm, confirm rejected/behaves differently than under Transceiver.
- Syniverse: TLS version/ports/certs/IP-allowlisting for the raw SMPP bind live only in a private per-customer "Detail Technical Plan" — not publicly documented — https://sdcsupport.syniverse.com/hc/en-us/articles/18883083316247. Test: document as an untestable gap rather than guess values.
## 2. Keepalive
- spec itself only mandates "reliable data transfer" at the transport layer and defines enquire_link as a no-op PDU — it does **not** mandate a specific interval or disconnect timeout (§2.4) — spec via smpp.org PDF. Test: none — confirms interval/timeout is vendor policy, not a spec requirement.
- Kannel `enquire-link-interval`: default 30 seconds — https://www.kannel.org/download/1.4.5/userguide-1.4.5/userguide.html. Test: idle a bound session 30s+, assert an enquire_link is sent.
- Melrose Labs simulator: sends enquire_link to the ESME after 45s of inactivity (server-initiated keepalive) — https://melroselabs.com/services/smsc-simulator/smsc-simulator-technical-details/. Test: idle a session against the simulator, assert a server-initiated enquire_link at ~45s.
- Sinch (current product): recommends enquire_link every 30s — https://developers.sinch.com/docs/sms/smpp/connectivity. Test: send enquire_link every 30s, confirm connection survives.
- Sinch (legacy Cloud SMPP): recommends 60s instead of 30s — https://developers.sinch.com/docs/sms/other/sms-other-cloud-smpp. Test: verify a different cadence is accepted on this endpoint vs. the current product's.
- Infobip: 30-second keep-alive timeout — https://www.infobip.com/docs/essentials/api-essentials/smpp-specification. Test: withhold all traffic (incl. enquire_link) >30s, expect session closed.
- LINK Mobility (Implementation Guide): SMSC sends enquire_link if idle 60s; read timeout 15s for DLR acks / 30s for other requests; recommended submit_sm_resp timeout 10 minutes; after receiving unbind, client should wait ≥30s before reconnecting — https://a.storyblok.com/f/151608/x/bb282a50a7/link-mobility-implementation-guide-smpp.pdf. Test: idle >60s, expect SMSC-initiated enquire_link; reconnect <30s after an unbind, check for rejection.
- LINK Mobility (SMSC-SMPP User Guide): client should call enquire_link every 60 seconds — https://www.linkmobility.com/resources/developer/SMSC-SMPP-User-Guide-1.5.pdf. Test: send at >60s intervals and confirm no disconnect vs sending too rarely.
- Telia: recommended connect/read timeout 10 seconds — https://cdn.messaging.teliacompany.com/documents/developer/index.html. Test: set client socket read timeout to 10s baseline.
- Telenor: general SMSC doc states 300s (5 min) default TCP inactivity timeout (scope not confirmed SMPP-exclusive — lower confidence) — https://developer.telenor.no/images/97-322719.pdf. Test: idle 5 min, expect disconnect.
- Clickatell: must send ≥1 enquire_link every **5 minutes** or be disconnected; recommends sending one every 1 minute for prompt lost-connection detection — https://archive.clickatell.com/developers/api-docs/connection-details/. Test: assert connection survives at 4:59 idle with no enquire_link, torn down by ~5:00.
- Kaleyra: client should enquire_link every 60s; platform auto-disconnects any link idle >5 minutes — https://messaging.kaleyra.com/support/solutions/articles/3000091795-introduction-to-smpp-interface. Test: same 5-minute idle-disconnect boundary assertion.
- Telesign: Enquire Link Timer = 30s; Response Timer = 30s for awaiting responses — https://developer.telesign.com/enterprise/docs/additional-features-smpp-protocol. Test: assert client sends enquire_link at ≤30s idle; assert a pending response times out at 30s.
- Route Mobile: client should enquire_link every minute; platform auto-disconnects any link inactive >5 minutes — routemobile.com PDF above. Test: simulate 5+ min silence, assert server closes socket.
- Bandwidth, Syniverse: no public keepalive numbers found — skipped.
## 3. Windowing / throughput
- Kannel `max-pending-submits`: max outstanding unacked SMPP ops between ESME/SMSC, default (recommended) 10 — Kannel User's Guide 1.4.5. Test: submit >10 concurrently without waiting for resp, assert Kannel throttles/queues further sends.
- Kannel `wait-ack` (default 60s) + `wait-ack-expire`: `0x00` disconnect/reconnect (default), `0x01` requeue ("could result in the msg arriving twice"), `0x02` keep waiting — Kannel User's Guide 1.4.5. Test: hold back a submit_sm_resp past `wait-ack` seconds and assert the configured expire action.
- Melrose Labs: deliver_sm window size = 25 (default) — https://melroselabs.com/services/smsc-simulator/smsc-simulator-technical-details/. Test: have the simulator queue >25 unacked deliver_sm and confirm it stalls further sends until acked.
- smpp.org free public test simulator: max 2 SMS/sec; paid tiers up to 100/sec or 10,000/sec — https://smpp.org/smpp-testing-development.html. Test: burst >2/sec against the free tier, expect throttling.
- Jasmin `submit_throughput`: MPS cap per connector, 0 = unlimited — https://docs.jasminsms.com/en/latest/management/jcli/modules.html. Test: exceed configured MPS on a connector, assert submissions delayed/rejected.
- `ESME_RTHROTTLED` (0x00000058/88, "ESME exceeded allowed message limits") vs `ESME_RMSGQFUL` (0x00000014/20, "Message Queue Full") — https://smpp.org/smpp-error-codes.html. Test: assert the library distinguishes these and applies different backoff (throttled = slow down; queue full = far-end resource, not rate, exhausted).
- SMPPSim: on receiving `ESME_RMSGQFUL` from the ESME for a deliver_sm, retries after a delay governed by `DELAYED_INBOUND_QUEUE_PROCESSING_PERIOD`/`DELAYED_INBOUND_QUEUE_MAX_ATTEMPTS` — https://github.com/haifzhan/SMPPSim/blob/master/SMPPSim_OFFICIAL_README. Test: reply ESME_RMSGQFUL to a deliver_sm, assert SMPPSim retries rather than dropping.
- Sinch: default throughput 10 msg/s per bind; recommended default window size 10 — https://developers.sinch.com/docs/sms/smpp/connectivity. Test: submit 11 concurrent unacked submit_sm, confirm throttling/rejection.
- Infobip: "no other TPS limitations besides internet speed"; scale via more binds (up to 4) rather than a hard cap — https://www.infobip.com/docs/essentials/api-essentials/smpp-specification. Test: ramp TPS on one bind and confirm no ESME_RTHROTTLED absent an explicit account limit.
- Telnyx: rate limits are **per-number**, not per-bind/window — 10 msg/number/min long code, 1200 msg/number/min toll-free — https://support.telnyx.com/en/articles/1667062-short-message-peer-to-peer-set-up-guide. Test: submit >10/min to the same long code sender, confirm per-number (not per-connection) throttling.
- Vonage: default 30 SMS/s per account; `ESME_RTHROTTLED`=0x00000058 returned on excess; no server-side queueing beyond this — https://api.support.vonage.com/hc/en-us/articles/204015703. Test: submit >30/s, assert 0x00000058 on the excess.
- Bird: provisioned bind-count + msg/s per account, enforced per SMPP server — https://docs.bird.com/connectivity-platform/other-integrations/how-can-i-use-the-short-message-peer-to-peer-smpp-network-protocol. Test: submit above provisioned rate, confirm a throttling status rather than silent unbounded queueing.
- Clickatell: recommends configuring the client with the account's max bind count and using async "windowing" (pipeline submit_sm without waiting per-resp) to reach full throughput — https://archive.clickatell.com/developers/api-docs/using-the-smpp-api/. Test: pipeline N outstanding submit_sm before the first resp arrives, confirm client doesn't serialize.
- LINK Mobility (Implementation Guide): default max window = 20 outstanding ops; default max throughput 10 msg/s per account (both configurable); exceeding either triggers throttling (ESME_RTHROTTLED=88), sometimes **without the vendor's own extension TLV present** — https://a.storyblok.com/f/151608/x/bb282a50a7/link-mobility-implementation-guide-smpp.pdf. Test: send >20 outstanding or >10/s, expect ESME_RTHROTTLED, possibly missing the vendor TLV.
- Telia: default window size 10, adjustable by contacting support — https://cdn.messaging.teliacompany.com/documents/developer/index.html. Test: send 11+ outstanding requests against the default window, expect throttling.
- Telenor: window size configurable 199, defaulting to 1 (stop-and-wait) — general SMSC doc, scope not confirmed SMPP-exclusive — https://developer.telenor.no/images/97-322719.pdf. Test: with window=1, verify strict request/response serialization required.
- Route Mobile: `ESME_RTHROTTLED`=0x00000058, no published numeric window/TPS — routemobile.com PDF. Test: assert 0x58 in submit_sm_resp triggers retry-with-backoff, not a hard failure.
- Bandwidth: baseline 1 MPS; Local A2P/10DLC default 1 MPS; toll-free default 3 MPS; short codes default 100 MPS; account-level limit overrides per-number limit; queue length default 15 min, max 4h; T-Mobile P2P capped at 300 unique recipients/day/sending number; **inbound (MO) has no rate limit** — https://www.bandwidth.com/support/en/articles/12823226 , https://www.bandwidth.com/support/en/articles/12823224. Test: burst MO traffic at the client, confirm no artificial inbound throttling expected; assert MT caps match documented MPS per number class.
- Syniverse: distinct codes 1039 "Throttling error" vs 1038 "Outgoing queue is full" (conceptually mirrors RTHROTTLED vs RMSGQFUL) — https://sdcsupport.syniverse.com/hc/en-us/articles/360038257473. Test: assert distinct handling paths exist for "throttled" vs "queue full" outcomes.
## 4. Message ids
- Kannel `msg-id-type` (03): `0x00` both decimal, `0x01` deliver_sm decimal/submit_sm_resp hex, `0x02` deliver_sm hex/submit_sm_resp decimal, `0x03` both hex — Kannel User's Guide 1.4.5. Test: set each value and assert submit_sm_resp id vs. DLR `id:` field parse in the matching base.
- Jasmin `dlr_msgid`: `0` = identical id in both submit_sm_resp and deliver_sm, `1` = submit_sm_resp hex / deliver_sm decimal, `2` = reverse — https://docs.jasminsms.com/en/latest/management/jcli/modules.html. Test: mirrors Kannel's quirk under a different config name — same base-mismatch bug class, two vendors, two spellings.
- Jasmin: internal message id is a UUID (gateway-side tracking) distinct from `id_smsc` (an integer — what the SMSC actually returned) — https://docs.jasminsms.com/en/latest/apis/http/index.html. Test: verify a test double returning a UUID as submit_sm_resp message_id is accepted (spec only requires a C-octet string, no fixed format).
- Melrose Labs simulator: message_id is 8 chars for bound v3.3 sessions, 64 chars for v3.4/v5 — https://melroselabs.com/services/smsc-simulator/smsc-simulator-technical-details/. Test: bind at 3.3 vs 3.4 against the simulator, assert id length differs.
- SMPPSim: randomised message ID start value with a configurable prefix (v2.6.0+) — not sequential/predictable — https://github.com/haifzhan/SMPPSim/blob/master/SMPPSim_OFFICIAL_README. Test: assert client doesn't assume sequential/incrementing ids from this simulator.
- Sinch (legacy Cloud SMPP): message_id is **hex on SMPP 3.3 binds, standard ASCII integer (decimal) on SMPP 3.4 binds** — same backend, format keyed off negotiated interface_version — https://developers.sinch.com/docs/sms/other/sms-other-cloud-smpp. Test: bind at 0x33 vs 0x34, confirm message_id format in submit_sm_resp changes accordingly.
- Vonage: by default, `submit_sm_resp.message_id` is **hex** while the corresponding `deliver_sm` DLR `id:` field is **decimal** — the two don't match without conversion unless the account is reconfigured; hex deprecated in 2023 in favor of UUID format for both — https://api.support.vonage.com/hc/en-us/articles/204015803 , https://api.support.vonage.com/hc/en-us/articles/204015663. Test: correlate submit_sm_resp message_id (hex) against DLR `id:` (decimal), confirm conversion is required to match; also test UUID-format ids on newer accounts.
- Bird: `receipted_message_id` TLV (tag 0x001E) in the DLR carries the **same value** as `submit_sm_resp.message_id` — https://developers.messagebird.com/api/sms-messaging/. Test: capture submit_sm_resp message_id, assert the DLR's receipted_message_id TLV matches byte-for-byte.
- Clickatell: message identity is carried in the DLR's `short_message` text (`id:xxx`) as well as via TLV — https://archive.clickatell.com/developers/api-docs/pdu-details/. Test: parse the DLR text body's `id:` token and confirm it matches submit_sm_resp's message_id.
- Telesign: `message_id` is a 32-digit hex value, random per request; for a concatenated message, submit_sm_resp returns the reference id **only for the first part** (a `message_parts_count` TLV covers the rest) — https://developer.telesign.com/enterprise/docs/smpp-protocol. Test: submit a message that auto-splits into N parts, assert only one message_id returns while `message_parts_count`=N.
- Bandwidth: `submit_sm_resp.message_id` is a **36-char UUID** (migrated from a legacy 10-char id), echoed back in the `receipted_message_id` TLV (0x1E) of the DLR — https://support.bandwidth.com/hc/en-us/articles/360046149833. Test: assert the library accepts a 36-char UUID-format message_id (not just numeric/short ids) and correlates via the TLV, not string-matching the DLR body.
- Route Mobile: only documents `ESME_RINVMSGID` (0x0C); no explicit hex/decimal format published — routemobile.com PDF. Test: N/A — document as an unpublished detail.
## 5. Delivery receipts
- spec: `sm_length`/`short_message` max 254 octets; `sm_length` must be 0 when `message_payload` TLV is used to carry >254 octets of user data — spec §1.2.21/1.2.22 via sysop.fr mirror. Test: assert a client switches to `message_payload` with `sm_length=0` once payload exceeds 254 octets rather than truncating.
- spec `esm_class` bit meanings for MC→ESME (deliver_sm): `0x00` normal message, `0x04` "Short Message contains SMSC Delivery Receipt", `0x08` SME delivery acknowledgement, `0x10` SME manual/user acknowledgement, `0x20` "Intermediate Delivery Notification" — spec via sysop.fr mirror. Test: assert a DLR arrives with esm_class 0x04 (not 0x00) and an intermediate notification with 0x20 — a naive "esm_class != 0 means DLR" check wrongly classifies intermediates as final.
- spec `registered_delivery` bits 1,0: `01` receipt on success or failure, `10` receipt on failure only, `00` none (default); intermediate notification requested via `registered_delivery & 0x10 == 0x10` — corroborated by SMPPSim ("set to 0x11 for both intermediate and final") — spec via sysop.fr mirror; https://github.com/haifzhan/SMPPSim/blob/master/SMPPSim_OFFICIAL_README. Test: request 0x01 vs 0x02 vs 0x11, assert the right receipt combination arrives.
- smpp.org receipt text format: `id` (variable), `sub` (3 octets, zero-padded), `dlvrd` (3 octets, zero-padded), `submit date`/`done date` (10 octets, YYMMDDhhmm), `stat` (7 octets), `err` (3 octets), `text` (**20 octets, documented as "unused field, result will be blank"** — not truncated message content, contrary to some vendor implementations) — https://smpp.org/smpp-delivery-receipt.html. Test: assert `text:` is blank by default and that a parser doesn't assume it always carries 20 chars of truncated body text.
- smpp.org's own `message_state`-numbered stat list: 0 SCHEDULED (intermediate), 1 ENROUTE (intermediate), 2 DELIVERED, 3 EXPIRED, 4 DELETED, 5 UNDELIVERABLE — https://smpp.org/smpp-delivery-receipt.html — cross-validated against Telesign's TLV table (`message_state`: 2=DELIVRD, 3=EXPIRED, 4=DELETED, 5=UNDELIV, 6=ACCEPTD, 7=UNKNOWN, 8=REJECTD, 9=SKIPPED vendor-added) — https://developer.telesign.com/enterprise/docs/sms-smpp-tlvs. Test: assert the numeric `message_state` TLV and the 7-char `stat:` text field are kept in sync by any test double (they're two encodings of the same fact and can drift).
- Melrose Labs simulator: DLR TLVs `receipted_message_id`, `message_state` (2=DELIVERED), `network_error_code` (0=no error); DLR "returned <1s after submit_sm_resp"; MO deliver_sm echoes the `data_coding` used on the original submit — https://melroselabs.com/services/smsc-simulator/smsc-simulator-technical-details/. Test: assert DLR arrives within ~1s and that MO deliver_sm's data_coding matches the original submit's.
- Jasmin `dlr_level`: `1`=SMS-C level, `2`=Terminal level, `3`=Both — https://docs.jasminsms.com/en/latest/apis/http/index.html. Test: request each level, confirm the number/timing of DLR callbacks differs (SMSC-level fires once at ack; terminal-level needs a simulated MO ack).
- Sinch: DLR body follows spec Appendix B; deliver_sm inbound fields (service_type, esm_class, protocol_id, priority_flag, schedule_delivery_time, validity_period, registered_delivery, replace_if_present_flag, sm_default_msg_id) are "always 0"; optional vendor TLV 0x1403 carries destination MCC+MNC — https://developers.sinch.com/docs/sms/smpp/send-message , https://developers.sinch.com/docs/sms/smpp/receive-message. Test: parse deliver_sm and confirm those fields are always zero; check for optional TLV 0x1403.
- Infobip: DLR format `id: sub: dlvrd: submit date: done date: stat: err:`; documented stat values include **DELIVRD, EXPIRED, UNDELIV, ACCEPTD, UNKNOWN, ENROUTE, REJECTD** — note ENROUTE appears as a possible DLR stat here, not just an intermediate flag — https://www.infobip.com/docs/essentials/api-essentials/smpp-specification. Test: assert the parser accepts `stat:ENROUTE` as a valid value, not just the plain 6-value spec set.
- Vonage: DLR format `id: sub:001 dlvrd:000 submit date: done date: stat: err: text:none`; `err:` only populated (non-000) when state ≠ DELIVRD/ACCEPTD; documented stat values: **ACCEPTD, DELIVRD, UNDELIV, EXPIRED, FAILED, REJECTD, DELETED, UNKNOWN** (8 values, including FAILED/DELETED beyond the plain spec set); **one DLR per segment** for concatenated messages, not one per logical message; retries deliver_sm toward the ESME on temporary-failure command_status, up to 130 attempts ≥62s apart, discarding after 24h — https://api.support.vonage.com/hc/en-us/articles/204015663 , https://api.support.vonage.com/hc/en-us/articles/204015773 , https://developer.vonage.com/en/messaging/sms/guides/delivery-receipts. Test: (a) assert err:000 only on DELIVRD/ACCEPTD; (b) return a temporary-failure status on deliver_sm_resp, confirm Vonage retries rather than treating it as final; (c) confirm one DLR per segment on a multi-part SMS.
- Bird: `message_state` TLV (0x0427) = final receipt state; `network_error_code` TLV (0x0423) = carrier-level error, distinct from the text `err:` value — both map into Bird's own numeric error table (0131) — https://developers.messagebird.com/api/sms-messaging/. Test: build DLR fixtures with both TLVs set and assert the parser surfaces them independently rather than conflating carrier code with SMPP err digits.
- Clickatell: DLR body exactly `id:xxx sub:001 dlvrd:NNN submit date:YYMMDDHHMMSS done date:YYMMDDHHMMSS stat:xxx err:000 text:`; observed stat values DELIVRD/ACCEPTD/REJECTD/UNDELIV — https://archive.clickatell.com/developers/api-docs/pdu-details/. Test: parse against this exact field order, reject malformed variants (missing `sub:`, wrong date length).
- Kaleyra: DLR only returned if `registered_delivery=1` **and** bound as receiver/transceiver (TX-only gets none); stat values DELIVRD/FAILED/EXPIRED/UNDELIV with an extensive per-operator err code table (000 DELIVRD, 001 unknown subscriber, 003/010 absent subscriber, 004/006/007 handset error, 005 barred, 008/012 net error, 011 SMSC system failure, 013 mobile off, 016 handset busy, 023032 DLT/template failures, hex 10241911 for blacklisting/DND/etc) — https://messaging.kaleyra.com/support/solutions/articles/3000091798-delivery-reports , https://messaging.kaleyra.com/support/solutions/articles/3000091797-error-codes-and-descriptions. Test: (a) submit with registered_delivery=0, assert no DLR ever arrives; (b) submit with registered_delivery=1 on a TX-only bind, assert none arrives there; (c) table-driven test mapping numeric err code to stat value.
- tyntec: sends a **temporary "buffered" DLR shortly after submission, followed by a final DLR later** — two receipts per message is expected, not a bug; large GSM error code table (0x0000 no error, 0x0001 unknown subscriber, 0x0006 absent subscriber, 0x000d call barred, 0x0021 message-waiting-list full, 0x0022 system failure, 0x6000 SIM memory full, 0xa001 no network response, 0xa002 message too long) — https://www.tyntec.com/helpcenter/docs/faqs/sms/sms-delivery/how-can-i-troubleshoot-sms-delivery/ , https://www.tyntec.com/helpcenter/docs/faqs/sms/sms-delivery/gsm-error-codes/. Test: assert two deliver_sm's for one submit_sm are accepted, not treated as a duplicate.
- CM.com: DLR format `id:(32 chars) submit date: done date: stat: err:`; confirmed 7-char stat values DELIVRD/EXPIRED/DELETED/UNDELIV/ACCEPTD/UNKNOWN/REJECTD; sent via deliver_sm's short_message OR via data_sm's message_payload depending on account config; also carries a `message_state` TLV; proprietary TLVs `operator` (0x1401, MCC/MNC) and `tariff` (0x140A) — https://developers.cm.com/messaging/docs/smpp. Test: parse both text-body stat and message_state TLV, assert agreement; assert exactly 7-char stat.
- LINK Mobility (SMSC-SMPP User Guide): stat values DELIVRD/EXPIRED/REJECTD/UNDELIV/DELETED; extended format `sub:000 dlvrd:000 submit date: done date: stat: err: text:` where **`sub`/`dlvrd` are always literally "000" and `text` is always empty**; only final delivery reports supported, **no intermediate receipts** — https://www.linkmobility.com/resources/developer/SMSC-SMPP-User-Guide-1.5.pdf. Test: assert sub/dlvrd are literally "000" and text is empty on every DLR from this profile; assert requesting an intermediate notification has no effect.
- LINK Mobility (Implementation Guide): `esm_class` always 0x04 for DLRs (unless configured for legacy "Logica style" text-only DRs); TLVs `message_state` (0x0427), `network_error_code` (0x0423), `receipted_message_id` (0x001E); vendor extension TLVs 0x17000x1706 (timestamp/operator/operator_timestamp/status_code/reason_code/status_text/message_id); non-standard `registered_delivery=0x21` ("request server delivery report") beyond the spec's normal value set — https://a.storyblok.com/f/151608/x/bb282a50a7/link-mobility-implementation-guide-smpp.pdf. Test: bind with/without "Logica style DR" config, assert DR shape differs (TLV-based vs legacy text); test 0x21 as a vendor-extension registered_delivery value.
- Telia: `network_error_code` TLV values SUBSCRIBER_UNKNOWN(201), SUBSCRIBER_TEMPORARILY_BARRED(202), REFUSED_BY_SERVICE_PROVIDER(207), SUBSCRIBER_ABSENT(208), ILLEGAL_EQUIPMENT(209) — https://cdn.messaging.teliacompany.com/documents/developer/index.html. Test: assert network_error_code values map to this reason set on simulated failure receipts.
- Twilio: supports DLRs across short/long code, 10DLC, alphanumeric via its enterprise SMPP API but explicitly **not** Messaging-Service-specific REST features (link shortening, scheduling, advanced opt-out) via that path — https://help.twilio.com/articles/31537798746267. Test: assert SMPP-submitted messages don't trigger REST-only status-callback features.
- Telesign: DLR presence signaled by esm_class bit 2; `err` is a 3-octet hex code with 40+ vendor values (e.g. 0x000004A6 blocked, 0x000004C2 sender-ID restriction); status given redundantly in both text body and TLVs; `message_parts_count` TLV reports segment count; DLRs route to a random connected data-center unless `system_type` targets one — https://developer.telesign.com/enterprise/docs/smpp-protocol , https://developer.telesign.com/enterprise/docs/sms-smpp-tlvs. Test: assert DLR text and TLV state never disagree; assert message_state=9 maps to a SKIPPED state most other vendors lack.
- Route Mobile: DLR only if bound RX/TRX and registered_delivery=1; base stats DELIVRD/FAILED/EXPIRED/UNDELIV/REJECTD plus a numeric 000412 status-error table (e.g. 001 unidentified subscriber→UNDELIV, 027 absent subscriber→EXPIRED, 408 DND→REJECTD, 411 duplicate submission→REJECTD) — routemobile.com PDF. Test: fixture-test each numeric code against its documented stat bucket.
- Bandwidth: `ACCEPTD` is "the most precise indicator of a successful message" (vs `DELIVRD` = actual handset confirmation); DLR listening window is **73 hours**, after which error 902/9902 (not a failure) or 4794 ("expired by carrier") if a late DLR does arrive; international "Intermediate DLR" only confirms carrier hand-off, not handset delivery, and DLRs aren't guaranteed internationally — https://www.bandwidth.com/support/en/articles/12823143 , https://www.bandwidth.com/support/en/articles/12823161 , https://www.bandwidth.com/support/en/articles/12823152. Test: assert a DLR arriving after 73h is flagged distinctly (902/9902/4794), not as generic UNDELIV; assert an intermediate DLR isn't conflated with terminal DELIVRD.
- Syniverse: explicit code table — DELIVRD=0, DELIVERED-TO-CARRIER=3/ACCEPTED-BUFFERED=4 (interim success), DELETED=5, UNDELIV=7/9/90/181/190, EXPIRED=8, REJECTD=23/35, INTERNAL ERROR=98, BAD ADDRESS=90, INVALID ROUTING=91, CARRIER GATEWAY ERROR=93, intermediate carrier states 184191 ("UNDELIV-INTERIM"), UNKNOWN=999 — https://sdcsupport.syniverse.com/hc/en-us/articles/360038257473. Test: fixture-test each numeric code against its documented interim-vs-terminal classification.
## 6. Long messages
- spec: `message_payload` TLV holds up to 64K octets — spec via smpp.org PDF fetch. Test: cap concatenation/payload tests at 64K.
- spec segmentation TLV tags: `sar_msg_ref_num`=0x020C, `sar_total_segments`=0x020E, `sar_segment_seqnum`=0x020F — spec via sysop.fr mirror. Test: submit a 3-segment message via sar_* TLVs (not UDH), confirm the receiver reassembles using the TLVs.
- UDH 8-bit vs 16-bit reference number, MO reassembly rules, max-segment limits, and a hard ">160 char rejection" rule: **not documented in the SMPP spec itself** (GSM 03.40 territory) — spec review. unverified — folklore beyond vendor-specific figures below.
- Sinch: auto-splits oversized single submissions server-side; client is not required to pre-segment — https://developers.sinch.com/docs/sms/smpp/send-message. Test: submit a single long body exceeding one segment, confirm Sinch splits it rather than rejecting.
- Infobip: **long SMS not supported for Flash notifications** — a concatenated + flash (data_coding 0x10/0x18) combination is unsupported — https://www.infobip.com/docs/essentials/api-essentials/smpp-specification. Test: submit a >1-segment flash SMS, confirm rejection/silent truncation.
- Vonage: UDH concatenation via esm_class=0x40 (UDHI bit); char budgets 160/306/459/612 for 14 part GSM-7 (≈153/part after UDH), 70 for single UCS-2 segment; accepts up to 3200 chars but "not all carriers do" — recommends max 6 parts; **all parts must go through the same SMPP server/gateway** or the handset never reassembles; **20-minute window** to submit all parts of one multipart SMS; MO concat params (`concat`/`concat-ref`/`concat-total`/`concat-part`) absent for US Sprint/Verizon — recommends grouping by same-sender + close timestamp as fallback — https://developer.vonage.com/en/messaging/sms/guides/concatenation-and-encoding , https://api.support.vonage.com/hc/en-us/articles/360046874352 , https://api.support.vonage.com/hc/en-us/articles/11615117187484 , https://api.support.vonage.com/hc/en-us/articles/205704158. Test: (a) submit segments with the same UDH ref to two different binds, confirm no reassembly; (b) delay a segment past 20 min, confirm the multipart message fails; (c) simulate MO concat without concat-ref params, confirm the client falls back to a heuristic.
- Bird: no public max-segment/UDH-vs-sar specifics beyond what's in topic 5/7 — skip.
- Clickatell: `message_payload` (up to 64K) as an alternative to short_message; optional `sar_msg_ref_num`/`sar_segment_seqnum`/`sar_total_segments` TLVs supported on submit_sm — https://archive.clickatell.com/developers/api-docs/using-the-smpp-api/ , https://archive.clickatell.com/developers/api-docs/pdu-details/. Test: submit >160 chars via message_payload only (short_message len 0), confirm accepted; submit the same via sar_* TLVs, confirm equivalent behaviour.
- Kaleyra: no `submit_multi`; concatenation via esm_class=0x43 ("Store and Forward with UDHI") with UDH inside short_message; `message_payload` (≤64K) **only available at interface_version 0x34** — requesting it under 3.3 silently has no effect — https://messaging.kaleyra.com/support/solutions/articles/3000091796-submitting-messages-through-smpp. Test: bind at 0x33, attempt message_payload, confirm rejected/ignored; works after rebinding at 0x34.
- tyntec: without UDH-based concatenation, MO/MT segment **order is not guaranteed**; fallback is appending a page marker (`1/3, 2/3, 3/3`) to each part's text — https://www.tyntec.com/helpcenter/docs/faqs/sms/sms-features/will-my-sms-messages-arrive-in-order-in-case-of-concatenated-sms/. Test: send 3 segments out of order from a mock SMSC, confirm the client reassembles by sar/UDH sequence number rather than arrival order.
- CM.com: no long-message specifics beyond DLR/payload routing already in topic 5 — skip.
- LINK Mobility (Implementation Guide): UDH only, signaled purely via esm_class bit 6 (0x40 present / 0x00 absent) — **no sar_* TLVs, no message_payload** supported; non-GSM chars force a 70-char UCS2 segment, not 160-char GSM — https://a.storyblok.com/f/151608/x/bb282a50a7/link-mobility-implementation-guide-smpp.pdf. Test: assert non-GSM chars force UCS2 70-char splitting, not GSM 160.
- LINK Mobility (SMSC-SMPP User Guide): `message_payload` TLV **not supported** — "only one SMS may be delivered per call" — https://www.linkmobility.com/resources/developer/SMSC-SMPP-User-Guide-1.5.pdf. Test: submit with message_payload against this profile, expect rejection/ignored.
- Telia: `message_payload` explicitly supported to avoid splitting into separate submit_sm; 153 GSM chars/segment when concatenating; REST API (separate from SMPP) caps at 16 segments — https://cdn.messaging.teliacompany.com/documents/developer/index.html. Test: assert 153-char GSM segment boundary for concatenated (not single) SMS via SMPP.
- Telesign: base SMS 140 bytes; single-message limits GSM7=160/ASCII=140/Latin-1=140/UTF-16BE=70; per-segment limits when concatenated GSM7=153/ASCII=134/Latin-1=134/UTF-16BE=67 (6-byte UDH overhead); **max 10 segments**; "smart splitting" avoids breaking words/URLs across segments; auto-splits an oversized single message and adds UDH per part, billed per part — https://developer.telesign.com/enterprise/docs/understand-sms-encoding-character-limits-and-splitting , https://developer.telesign.com/enterprise/docs/smpp-protocol. Test: assert segmentation breakpoints match 160/153 (GSM7) and 70/67 (UCS2); refuse an 11th segment.
- Route Mobile: **no `submit_multi` or `sar_*` TLVs at all**; concatenation only via UDH with esm_class=0x43; `message_payload` up to 64K but only at interface_version=0x34; strict UDH length validation — `ESME_RINVUDHLEN` (0x406) unless UDH length is 05/06 (any data_coding) or 0b (only data_coding=245) — routemobile.com PDF. Test: submit with sar_msg_ref_num TLV set, assert rejection (`ESME_ROPTPARNOTALLWD`); submit UDH length 07, assert `ESME_RINVUDHLEN`.
- Syniverse: GSM7 single=160/fragment=152 chars; UCS-2 single=70/fragment=67 chars; max **10 fragments**; extended GSM chars (€, ^, |, etc.) count as **2 septets** each — https://sdcsupport.syniverse.com/hc/en-us/articles/360010947674. Test: assert a message containing `€` consumes 2 GSM7 code units for segmentation-count purposes.
## 7. Encodings
- spec `data_coding` table: `0x00` SMSC Default Alphabet, `0x01` IA5/ASCII, `0x02` Octet unspecified (8-bit binary), `0x03` Latin-1, `0x04` Octet unspecified (8-bit binary) — **spec itself lists 0x02 and 0x04 as duplicate "binary" entries**; `0x05` JIS, `0x06` Cyrillic, `0x07` Latin/Hebrew, `0x08` UCS2, `0x09` Pictogram, `0x0A` ISO-2022-JP, `0x0D` Extended Kanji JIS, `0x0E` KS C 5601, `0xCC``0xDF` GSM MWI control, `0xF0``0xFF` GSM message class control (where flash/class-0 lives — the common "0x10/0x18" shorthand is a non-standard vendor simplification of this range) — spec via sysop.fr mirror. Test: verify the library picks one binary code (most vendors use 4, not 2) and documents the choice; verify flash handling covers the 0xF0-0xFF range, not just the two literal bytes 0x10/0x18.
- spec `esm_class` UDHI bit 0x40 ("UDHI Indicator") — spec via sysop.fr mirror. Test: an 8-bit binary message (data_coding=4) carrying UDH must set esm_class 0x40; a decoder should flag UDH-shaped payload bytes arriving without this bit set.
- Jasmin's `coding` parameter mirrors the spec's DCS table 1:1 (0=SMSC default…8=UCS2…13=Extended Kanji JIS, 14=KS C 5601) — https://docs.jasminsms.com/en/latest/management/jcli/modules.html. Test: round-trip all Jasmin-listed DCS values through the parser.
- National language shift tables: not found documented in any researched source — unverified — folklore.
- Sinch: data_coding 0x00 GSM-7 default, 0x01 US-ASCII, 0x02/0x04 binary, 0x03 Latin-1, 0x08 UCS2/UTF-16BE — **but "only characters within the GSM-7 table can be parsed to handset" for DCS 3**, i.e. Latin-1 bytes outside GSM-7 mis-render on many handsets — https://developers.sinch.com/docs/sms/smpp/message-encoding. Test: send data_coding=3 with non-GSM-7 chars, confirm mis-rendering is documented/expected, not a bug to "fix".
- Infobip: DCS 0/1 = GSM7/IA5 default, 3 = Latin-1, 8 = Unicode/UCS-2; flash uses DCS **16 or 24** (both valid) — https://www.infobip.com/docs/essentials/api-essentials/smpp-specification. Test: submit flash with both 0x10 and 0x18, confirm both accepted.
- Vonage: **default encoding for new accounts is Latin-1 (ISO-8859-1), not GSM-7** — a real provisioning-default quirk; example DCS: Latin-1=3, Cyrillic=6, Hebrew=7, Unicode=8 — https://api.support.vonage.com/hc/en-us/articles/204015683 , https://api.support.vonage.com/hc/en-us/articles/204015813. Test: send GSM-7-only text on a fresh Vonage account without specifying data_coding, confirm it defaults to 0x03, not 0x00.
- Bird: supported data_coding {0,1,2,8}; any other value received is **remapped to the "most appropriate" of 0/2/8** before forwarding to the operator — https://developers.messagebird.com/api/sms-messaging/. Test: submit with data_coding=3, assert the outbound MT is remapped to one of {0,2,8}, not passed through verbatim.
- Kaleyra: data_coding=0 GSM 03.38, =3 Latin-1, =8 expects **UTF-16 Big Endian**; data_coding=1 (ASCII) explicitly "NOT RECOMMENDED, known to cause problems" — https://messaging.kaleyra.com/support/solutions/articles/3000091796-submitting-messages-through-smpp. Test: submit UCS-2 as little-endian vs big-endian, confirm only big-endian round-trips; lint against data_coding=1.
- LINK Mobility (Implementation Guide): data_coding table 0x00 default/GSM, 0x01 IA5→GSM, 0x02/0x04 binary, 0x03 Latin-1, **0x050x07 not supported**, 0x08 UCS2; regardless of requested DCS, everything is **transcoded into GSM or Unicode only** before reaching the handset (arbitrary DCS is not preserved on the wire) — https://a.storyblok.com/f/151608/x/bb282a50a7/link-mobility-implementation-guide-smpp.pdf. Test: submit data_coding=3 (Latin-1), assert the network delivers transcoded GSM/Unicode, not raw Latin-1.
- Telia: configurable default among GSM8/GSM7/Latin-1/Latin-9; UCS-2 for non-GSM content — https://cdn.messaging.teliacompany.com/documents/developer/index.html. Test: assert UCS-2 required/accepted for chars outside the GSM 7-bit default alphabet.
- Telesign: data_coding 0/1 = GSM 03.38 default, 3 = Latin-1 (SMPP-only), 8 = Unicode/UTF-16BE; 0/1/3 "may result in ASCII encoding if the input stream can't be decoded" as GSM/Latin-1 — https://developer.telesign.com/enterprise/docs/smpp-protocol. Test: send non-GSM7-valid bytes with data_coding=0, confirm documented ASCII fallback rather than a hard failure.
- Route Mobile: data_coding=0 default GSM 03.38; =1 also GSM 03.38 but **explicitly flagged "NOT RECOMMENDED... known to cause problems"**; =3 Latin-1 "if and only if told so explicitly"; =8 Unicode, body expected **UTF-16 Big Endian** — routemobile.com PDF. Test: assert the library warns against data_coding=1 and defaults new integrations to 0; assert UCS-2 bytes are big-endian.
- Syniverse: customers submit UTF-8; the system auto-detects and, if **any** character isn't GSM-7, converts the **entire message** to UCS-2 (no partial/mixed encoding) — https://sdcsupport.syniverse.com/hc/en-us/articles/360010947674. Test: send one emoji mixed into otherwise-GSM7 text, confirm the whole message is segmented under UCS-2 limits, not GSM7's.
## 8. Addresses
- spec TON table: 0 Unknown, 1 International, 2 National, 3 Network Specific, 4 Subscriber Number, 5 Alphanumeric, 6 Abbreviated. NPI table: 0 Unknown, 1 ISDN(E163/E164), 3 Data(X.121), 4 Telex(F.69), 6 Land Mobile(E.212), 8 National, 9 Private, 10 ERMES, 14 Internet(IP), 18 WAP Client Id — spec via sysop.fr mirror. Test: alphanumeric sender should be TON=5; the spec treats address as a C-octet digit string and doesn't mandate '+' handling — test both '+' and bare-digit forms are accepted.
- Melrose Labs: destination_address <8 chars → `ESME_RINVDSTADR` — see topic 1. Test: useful edge case specifically for short-code-length destination addresses.
- Kannel: default source-addr TON=2/NPI=1 — Kannel User's Guide 1.4.5 (see topic 1).
- Sinch: alphanumeric sender TON=5/NPI=0, max 11 chars; MSISDN sender TON=1/NPI=1, max 18 chars; destination must be E.164 — https://developers.sinch.com/docs/sms/smpp/send-message. Test: submit a 12-char alphanumeric sender or non-E.164 destination, confirm rejection/truncation.
- Infobip: shortcode→TON 3/NPI 0; alphanumeric→TON 5/NPI 0; "+"-prefixed→TON 1/NPI 1; else→TON 0/NPI 1; recommends TON/NPI=1 + E.164 as most reliable; `address_range` optional unless the account requires it — https://www.infobip.com/docs/essentials/api-essentials/smpp-specification. Test: submit each TON/NPI combo against its matching address shape, confirm acceptance; submit mismatched combos, check rejection vs silent tolerance.
- Telnyx: bind requires addr_ton=1/addr_npi=1, fixed — https://support.telnyx.com/en/articles/1667062-short-message-peer-to-peer-set-up-guide. Test: bind with different values, confirm only 1/1 accepted.
- Vonage: documented alphanumeric-sender example uses **TON=5, NPI=1** — notably NPI=1, not the NPI=0 convention used by Infobip/Sinch for alphanumeric senders — https://api.support.vonage.com/hc/en-us/articles/204015853. Test: submit alphanumeric sender with NPI=0 vs NPI=1 against Vonage, confirm which the documented example actually expects (cross-vendor inconsistency worth its own regression test).
- Kaleyra: TON 1=International MSISDN, 3=National/Network Short Code, 5=Alphabetic — https://messaging.kaleyra.com/support/solutions/articles/3000091796-submitting-messages-through-smpp. Test: submit ton=5 with an alphanumeric source_addr, confirm accepted.
- Clickatell: TON/NPI "auto detected" rather than strictly validated; alphanumeric sender "not available on all networks" — downgraded/rejected per destination operator rather than at the SMPP layer — https://archive.clickatell.com/developers/api-docs/using-the-smpp-api/. Test: submit an alphanumeric sender to an operator that doesn't support it, confirm the failure surfaces as a per-operator DLR err, not an SMPP submit_sm_resp rejection.
- LINK Mobility (Implementation Guide): source addr_ton restricted to 1 (MSISDN Int'l)/2 (national short code)/5 (alphanumeric); addr_npi ignored entirely — https://a.storyblok.com/f/151608/x/bb282a50a7/link-mobility-implementation-guide-smpp.pdf. Test: submit with an unlisted TON (e.g. 3), check rejection/fallback.
- LINK Mobility (SMSC-SMPP User Guide): source TON options Alphanumeric(5)/International(1)/National(2)/Network-specific(3)/Subscriber(4)/Abbreviated(6), each NPI Unknown(0) or ISDN(1); **destination restricted to International(1) TON only** — https://www.linkmobility.com/resources/developer/SMSC-SMPP-User-Guide-1.5.pdf. Test: submit a national-format (non-E.164) destination TON, expect rejection.
- Telia: alphanumeric sender TON=5/NPI=0, max 11 chars; International TON=1/NPI=1; Short number TON=3/NPI=0 — https://cdn.messaging.teliacompany.com/documents/developer/index.html. Test: assert alphanumeric sender rejected/truncated beyond 11 chars.
- Telesign: destination requires dest_addr_ton=1/npi=1; source addresses require prior whitelisting/approval before use — https://developer.telesign.com/enterprise/docs/smpp-protocol. Test: submit with dest_addr_ton=0, confirm rejection per this vendor's stated model.
- Route Mobile: source_addr_ton Alphanumeric=5, National/Network Short Code=3, International MSISDN=1 (default); **destination TON/NPI not enforced at all** — "always interpreted as 1," destination must be international format **without leading "00"** — routemobile.com PDF. Test: submit dest_addr_ton=9 (garbage), confirm still accepted and treated as TON=1; submit a destination with leading "00", confirm rejection/mis-routing.
- Syniverse: alphanumeric sender requires per-operator registration/approval (no TON/NPI published); short codes 56 digits — https://sdcsupport.syniverse.com/hc/en-us/articles/360019714593 , https://sdcsupport.syniverse.com/hc/en-us/articles/229476208. Test: validate short-code source addresses are 56 digits.
## 9. Time fields
- Absolute format `YYMMDDhhmmsstnnp`: YY year, MM month, DD day, hh hour, mm minute, ss second, t tenths of a second, nn UTC time-differential in quarter-hour units, p = `+`/`-` — Kannel devel mailing list quoting the spec, https://www.kannel.org/pipermail/devel/2009-April/002384.html. Test: build an absolute validity_period string per this layout and confirm it parses.
- Relative format: replace `p` with `R`; example `000000000500000R` = "5 minutes validity from message receipt" — same source. Test: submit a relative validity_period, confirm the SMSC computes expiry from its own receipt time, not the client's clock.
- Kannel real-world quirk: "will send validity in utc therefore 00+ is hardcoded" — Kannel never emits a non-zero UTC offset even though the format supports one — same Kannel devel-list source. Test: assert a test against Kannel doesn't expect timezone-aware absolute validity_period.
- Sinch: schedule_delivery_time supports both absolute and relative; max 168 hours (1 week) ahead — https://developers.sinch.com/docs/sms/other/sms-other-cloud-smpp. Test: schedule >168h out, confirm rejection.
- Infobip: relative format worked example `"070605040302100R"` = 7y 6mo 5d 4h 3m 2s 1 tenth-sec, relative — https://www.infobip.com/docs/essentials/api-essentials/smpp-specification. Test: submit that exact string in schedule_delivery_time/validity_period, confirm Infobip parses it as relative.
- Telesign: validity_period is a 17-octet C-octet string; absolute form uses `+`/`-` UTC offset in quarter-hours as the last char; relative form uses `R` there instead with unused fields zeroed ("relative to current MC time") — https://developer.telesign.com/enterprise/docs/smpp-protocol. Test: build both absolute (+/-) and relative (R) strings per this layout, assert both parse.
- Route Mobile / Syniverse: only document the *error* codes (`ESME_RINVEXPIRY` 0x62 for Route Mobile; 1045 "Invalid Validity Period"/1040 "Invalid scheduled delivery time" for Syniverse) — no accepted-format detail published — routemobile.com PDF; https://sdcsupport.syniverse.com/hc/en-us/articles/360038257473. Test: assert malformed validity_period strings trigger these vendor-specific error codes.
- Clickatell: documents validity_period as a mechanism to stagger bulk-send delivery when the gateway has queueing delays — a throttling/scheduling tool, not just a TTL — https://archive.clickatell.com/developers/api-docs/validity-period-advanced-message-send/. Test: set a short validity_period on a message stuck behind a large queued batch, confirm it expires (stat:EXPIRED) rather than delivering late.
- LINK Mobility (SMSC-SMPP User Guide): recommends validity_period ≥15 minutes; `schedule_delivery_time` is **unsupported** on submit_sm for this profile — https://www.linkmobility.com/resources/developer/SMSC-SMPP-User-Guide-1.5.pdf. Test: submit with schedule_delivery_time set, expect rejection/ignoring on this profile.
- Absolute-vs-relative rejection rules for the remaining vendors (Bird, Clickatell beyond above, Kaleyra, tyntec, CM.com, Bandwidth): not publicly documented — unverified — folklore.
## 10. Error handling
- spec `generic_nack` triggers: invalid `command_length` ("assume the data is corrupt... a generic_nack PDU must be returned") and unknown `command_id` ("a generic_nack PDU must also be returned") — spec via smpp.org PDF fetch. Test: send a PDU with a bogus command_id, assert generic_nack, not a silent drop or close.
- spec session states OPEN/BOUND_TX/BOUND_RX/BOUND_TRX/CLOSED gate which PDUs are valid when; out-of-state PDUs aren't given an explicit error rule in the spec text itself, but `ESME_RINVBNDSTS` (0x00000004, "Incorrect BIND Status for given command") is the closest documented code — spec via smpp.org PDF fetch. Test: send deliver_sm on an unbound connection, expect ESME_RINVBNDSTS or a connection close, not silent processing.
- spec `outbind`: lets the SMSC signal an ESME to originate a bind_receiver, typically because the SMSC has outstanding messages for it (§2.2.1) — spec via smpp.org PDF fetch. Test: simulate an SMSC-initiated outbind, assert the ESME responds by binding as receiver.
- spec `alert_notification`: SMSC-issued in BOUND_RX/BOUND_TRX, and is the **one PDU with no response** — spec via smpp.org PDF fetch. Test: after receiving alert_notification, assert the ESME sends back no response PDU (unlike every other request).
- spec `data_sm`: uniquely supports Transaction Message Mode; submit_sm does **not** support transaction mode (§2.10.2/2.10.3/§4.4) — spec via smpp.org PDF fetch. Test: assert the library rejects/flags an attempt to request transaction mode via submit_sm's esm_class instead of data_sm.
- Whether submit_sm_resp with non-zero status still carries a body: not confirmed either way in the reachable spec text — unverified — folklore.
- Vonage: `command_status=0x00000005` (ESME_RALYBND) beyond the 2-connection default; `0x0d`/13 decimal (ESME_RBINDFAIL) for wrong host/zone/creds; on deliver_sm, only status codes the spec's table marks "temporary failure" trigger a Vonage retry — everything else is final, no retry — https://api.support.vonage.com/hc/en-us/articles/204015713 , https://api.support.vonage.com/hc/en-us/articles/204015783 , https://api.support.vonage.com/hc/en-us/articles/204015773. Test: return each documented temporary vs non-temporary status in deliver_sm_resp, confirm retry only for the temporary set.
- Bird: DLR text `err:` and `network_error_code` TLV both index into Bird's own proprietary 0131 error table, distinct from the underlying carrier's raw code — https://developers.messagebird.com/api/sms-messaging/. Test: table-driven test asserting err code → Bird meaning, kept separate from any carrier-code test.
- Kaleyra: successful submit_sm_resp returns error code 0 + non-null message reference; failure returns "a Kaleyra vendor specific error code" — not necessarily a standard SMPP ESME_* value — https://messaging.kaleyra.com/support/solutions/articles/3000091796-submitting-messages-through-smpp. Test: assert error handling tolerates/logs unrecognized vendor codes rather than assuming the standard ESME_* enum is exhaustive.
- LINK Mobility (Implementation Guide): generic_nack supported bidirectionally; client must handle SMSC-initiated unbind and respond before disconnecting, then wait ≥30s before reconnecting; outbind not mentioned (implicitly unsupported); ESME_RSYSERR(8)/ESME_RTHROTTLED(88) responses **lack the vendor extension TLV** even when the extension feature is enabled — https://a.storyblok.com/f/151608/x/bb282a50a7/link-mobility-implementation-guide-smpp.pdf. Test: assert reconnect within 30s of an unbind is refused; assert RSYSERR/RTHROTTLED responses lack the vendor extension even with it turned on.
- Telenor: OUTBIND/SUBMIT_MULTI/ALERT_NOTIFICATION unsupported (see topic 1); GENERIC_NACK is in the supported-operations list — https://developer.telenor.no/images/97-322719.pdf.
- Telia: ESME must send unbind before closing the TCP connection; SMSC can also unilaterally unbind for maintenance — https://cdn.messaging.teliacompany.com/documents/developer/index.html. Test: close TCP without unbind vs proper unbind, compare vendor-side session-cleanup expectations.
- Route Mobile: rich vendor error-code extension beyond standard SMPP: `ESME_CREDIT_ERROR` 0x401, `ESME_SPAM_MESSAGE` 0x404, `ESME_RINVSMLEN` 0x405 (>160 char text message), `ESME_RINVUDHLEN` 0x406, `ESME_RINSMSEMPTY` 0x407, `ESME_RINDSTDND` 0x408 (destination in DND), 0x4090x412 (invalid source/template, long-message-template error, duplicate submission, destination/source barred) — routemobile.com PDF. Test: assert each vendor-extension code round-trips through generic error parsing without falling back to "unknown error."
- Bandwidth: error ranges by origin — Bandwidth client 301-476, Bandwidth server 100-231, carrier client 700-795, carrier server 600-650, carrier ambiguous 902/999; 306 = registered_delivery out of range; 304-305 = invalid UDH; 720 = destination not in numbering plan — https://www.bandwidth.com/support/en/articles/12823160. Test: assert a registered_delivery value outside {0,1} on submission is flagged in the same class as Bandwidth's 306.
- Syniverse: 1032 invalid registered_delivery, 1033 system error, 1034 invalid service_type, 1035 invalid command length, 1036 wrong state for command, 1037 invalid message id, 1038 outgoing queue full, 1039 throttling, 1041 submit failed, 1042/1043 invalid source/dest TON, 1048 invalid data_coding — https://sdcsupport.syniverse.com/hc/en-us/articles/360038257473. Test: checklist that the library independently validates/reports each of command_length, session state, message id, service_type, registered_delivery, TON, data_coding.
## 11. Framing / transport
- spec §2.4: the underlying network connection is assumed to provide "reliable data transfer... including packet encoding, windowing, flow control and error handling," and segmentation/reassembly of PDUs across packets happens **below** the SMPP layer — spec via smpp.org PDF fetch. Test: feed a test harness PDUs deliberately split across multiple TCP writes and multiple PDUs coalesced into one read, assert the parser reassembles by `command_length` rather than assuming one PDU per read.
- spec §3.1 C-octet String: "a series of ASCII characters terminated with the NULL character... empty strings encode as a single NULL octet (0x00)" — spec via smpp.org PDF fetch. Test: a field that's non-null-terminated or contains embedded NULs should be rejected/flagged by a strict parser.
- spec PDU header: four 4-octet big-endian fields (command_length, command_id, command_status, sequence_number); command_id 0x000000000x000001FF requests / 0x800000000x800001FF responses; sequence_number 0x000000010x7FFFFFFF — spec via smpp.org PDF fetch. Test: fuzz a header with command_length smaller than actual PDU bytes present, confirm the generic_nack "invalid command_length" path (topic 10) fires.
- `sm_length=0` + `message_payload` combination is the standard way to carry >254-octet user data — spec via sysop.fr mirror (also topic 5/6). Test: assert a parser accepts short_message len 0 alongside a populated message_payload TLV.
- Telia: requires SNI on all SMPP TLS connections, lists 14 supported TLS cipher suites, min TLS1.2, port 3550 — https://cdn.messaging.teliacompany.com/documents/developer/index.html. Test: attempt a TLS handshake without SNI, expect rejection.
- LINK Mobility: TLS 1.2/1.3 required, ports 3600 (plain)/3601 (TLS), TLS1.0/1.1 discontinued since 2020-11-15 — https://www.linkmobility.com/resources/developer/SMSC-SMPP-User-Guide-1.5.pdf. Test: attempt TLS1.1, expect rejection.
- Kaleyra: `message_payload` explicitly capped at 64K octets, gated behind interface_version=0x34 — https://messaging.kaleyra.com/support/solutions/articles/3000091796-submitting-messages-through-smpp. Test: send message_payload right at and just over 64K, confirm boundary behaviour.
- Bird: plaintext SMPP on TCP 2775, TLS on TCP 2776 — https://docs.bird.com/connectivity-platform/other-integrations/how-can-i-use-the-short-message-peer-to-peer-smpp-network-protocol. Test: configure a mock SMSC on 2775/2776 to match a Bird-shaped client config.
- No vendor among the ~20 researched publicly documents multi-PDU-per-TCP-segment behaviour, `command_length`-lies-about-actual-size handling, or enquire_link burst tolerance by name — this whole topic is essentially spec-only + implementation-defined across all vendors; treat the spec §2.4/§3.1 items above as the ground truth and the rest as **unverified — folklore**.
---
## Top 10 quirks most likely to break a fresh implementation (ranked)
1. **Message ID base/field mismatch between submit_sm_resp and DLR `id:`.** Kannel `msg-id-type`, Jasmin `dlr_msgid`, and Vonage's default hex-vs-decimal split all independently produce a submit_sm_resp id that doesn't string-match the DLR's `id:` field unless converted. A correlation layer that assumes "same string" will silently lose DLR-to-message mapping. (Kannel User's Guide; Jasmin docs; Vonage API Support.)
2. **`esm_class` DLR bit confusion: 0x04 (final receipt) vs 0x20 (intermediate) vs 0x00 (plain MO).** A naive "esm_class != 0 means DLR" check misclassifies intermediate notifications and even non-DLR SME acks (0x08/0x10) as final delivery receipts. (SMPP 3.4 spec.)
3. **Long-message mechanism varies by vendor and none is universal.** UDH-only (LINK Mobility, Vonage, Route Mobile via esm_class 0x43), `sar_*` TLVs (Clickatell, spec-native), and `message_payload` (Telia, Kaleyra — gated behind interface_version 0x34 — Route Mobile) are all real, mutually exclusive requirements. A library hardcoded to one mechanism won't interop with vendors requiring another. (Multiple vendor docs above.)
4. **UCS-2 byte order.** Kaleyra and Route Mobile explicitly require UTF-16 **big-endian**; a JS/TS implementation defaulting to native (often little-endian) UTF-16 encoding silently corrupts every non-GSM message. (Kaleyra, Route Mobile docs.)
5. **Throttling semantics: `ESME_RTHROTTLED` (0x58) vs `ESME_RMSGQFUL` (0x14) need different backoff, and window/TPS limits vary 10x across vendors** (2/s free SMPPSim vs 100+/s Infobip) with some vendors closing the socket instead of responding. A fixed retry policy will misbehave against most real SMSCs. (smpp.org error codes; LINK Mobility, Sinch, Vonage, Telia docs.)
6. **DLR text-body field variability is much wider than the "standard" format suggests.** `sub`/`dlvrd` are hardcoded "000" and `text` always blank at LINK Mobility; `stat` vocabularies extend beyond the core 7 values (Vonage adds FAILED/DELETED, Infobip allows ENROUTE as a stat, Telesign adds SKIPPED). A strict fixed-format parser breaks on the first non-conforming vendor. (LINK Mobility, Vonage, Infobip, Telesign, smpp.org.)
7. **Concatenated SMS must stay on one session/server, and MO concat params can be absent.** Vonage: all parts must go through the same SMPP server or the handset never reassembles, within a 20-minute submission window; MO concat TLVs are missing entirely for some US carriers, requiring a same-sender+timestamp heuristic. tyntec: without UDH, segment order isn't guaranteed at all. (Vonage, tyntec docs.)
8. **PDU framing over TCP is not 1:1 with `read()`/`write()` calls.** The spec explicitly pushes segmentation/reassembly below the SMPP layer, but no vendor publishes concrete "multiple PDUs per segment" or "PDU split across segments" test guidance — a stream parser that assumes one PDU per socket read will break under real-world TCP behavior (Nagle, MTU, coalescing) the first time it hits production traffic. (SMPP 3.4 spec §2.4/§3.1.)
9. **Interface-version-gated features fail silently, not loudly.** Binding at 0x33 instead of 0x34 silently disables `message_payload`/certain TLVs (Kaleyra, Route Mobile) or changes message_id format (Sinch legacy) rather than rejecting the bind — a library that doesn't track negotiated version per-session will send PDUs the far end simply ignores or misinterprets. (Kaleyra, Route Mobile, Sinch docs.)
10. **TON/NPI conventions for alphanumeric sender disagree across vendors.** NPI=0 (Infobip, Sinch, Telia) vs NPI=1 (Vonage's own documented example) for the same TON=5 alphanumeric-sender case — a library validating "correct" TON/NPI combinations against one vendor's convention will reject valid submissions to another. (Infobip, Sinch, Telia, Vonage docs.)
---
### Coverage notes
- Kannel's `dlr-mask` (named in the brief) was searched for directly in the Kannel 1.4.5 User's Guide `smsc smpp` section and **does not appear there** — confirmed absent via targeted fetch, not merely unfound. It may exist under a different name/group in newer Kannel, or the brief may have conflated it with another gateway's option. Marked **unverified — folklore** rather than guessed.
- Twilio confirmed to offer SMPP, but enterprise-gated only (no public/self-service target) — see topic 1.
- Tele2 and Hi3G/Tre (3, Sweden) have no public SMPP interconnect technical spec discoverable in English or Swedish — explicitly no findings, not omitted by oversight.
- Esendex's public docs describe only capability-level facts (multi-bind, separate in/out binds); no protocol-level parameters (TON/NPI, data_coding, DLR format) could be recovered — its SMPP developer sub-pages are JS-rendered and returned empty content to fetching.
- Syniverse's raw SMPP bind-level technical manual is not publicly reachable; everything sourced instead comes from its SCG platform docs, which wrap SMPP in a REST layer but still publish SMPP-level status-code tables.
- Telenor's two public PDFs are business/operational overviews confirming supported-vs-unsupported PDU types but not publishing message_id format, DLR text format, or TON/NPI tables.