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

# SMPP edge model

> How Orbit's SMPP edge works underneath your bind: the Jasmin relay it fronts, the per-credential Jasmin user model, and the 30-second reconciliation loop that keeps the relay consistent with your configuration — including DLR delivery modes and bring-your-own carrier routing.

# SMPP edge model

Orbit's SMPP edge is a managed relay, not an open SMPP gateway you configure
directly. The relay fronts [Jasmin](https://jasminsms.com), an open-source SMS
gateway, and everything you can configure on it — bind credentials, throughput
caps, DLR delivery mode, upstream carriers — lives in your account
configuration. A background reconciliation loop continuously applies that
configuration to the relay, so the state your SMPP client observes is always a
projection of what you set.

The [SMPP bind guide](/guides/smpp) covers the client side: how to connect and
send. This page covers the platform model underneath — who owns each piece of
state, and why a status change you make takes up to \~30 seconds to take effect
on the wire.

## Why a relay sits in front of SMS

SMPP 3.4 is the interchange protocol aggregators and high-throughput senders
already speak. Exposing it directly from a bespoke service would mean
re-implementing a full SMPP server — session state, PDUs, delivery-receipt
queues, multi-tenant isolation — before a single message moved. Jasmin already
is a mature SMPP server, so Orbit runs Jasmin as the protocol endpoint and
layers its multi-tenant model on top: each customer's bind credentials become
Jasmin users, and the boundary between tenants is enforcement Orbit applies on
top of the credentials it issues.

The bind endpoint at `smpp.orbit.devotel.io` speaks SMPP on exactly two ports
(`2775` plain TCP, `3550` TLS). Both terminate on the same Jasmin instance and
take the same credentials. (The [bind guide](/guides/smpp) covers connectivity,
including the connect-timeout behavior you need when the port is wrong.)

## The credential-to-Jasmin-user model

Every SMPP credential you create — via `POST /api/v1/messaging/smpp/credentials`
or the dashboard — becomes one row in `smpp_credentials` in your tenant schema
(see [Tenant isolation](/concepts/tenant-isolation), Section 2, which lists
`smpp_credentials` as a per-tenant table). The row carries the bind identity
(`systemId`), the throughput cap (`tpsLimit`), an optional IP allow-list
(`allowedCidrs`), the DLR delivery mode (`dlrMode` / `dlrWebhookUrl`), and a
lifecycle `status`.

Each such row maps to exactly one Jasmin *user* on the relay — the same id, so
there is no mapping table to drift. The API never touches the relay directly;
it writes the row, and the reconciliation loop (below) turns the row into the
Jasmin user. Your `system_id` from the API response *is* the Jasmin username
your client binds with.

Because credentials live in your tenant schema and nowhere else, one tenant's
bind credentials are invisible to another tenant by construction — the same
isolation rule the rest of your account data follows.

## The reconciler contract

The relay's configuration is owned by a reconciliation service that runs a
pass every \~30 seconds. Each pass is one shot, with no long-lived in-memory
state:

1. **Read the desired state** — every tenant's `smpp_credentials` rows (and
   any registered BYO-carrier rows).
2. **Diff against the relay** — the pass lists what Jasmin currently holds and
   computes which users or connectors need to be added, updated, disabled, or
   removed.
3. **Apply the diff** — provision new users for new credentials, update
   password/TPS/status on existing ones, and disable or remove users whose
   rows were suspended or revoked.

Three properties of that contract matter operationally:

* **`status` is reconciler-routed, not immediate.** Flipping a credential to
  `suspended` in the dashboard or via `PATCH` writes the row; the next pass
  disables the Jasmin user. A status change takes effect on the wire within
  one reconciliation tick — budget a minute, not a millisecond, when you
  schedule a suspend/rotate sequence.
* **The pass is idempotent.** The same diff applied twice is a no-op, and a
  pass interrupted mid-way is simply retried by the next one. Running two
  copies of the reconciler at the same time converges rather than conflicts.
* **The loop only manages what your configuration created.** Binds and
  connectors the platform provisions for its own purposes — the inbound
  carrier trunks that receive your delivered-DLR and MO traffic, for example —
  are outside the reconciler's diff, so no credential change you make can
  ever delete them.

The same contract governs the BYO-carrier side: registering an upstream
carrier row (`POST /api/v1/messaging/smpp/carriers`) reconciles to a Jasmin
`smppc` connector plus a routing (`mtrouter`) rule on the next pass, and
soft-deleting it (`DELETE`) removes both. The [DLR and MO gateway
pipeline](/concepts/dlr-and-mo-pipeline) page covers what the loop stamps back
onto your rows — bind counts and connector bind status — which is what the
dashboard renders.

## DLR delivery modes

Each credential decides where its delivery receipts leave Orbit. The
`dlrMode` field is the whole contract:

| `dlrMode`        | What happens to an outgoing receipt                                                                                                                               |
| ---------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `bind` (default) | The receipt is delivered back over your SMPP session as a `deliver_sm` PDU. Nothing is POSTed anywhere.                                                           |
| `webhook`        | The receipt is POSTed as JSON to `dlrWebhookUrl` (HTTPS only, and the URL is validated to block requests to internal addresses). Nothing is returned on the bind. |
| `both`           | Both of the above, for the same receipt.                                                                                                                          |

Two operational consequences:

* **`bind` mode can only deliver to a live bind.** Receipts queued while no
  session is open wait for the next bind; if your integration needs receipts
  even when your SMPP client is down, use `webhook` or `both`.
* **Switching modes is a row update, reconciled like any other.** A change to
  `dlrMode` or `dlrWebhookUrl` propagates on the next reconciliation pass, and
  receipts generated before the change still obey the mode in effect at
  generation time.

Per-credential `tpsLimit` and `allowedCidrs` ride the same path: the API
writes the row; the reconciler applies the cap and the source-IP allow-list to
the Jasmin user on the next pass.

## BYO carrier model

Enterprise tenants with their own carrier agreements can register upstream
SMSC connections (`type: smpp` or `type: http`) that Orbit routes outbound
traffic through alongside its own carrier network. Routing per carrier row is
declarative:

* **`scope`** — `all` (the carrier can serve any outbound message) or
  `by_country_mcc` (only the MCC list in `scopeFilter.mccs`, up to 50).
* **`priority`** — an integer where lower wins; two active carriers serving
  the same scope resolve by priority, not by registration order.
* **`status`** — `active` reconciles the connector and route in; `inactive`
  (the soft-deleted state) reconciles them out.

Because both the `smppc` connector and its routing rule are reconciler-managed,
a `POST /carriers` followed one tick later by an outbound send is the normal
flow — the send picks the connector up the moment the pass has run. Likewise,
`DELETE /carriers/{id}` stops new sends from routing to the carrier after the
next pass completes. In-flight submissions made before the pass are finished on
the old route.

## The status timeline a bind observes

From a connected SMPP client's point of view, credential status changes show
up as protocol behavior, always after one reconciliation tick at most:

| Credential status | What the relay does on the next pass                         | What your client observes                                  |
| ----------------- | ------------------------------------------------------------ | ---------------------------------------------------------- |
| `active`          | Jasmin user exists and is enabled.                           | `bind_transceiver` succeeds; live binds keep working.      |
| `suspended`       | Jasmin user is disabled.                                     | New binds are rejected; existing sessions are dropped.     |
| `revoked`         | Jasmin user is removed (the row stays for your audit trail). | Existing binds drop; new binds fail with `ESME_RINVSYSID`. |

Revocation is therefore the strongest of the three: not only is bind access
gone, the identity itself is removed from the relay, and `POST
/credentials/{id}/rotate` on a revoked credential has nothing left to
rotate against. Create a fresh credential instead.

## Who owns which failure

When a `bind` or `submit_sm` fails, the actor that owns it follows the model:

| Symptom                                                            | Which actor owns it                                                                       | First thing to check                                                                   |
| ------------------------------------------------------------------ | ----------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------- |
| `bind_transceiver` rejected (`ESME_RINVPASSWD` / `ESME_RINVSYSID`) | The credential — wrong password, unknown system\_id, or `suspended` / `revoked` status.   | Dashboard → Developer → SMPP: status and whether you rotated the password recently.    |
| TCP connect / TLS failure before any bind response                 | Your network or the port choice — the edge is fine.                                       | Port `2775`/`3550`, egress firewall, TLS verification against `smpp.orbit.devotel.io`. |
| Bind succeeds but nothing passes the source-IP check               | The credential's `allowedCidrs` allow-list.                                               | Your egress source IP against the CIDRs configured on the credential.                  |
| `submit_sm` accepted on the bind but receipt never arrives         | DLR delivery mode — receipts may be queued on the bind you closed, or a webhook endpoint. | `dlrMode` on the credential; for `webhook`, reachability of `dlrWebhookUrl`.           |
| `submit_sm` rejected for rate limiting                             | The credential's `tpsLimit`, applied by the reconciled user.                              | The configured `tpsLimit` vs. your sending rate.                                       |
| Send accepted but routed to an unexpected upstream carrier         | BYO carrier scope/priority.                                                               | The active carriers' `scope` / `scopeFilter.mccs` / `priority` ordering.               |

Anything not in the table — a credential that is `active` in the dashboard
yet still rejected minutes after a status change, or a carrier whose
`bindStatus` never flips — is signal to raise with support, since the
reconciler itself converges within a minute under normal operation.

## Related

* [Connect via SMPP](/guides/smpp) — the client-side guide: ports, creating a
  credential, DLR fields, and choosing SMPP vs REST.
* [SMPP API reference](/api-reference/endpoints/smpp) — the full
  `/messaging/smpp` endpoint surface (credentials + carriers).
* [Tenant isolation](/concepts/tenant-isolation) — where `smpp_credentials`
  lives and why that isolates your binds at the schema level.
* [DLR and MO gateway pipeline](/concepts/dlr-and-mo-pipeline) — how receipts
  and inbound messages fan out once they cross the gateway, and what the
  reconciler stamps back into your rows.
