Skip to main content

BYO SMPP carrier lifecycle

Outbound SMS in Orbit splits two ways: the Devotel wholesale trunk, which is the built-in termination path every tenant already has, and your own upstream carriers: SMPP gateways (or HTTP carrier APIs) that you operate outside Orbit and attach to your account. The SMPP edge model describes the relay and the least-cost routing policy ranks the candidates. This page covers the carrier itself from attach to detach: how it connects, how its health is measured, and how that health changes where your traffic goes. Everything a BYO carrier does stays tenant-owned: the row lives in your tenant schema, the credentials you give it never leave your account, and only your traffic ever crosses the connector.

Attach

Attaching a carrier is a row write plus one reconciliation pass:
  1. POST /api/v1/messaging/smpp/carriers creates the carrier with the bind profile your upstream gave you: remoteHost / remotePort / remoteSystemId / remotePassword, plus how you want it treated: scope (all or by_country_mcc with an MCC list), priority (your own cost rank; lower wins), and status. The password is encrypted at rest and never returned in full again; reads only expose a remotePasswordSet boolean.
  2. Validation at the boundary — the create hooks reject a host that points at localhost, a private range, or a cloud metadata endpoint, before anything tries to reach it on your behalf.
  3. status starts as inactive. Stage the credentials, compare a dry-run quote, and flip to active when you want the route to go live. Setting active is the moment the next reconciliation pass provisions the SMPP connector and its routing rule on the platform edge; there is no separate “enable” call.
  4. Failed or pending binds stay off the selection race (see health below), so a carrier you just attached never hijacks traffic on its first tick.
One reconciliation pass applies at most 30 seconds to turn the row into wire state, the same cadence your SMPP bind credentials use.

Health: probe, reconciler, and what a failing bind means

The platform measures a BYO carrier two ways, continuously and on demand:
  • Continuously (the reconciler). Every ~30-second pass reads your active carriers, asks the edge what the connector’s session looks like, and stamps the result back onto the row as bindStatus and lastSeenAt. The state your dashboard and reads show (BOUND, UNBOUND, BIND_FAILED:<reason>) is a projection of the last pass, never hand-set by the API. A connector that silently dropped its session shows up as a failed bind on the next pass and is nudged restarted so the bind cycle re-establishes.
  • On demand (POST /api/v1/messaging/smpp/carriers/:id/test). A synchronous check that the row is well-formed and its stored credential decrypts. It reads the reconciler-stamped bindStatus back rather than blocking on a live dial, and it is rate-limited so a stuck upstream can’t be hammered.
A live submit_sm handed to a BIND_FAILED / UNBOUND connector drops silently with no app-side error; the bind-state stamp is the only signal your outbound SMS has gone dark short of the slow delivery-rate reconciliation. Correspondingly, route scoring (next section) removes a failed-bind carrier from contention almost immediately, so a broken bind demotes the route instead of letting it keep accepting traffic it can’t deliver.

Where the carrier feeds routing

Carrier health isn’t just a badge on a read; three separate consumers use it:
  • LCR scoring ([concepts/least-cost-routing]) multiplies four axes: costScore from your priority, deliveryQuality (smoothed delivered/terminal over a window), bindHealth (the value above: failed bind ≈ 0.05, unbound ≈ 0.4, stale bound hit slightly, unknown treated neutral), and a small stickyBonus for the incumbent. A failed bind nearly removes the carrier from contention; a missing health signal never punishes it.
  • Route-quality and circuit breakers ([concepts/outbound-route-quality-and-circuit-breakers]) meter the route operationally over your own messages (delivery conversion, DLR latency, acknowledged-but-no-receipt) and, once you opt in, trip a breaker that suspends a sustained-failing route and records the failover channel. A flapping bind both loses its sticky bonus (the LCR failover moment) and, once the degradation becomes a sustained signal, feeds the trip gate.
  • Dry-run preview. POST /api/v1/messaging/smpp/carriers/route-quote (and the policy-aware POST /api/v1/messaging/lcr/quote) runs the same engine the live gate does, so a preview cannot disagree with a live send.
Because outbound MT exits only over the managed edge, BYO attachment is a policy preference, not a new wire: attach/detach never changes what sends are allowed, only which connector they prefer.

Detach

DELETE /api/v1/messaging/smpp/carriers/:id soft-deletes: the row stays for your audit trail but status goes to inactive (the same shape a PATCH /api/v1/messaging/smpp/carriers/:id with { "status": "inactive" } achieves non-destructively). The next reconciliation pass removes the SMPP connector and its routing rule from the edge, and the carrier disappears from candidate sets. Two properties matter at the edge:
  • In candidateOrder it does not error. A stale id in your saved policy’s explicit candidateOrder is dropped at evaluation time; the remaining candidates fold back into scorer order, and the route picks the next-best entrant. A misconfigured (or now-missing) candidate order degrades, it never rejects a send.
  • In-flight traffic already handed to the carrier finishes on the old route. New sends stop selecting it only after the pass completes, so a detach made during a burst can let a few in-flight messages drain.

Credentials and capacity

Attach is per-tenant isolation like the rest of your account: the row lives in your tenant schema, and no other tenant sees or uses your connector. Two cost/capacity points to plan around:
  • Cost is not computable. The quote preview prices the Devotel wholesale row off the pricing ladder; a BYO candidate returns null for cents because your upstream spend lives outside Orbit. A fabricated 0 would mislead the preview. Your priority is the preference rank you give the scorer, not an Orbit price.
  • Throughput and scope are yours. Two active carriers matching a destination resolve by priority, and a carrier scoped by_country_mcc never drags cross-region traffic it can’t serve. Set priority before you enable, and scope to the MCC list if the upstream only covers a region.

Example: attach → probe → dry-run quote with preferByo: true

The same probes work live on GET /api/v1/messaging/smpp/carriers/route-scores — a read-only snapshot of the exact axis scores the gateway used for your active carriers.

See also