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

# BYO SMPP carrier lifecycle

> The full lifecycle of a tenant-attached upstream SMS carrier — attach, probe and health, how carrier health feeds LCR scoring and circuit breakers, and what detach means for traffic and your saved candidate order.

# 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](/concepts/smpp-edge-model) describes the relay and the [least-cost routing policy](/concepts/least-cost-routing) 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`

```bash theme={null}
# 1. Stage the carrier (inactive so no traffic yet)
curl -X POST https://orbit.devotel.io/api/v1/messaging/smpp/carriers \
  -H "Authorization: Bearer $DEVOTEL_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "label": "My upstream SMSC",
    "type": "smpp",
    "remoteHost": "smsc.example-carrier.net",
    "remotePort": 2775,
    "remoteSystemId": "acct_001",
    "remotePassword": "••••••••",
    "bindType": "tx",
    "scope": "all",
    "priority": 10
  }'

# 2. Enable it (next reconcile pass provisions the connector)
curl -X PATCH https://orbit.devotel.io/api/v1/messaging/smpp/carriers/smppcr_abc123 \
  -H "Authorization: Bearer $DEVOTEL_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "status": "active" }'

# 3. Synchronous probe of the row after one pass
curl -X POST https://orbit.devotel.io/api/v1/messaging/smpp/carriers/smppcr_abc123/test \
  -H "Authorization: Bearer $DEVOTEL_API_KEY"
# → { ok: true, bindStatus: "BOUND", ... }

# 4. Dry-run: which route would a send to this destination take
#    under preferByo: true (candidates ranked, no side effects)
curl -X POST https://orbit.devotel.io/api/v1/messaging/lcr/quote \
  -H "Authorization: Bearer $DEVOTEL_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "to": "+12125550123",
    "policy": { "preferByo": true }
  }'
# The BYO carrier outranks the Devotel wholesale row when its
# composite score wins; its estimatedUnitCents stays null (BYO spend
# isn't computable), while the Devotel row carries the ledger price.
```

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

* [Least-cost routing (LCR) policy](/concepts/least-cost-routing) — how the winner is ranked and how `candidateOrder` and `preferByo` steer it
* [SMPP edge model](/concepts/smpp-edge-model) — the relay, the per-credential Jasmin user model, and the reconciler contract
* [Outbound route quality and circuit breakers](/concepts/outbound-route-quality-and-circuit-breakers) — operational metering and the trip gate a degrading route feeds
* [Sender resolution](/concepts/sender-resolution) — the step LCR runs after
