Files

238 lines
66 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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.