Least-cost routing (LCR) policy
For every outbound SMS there are two selection problems: which sender the message goes out from, and which upstream route carries it to the destination operator. Sender resolution owns the first. The LCR policy owns the second — your per-organization preference for ordering Devotel’s wholesale trunk against your own connected (BYO) carriers. This page covers what the policy controls, how routes are scored, and the dry-run quote you can run before saving a change. Routing behaviour only — the request and response schemas live in the API reference.Where LCR sits in the routing chain
LCR evaluates recipients on three inputs — the recipient’s identifier class, the destination it implies, and the per-route price — and runs after sender resolution:- Sender resolution picks the sending identity (a sender pool, an explicit
from, a messaging-service default, or the fallback chain). - LCR upstream selection — the stage this page covers — ranks the wholesale trunk and your BYO carriers for that destination.
- Delivery proceeds over the top-ranked route; route health and circuit breakers then meter every route continuously (see Outbound route quality and circuit breakers).
How routes are scored
Each BYO carrier you have connected for a destination is scored on four axes, multiplied together:
The highest composite score is the route the live gateway prefers. The composite is the four axes above multiplied together — when the dashboard and the quote show one percentage, it is the product of
costScore × deliveryQuality × bindHealth × stickyBonus, not a weighted average. Your policy decides how that scorer-first order relates to the Devotel wholesale entry:
trafficType: "cost"— rank BYO carriers strictly by the cost axis.trafficType: "quality"(default) — rank BYO carriers by the composite score.preferByo: false(default) — the Devotel wholesale trunk leads; BYO carriers follow in scorer order.preferByo: true— every active BYO carrier ranks before the Devotel entry, without you enumerating them.candidateOrder— an explicit list of candidate ids (the reserved iddevotel, and BYO carrier ids) that completely overrides the editorial order. Unknown or stale ids are dropped at evaluation time, and unnamed candidates fold back into scorer order — a misconfigured list degrades, it never errors.
The dry-run quote preview
You don’t have to commit a policy to see what it would do. The quote endpoint ranks routes for a destination and returns the full breakdown:- Request: a destination in E.164 (
to), an optional messagequantity(drives per-segment totals), and an optionalpolicybody. With nopolicy, your saved policy is used; pass one to preview a candidate policy before saving it. - Response:
winnerId/winnerKind, the policy actually in force, anenabledflag, and acandidatesarray in rank order. Each candidate carriesrank(1 = winner), a plain-languagereason(for example “Devotel first (preferByo: false)” or “BYO before Devotel (preferByo)”),estimatedUnitCents/estimatedTotalCentsfor the Devotel entry, andcompositeScorefor BYO entries.
null cents because your upstream spend with that carrier isn’t computable — a fabricated 0 would mislead the preview. GET /messaging/lcr/policy returns the saved policy (or the defaults on absence), and PUT /messaging/lcr/policy upserts yours idempotently.
Common configuration patterns
- Cheapest-first — set
trafficType: "cost"withpreferByo: true. Your lowest-priorityBYO carriers lead, and the Devotel trunk becomes the runner-up rather than the fallback default. - Filtered to a region — scope carriers per-country on the carrier profile (matching by destination MCC list). The quote only considers carriers whose country scope matches the destination, and the Devotel entry is priced for the resolved destination country.
- Sticky incumbents — keep the default
qualitymode: the route a message last succeeded on keeps a small advantage until its health degrades, so near-equal scores don’t cause route flapping.
Read it in the dashboard
You don’t have to talk to the API to see the scorer’s verdict. Under Developer → SMPP, two read-only panels expose the ranking:Route scores
A ranking table over your connected carriers, bound to the same scorer the send gate uses. Rows arrive ranked — most-preferred first — and each row breaks the composite into three readable columns:- Delivery — the smoothed delivered ratio (
deliveryQuality), with the raw delivered/terminal counts in parentheses. - Bind — the SMPP session state (
bindHealth), so a demoted carrier reads as “the bind is down”, not “the score is a mystery”. - Cost — the cost score from the carrier profile’s
priorityfield (costScore).
stickyBonus in play). A Gauge icon heads the panel. The panel polls the route-scores endpoint (useSmppCarrierRouteScores in apps/web/src/app/[locale]/(dashboard)/developer/smpp/_components/route-scores-panel.tsx), and percentages throughout are rendered with formatRouteScorePct. It is reporting only — no routing rule changes.
Dry-run route quote
Paste a destination E.164 and optionally a message count, then Run dry run to preview the winner:- A BYO route verdict names the winning carrier, lists every candidate with per-candidate quality/bind/cost percentages (
formatRouteScorePct) and destination MCCs, and confirms you pay your upstream directly. The Picked flag marks the winner row. - A Devotel route verdict — no BYO carrier matched the destination — quotes the Devotel egress fallback: per-message and total estimate via
formatCentsPrecisefrom the ledger pricing ladder.
POST /messaging/smpp/carriers/route-quote (useQuoteSmppRoute in apps/web/src/app/[locale]/(dashboard)/developer/smpp/_components/route-quote-panel.tsx). A dry run bills nothing and moves no traffic.
compositeScore, mapped back to the table
Both panels render one percentage per axis; the composite the policy engine ranks on is the multiplication of the four signals in the scoring table above —costScore, deliveryQuality, bindHealth, and stickyBonus (when the constraint holds). A carrier winning on cost alone while its bind degrades still sorts below a healthy incumbent, and the demotion reads in the panel.
Auditability
Every policy write is recorded: eachPUT /messaging/lcr/policy call writes an audit entry naming the enabled flag, traffic type, BYO preference, and candidate order, so a route-ordering change is always attributable to a user and a timestamp. Reads also surface the effective policy even when no policy has been saved (the wire defaults).
See also
- Sender resolution — the step LCR runs after: which sender a message goes out from
- Unified termination routing — the stage between sender resolution and LCR: whether the send stays on SMS or terminates onto another channel
- SMPP edge model — how the managed relay fronts your bind and BYO carrier routing
- Connect via SMPP — bind your SMPP client; the Developer → SMPP surface hosts the route-scores and dry-run quote panels this page walks through
- Outbound route quality and circuit breakers — continuous per-route health metering and auto-suspension
- How routing picks a sender — and where a reply lands — the two-sided routing reference (outbound chain summary + inbound rules)