> ## 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.

# Configure your LCR policy (Devotel-first vs BYO-first)

> Walk the full least-cost-routing workflow end to end: attach your own BYO carriers, decide cost vs quality ranking, preview the ranked route order with the dry-run quote, and commit the policy with a single idempotent write.

# Configure your LCR policy (Devotel-first vs BYO-first)

The least-cost-routing (LCR) policy is your per-organization preference for ordering outbound SMS routes: Devotel's wholesale trunk first, or your own connected (BYO) carriers first. This guide walks the whole workflow — attach a BYO carrier, read the current policy, preview the ranked route order without saving anything, and commit the change. The model behind the scoring lives on the [LCR concept page](/concepts/least-cost-routing); this page is the task.

## When this applies

Configure an LCR policy when all three of these are true:

* You send SMS in volume, so route order moves real money or real delivery rates.
* You have your own upstream carrier relationship(s) — an SMPP account you can hand Orbit as a BYO carrier — or you're about to attach one.
* You want to steer the route order (cheapest BYO first, BYO-before-Devotel, or an explicit order) instead of the out-of-the-box default.

If you only ever send over the Devotel wholesale trunk, the default policy already does the right thing; skip the rest of this page.

## Decision table

Four fields make a policy. Every field is optional on a write; omitted fields fall back to the defaults shown.

| Field            | Values                  | What it steers                                                                                                                                                                                                                                                                      | Default     |
| ---------------- | ----------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------- |
| `trafficType`    | `"cost"` or `"quality"` | How your BYO carriers rank against each other: `"cost"` orders strictly by the carrier's cost figure (its `priority` on the carrier profile), `"quality"` orders by the composite score (cost × delivery quality × bind health × sticky bonus).                                     | `"quality"` |
| `preferByo`      | `true` or `false`       | `false` puts the Devotel wholesale trunk first; `true` ranks every active BYO carrier before the Devotel entry without enumerating them.                                                                                                                                            | `false`     |
| `candidateOrder` | array of candidate ids  | An explicit route order (the reserved id `devotel`, plus BYO carrier ids). When present it overrides both fields above. Unknown or stale ids are dropped at evaluation time; candidates you didn't name fold back in scorer order. A misconfigured list degrades — it never errors. | unset       |
| `enabled`        | boolean                 | Master switch. When `false` the quote preview reports `enabled: false` and the live gate keeps its scorer-first order.                                                                                                                                                              | `true`      |

The safe starting point — and the platform default — is `trafficType: "quality"` with `preferByo: false`. Devotel's trunk leads, and your BYO carriers follow in quality order; a degraded BYO bind never outranks the healthy trunk on price alone. Flip to `"cost"` only when per-message margin on your own carrier contract genuinely outweighs delivery quality for your destinations, and verify that with the preview before saving.

## Read the current policy

```bash theme={null}
curl "https://api.orbit.devotel.io/api/v1/messaging/lcr/policy" \
  -H "X-API-Key: $ORBIT_API_KEY"
```

The response wraps the policy in the standard `data` envelope:

```json theme={null}
{
  "data": {
    "enabled": true,
    "trafficType": "quality",
    "preferByo": false
  }
}
```

You never have to guess what a never-configured organization returns: when no policy has been saved, the read rebuilds the wire defaults above and returns those — the read never errors and never returns `404`.

## Write the policy

`PUT` upserts the policy idempotently — re-sending the same body is a no-op — and writes an audit entry (`lcr_policy.updated`) with the effective values, so a route-order change is always attributable to a user and a timestamp.

```bash theme={null}
curl -X PUT "https://api.orbit.devotel.io/api/v1/messaging/lcr/policy" \
  -H "X-API-Key: $ORBIT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "preferByo": true, "trafficType": "cost" }'
```

To pin an explicit route order, send `candidateOrder` with the reserved `devotel` id plus your BYO carrier ids (the `id` values returned when you register the carriers — see [Attach a BYO carrier](#attach-a-byo-carrier)):

```bash theme={null}
curl -X PUT "https://api.orbit.devotel.io/api/v1/messaging/lcr/policy" \
  -H "X-API-Key: $ORBIT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "candidateOrder": ["cr_eu_hub", "devotel", "cr_legacy_trunk"] }'
```

The service validates the whole body: a bad enum (`trafficType: "cheapest"`), a non-boolean `preferByo`, or a 422-inducing shape fails with a 422 and changes nothing.

## Preview before you commit

You don't have to save a policy to see what it would rank. The dry-run quote takes a destination, an optional message quantity, and optionally a policy body; with no `policy` it evaluates your **saved** policy, and with one it evaluates the request-scoped body so nothing commits.

```bash theme={null}
curl -X POST "https://api.orbit.devotel.io/api/v1/messaging/lcr/quote" \
  -H "X-API-Key: $ORBIT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "to": "+447700900123", "quantity": 1, "policy": { "preferByo": true } }'
```

The response ranks every candidate and names the winner:

```json theme={null}
{
  "data": {
    "winnerId": "cr_eu_hub",
    "winnerKind": "byo",
    "enabled": true,
    "policy": { "enabled": true, "trafficType": "quality", "preferByo": true },
    "candidates": [
      {
        "id": "cr_eu_hub",
        "kind": "byo",
        "label": "EU hub carrier",
        "estimatedUnitCents": null,
        "estimatedTotalCents": null,
        "priceSource": null,
        "compositeScore": 0.62,
        "rank": 1,
        "reason": "BYO before Devotel (preferByo)"
      },
      {
        "id": "devotel",
        "kind": "devotel",
        "label": "Devotel wholesale",
        "estimatedUnitCents": 0.74,
        "estimatedTotalCents": 0.74,
        "priceSource": "rate_card",
        "compositeScore": null,
        "rank": 2,
        "reason": "Devotel runner-up (preferByo)"
      }
    ]
  }
}
```

Read the numbers honestly:

* The **Devotel entry** carries a real `estimatedUnitCents` and `estimatedTotalCents` off the same pricing ladder the ledger settles; `priceSource` names where the price came from (`rate_card`, an override, or the default lane).
* A **BYO entry** always shows `null` cents, because what you pay your own upstream for that destination isn't computable here — a fabricated `0` would mislead the comparison. The `compositeScore` is the BYO entry's competitive figure instead: cost × delivery quality × bind health × sticky bonus, multiplied (see [How routes are scored](/concepts/least-cost-routing#how-routes-are-scored)).
* `winnerId` / `winnerKind` marks the route the live gate would pick under the evaluated policy, and every candidate carries its integer `rank` and a plain-language `reason` you can show in a console.

A dry run bills nothing and moves no traffic.

## Attach a BYO carrier

Your BYO carrier is the upstream account Orbit binds to over SMPP. Register it once, keep it healthy, and the LCR candidates update on their own.

1. **Register the carrier** with `POST /api/v1/messaging/smpp/carriers` — the bind coordinates (`remoteHost`, `remotePort`, `remoteSystemId`, `remotePassword`, `bindType`), a memorable `label`, a `scope` (`"all"`, or `"by_country_mcc"` with `scopeFilter.mccs` to limit it to specific destination operator lists), and a `priority`. Worked requests, in cURL and SDK form, live in the [SMPP API reference](/api-reference/endpoints/smpp); the bind lifecycle is covered in [Connect via SMPP](/guides/smpp).
2. **Give it a cost figure**. `priority` is a monotonically decreasing cost score: `0` maps to a full 1.0 cost score, the default `100` to 0.5, and `300` to 0.25 — set it intentionally; a cost-mode policy ranks on exactly this number.
3. **Keep the bind healthy**. A failed SMPP bind nearly removes the carrier from contention; an unbound one is demoted over a bound peer; and a bind that hasn't been heard from for a while takes a smaller, partial hit. The dashboard renders the breakdown under **Developer → SMPP → Route scores**, and `GET /api/v1/messaging/smpp/carriers/route-scores` reads the same composite over the API.

If your quote skips a carrier entirely, check the scope: a `"by_country_mcc"` carrier only enters the candidate set when the destination resolves to one of the MCCs in `scopeFilter.mccs`.

## Cut over

Change a live policy the way you change any route-shaping config — stage, check, then flip:

1. **Stage explicitly.** Set the candidate list you intend (`candidateOrder`), not a naked `preferByo` flip where the BYO family rank surprises you. Preview it with `POST /messaging/lcr/quote` and confirm the `winnerId` on a handful of your real destinations.
2. **Save.** `PUT /messaging/lcr/policy` with the staged body.
3. **Watch failover.** Rank order isn't the whole story — a lead route that degrades hands traffic to the next ranked route. Track that with [Outbound route quality and circuit breakers](/concepts/outbound-route-quality-and-circuit-breakers) and the runbook at [Configure route-quality thresholds](/guides/route-quality-circuit-breaker-runbook). If the cutover hurts delivery, flip `preferByo` back to `false` (or restore your saved `candidateOrder`) — a one-call rollback.

## See also

* [Least-cost routing (LCR) policy](/concepts/least-cost-routing) — the scoring model this page configures
* [How routing picks a sender — and where a reply lands](/concepts/sender-and-routing) — where LCR sits in the outbound chain
* [Messaging credentials API](/api-reference/messaging-credentials) — the API-reference surface for the credentials these carriers run on
* [Connect via SMPP](/guides/smpp) — the BYO carrier bind lifecycle
* [SMPP API reference](/api-reference/endpoints/smpp) — carrier register/list/delete with worked requests
* [Configure route-quality thresholds](/guides/route-quality-circuit-breaker-runbook) — monitor the failover path after you cut over
