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

# Route preview — the pre-send what-if estimator

> The model behind POST /messages/route-preview: a read-only, no-side-effects estimator that answers 'what would a smart send do with these inputs' — which channel, why, what falls back, and what it costs — before a single message exists. How it differs from least-cost routing and the Deliverability Lab.

# Route preview — the pre-send what-if estimator

Before a message goes out, Orbit owns one question you can ask for free: *if I dispatched this right now, what would the smart router do?* `POST /messages/route-preview` answers it — the recommended channel, a plain-language reason, the ordered fallback chain, a per-message cost estimate, and a predicted engagement score — and it answers it **without creating a message row, touching the wallet, or emitting a webhook.** This page covers what the estimator evaluates, what it returns, and the boundary it holds against its neighboring pre-send tools.

## Where it sits — before the send, beside the other estimator lenses

Orbit's pre-send checks split along the axes each one owns:

* **[Least-cost routing](/concepts/least-cost-routing)** ranks *upstream carriers* for SMS — which trunk a message exits over. That is a route-internal ordering.
* **[Deliverability Lab](/concepts/deliverability-lab-pre-send-scoring)** scores *content risk* — will carrier filters treat this copy as spam-shaped.
* **Route preview (this page)** answers the *whole-send* question at the channel layer — which channel the smart router picks, what the fallback chain looks like, and what the per-message cost reads.

The lab never routes a send; LCR never picks a channel; the route preview never scores your copy's filter risk. Each lens stays in its own lane, and the preview is deliberately the cheapest of the three — pass a recipient and a message shape, get the channel answer back.

## What it evaluates

The preview runs the same contact-aware channel scorer the smart-send path dispatches from, so its answer is what a `POST /messages/smart-send` (or `channel: "auto"` unified send) with the same inputs would pick:

1. **Channel eligibility.** Phone-capable channels (SMS, MMS, WhatsApp, RCS) enter candidates by phone reachability; email enters when an address is present; the social/OTT identifiers (`instagram_psid`, `messenger_psid`, `telegram_id`, `line_id`, `viber_id`, `device_token`) resolve from the contact record you already hold. A channel with no reachable identifier scores zero and drops out.
2. **Historical delivery and engagement.** The scorer reads *your own* past sends and replies — a terminal-denominator delivery rate per channel, and the response-share per channel — so a contact who answers on email prefers email.
3. **Message-type and goal weighting.** OTP biases SMS (and WhatsApp, and against email). Marketing leans toward rich channels (WhatsApp, RCS, email). `goal` shifts the balance — `cheapest` boosts low-cost channels, `fastest` boosts SMS and push, `highest_engagement` weights channel richness. `cost_optimize: true` further biases cheaper routes.
4. **Cost per candidate.** Per-message cost comes from the tenant channel cost table, so the estimate is a per-cohort planning figure, not a guess.
5. **Fallback chain.** The ordered fallback channels behind the winner are the same stack a live dispatch would advance through on a hard failure. When a `cascade_policy` is attached, the preview also resolves the same cascade arm the send would stamp.

Two special cases the estimator names explicitly in its `reason`:

* **OTP** returns a regulatory note rather than an arbitrary pick — the scorer acknowledges SMS as the OTP primary and only falls forward to WhatsApp/voice.
* **All-flags-off recipient** — no detected capabilities and no email — returns `"No channel capabilities detected — defaulting to SMS"`, naming the router's last-resort default rather than inventing a channel.

## What it returns

One JSON envelope, five answers, plus the boundaries the estimator never crosses:

* **`channel`** — the router's pick (one of `sms`, `whatsapp`, `email`, `viber`, `rcs`, `push`, `voice`).
* **`reason`** — plain text composed from whichever factor fired (`cost-optimized`, `high engagement (N%)`, `fast delivery for urgent message`, `rich content for marketing`, the OTP regulatory note, or the all-flags-off SMS default). Not a black-box score — the text tells you *which* of the inputs decided.
* **`fallback`** and **`fallbackChain`** — `fallback` is the next channel to try; `fallbackChain` is the full ordered list after the winner, in priority order. The first entry of `fallbackChain` always equals `fallback`. An empty array means no reach-able next channel for this recipient — treat the winner as terminal.
* **`estimatedCostCents`** — per-message cost in cents on the winner, before any volume tiering. Sub-cent channels come back fractional, so multiply rather than round when you sum a cohort.
* **`engagementScore`** — predicted engagement 0.0–1.0: your `engagement_rates` value for the winner when you supplied it, otherwise the platform per-channel default.
* **Blocked destinations named.** Per-channel ineligible channels are not silently skipped — the winner's `reason` and the chain reflect their absence, and a `422` names the malformed field with a human-readable message. (This is distinct from country-level destination gating, which sits in the [country allowlist](/concepts/country-allowlist-gate-model) — the preview reads channel eligibility, the allowlist gates destinations.)

The model-level boundary: the preview never resolves a sender ([sender resolution](/concepts/sender-resolution) is the dedicated stage), never routes carriers ([LCR](/concepts/least-cost-routing)), and never writes a message row — those are the other stages' lanes. Per-message cascade arms resolve only when `cascade_policy` is attached; otherwise the response stops at the chain.

## No-side-effects contract

A preview is a pure read. Every call runs with these invariants, which the endpoint shares with the LCR dry-run quote:

* **No message row.** Nothing persists — no `messages` insert, no contacts upsert, no conversation entry.
* **No wallet touch.** Costs are estimated, never deducted. The estimator is free to call in a planning loop.
* **No provider or webhook emission.** No provider call fires, no DLR arrives, no webhook goes out to your endpoint, and the preview never shows up in delivery stats.
* **No tenant data read-across.** When you omit `engagement_rates`, the defaults are global per-channel constants; nothing reads another tenant's history.

That posture is what lets the dashboard-composer wizard poll the endpoint on every keystroke, and what makes per-recipient planning passes safe to batch.

## When to wire it

Four call shapes fit the estimator:

* **Pre-launch campaign checks.** Run it over a recipient sample before you schedule, so the assumption "this blast goes on SMS" gets verified against the router's answer for each recipient.
* **CI integration gates.** A pipeline that plans a rollout can assert the estimated cost stays under a ceiling, or that the fallback chain degrades the way you expect, before any traffic moves.
* **Dashboard composer previews.** The [Outbound → Route Preview](/guides/smart-route-preview) page polls this endpoint — a non-API operator can answer "does enabling cost-optimize swap SMS for WhatsApp on a Brazil recipient?" inline. ([Settings → API keys](/guides/smart-route-preview) grants `messages:read` — the same scope the endpoint accepts.)
* **Send-pipeline fallback ladders.** Pre-fetch capabilities per recipient, run the preview, dispatch on the winner, and advance through `fallbackChain` on hard failures — the [smart-route-preview guide](/guides/smart-route-preview) walks that loop end to end.

When you need legality (policy scanner) or content-risk (Deliverability Lab), the preview runs alongside, not instead.

## Worked samples

Two concrete calls, both returning only the estimator's five answers.

### WhatsApp → SMS fallback chain on an all-capable recipient

```bash theme={null}
curl -X POST "https://api.orbit.devotel.io/api/v1/messages/route-preview" \
  -H "X-API-Key: $ORBIT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "to": "+14155552671",
    "body": "Your order #8421 shipped — track it here.",
    "message_type": "transactional",
    "urgency": "medium",
    "whatsapp_available": true,
    "rcs_available": true,
    "email": "sara@example.com",
    "cost_optimize": true,
    "engagement_rates": { "whatsapp": 0.91, "sms": 0.38 }
  }'
```

```json theme={null}
{
  "channel": "whatsapp",
  "reason": "Best channel: high engagement (91%)",
  "fallback": "rcs",
  "fallbackChain": ["rcs", "sms", "email"],
  "estimatedCostCents": 3,
  "engagementScore": 0.91
}
```

The router picked WhatsApp on engagement (`0.91 ≥ 0.70` threshold), kept RCS and SMS in reach, and priced the winner at 3¢. Wire `fallbackChain` into your pipeline as the fallback ladder — dispatch the primary, advance on hard failure.

### Blocked field named, not a silent skip

The estimator surfaces *which input* governs the answer. Here, an absent capability flag defaults to "unknown", so the recipient is not considered reachable on that channel:

```json theme={null}
{
  "to": "+14155552671",
  "body": "…",
  "message_type": "transactional",
  "urgency": "low",
  "whatsapp_available": false
}
```

WhatsApp drops out of the candidate set because the flag declared it unreachable; the winner is the next eligible channel that scores highest, and `reason` names the governing factor (for example `"No channel capabilities detected — defaulting to SMS"` when nothing else qualified). A `422 VALIDATION_ERROR` on a malformed body likewise names the field with a human-readable message — the estimator never returns a generic failure when a specific input is the blocker.

## See also

* [Least-cost routing](/concepts/least-cost-routing) — the sibling estimator that ranks upstream carriers after sender resolution.
* [Sender resolution](/concepts/sender-resolution) — which sender a message goes out from; the preview is deliberately one lane downstream of that stage.
* [The five fallback planes](/concepts/fallback-and-cascade-planes) — the decision map over the planes the preview's `fallbackChain` intersects.
* [Deliverability Lab](/concepts/deliverability-lab-pre-send-scoring) — the content-risk lens the preview explicitly does not own.
* [Cross-channel fallback](/concepts/cross-channel-fallback) — the org-level chain the preview's `fallbackChain` composes with at dispatch time.
* [Country allowlist](/concepts/country-allowlist-gate-model) — the tenant-owned destination gate the preview *does not* replace; the preview reads channel eligibility, the allowlist gates destinations.
* [Smart-route-preview guide](/guides/smart-route-preview) — the endpoint's request/response reference, the dashboard-composer walk-through, and the send-pipeline loop.
* [Channel optimization model](/concepts/channel-optimization-model) — the scorer behind the preview's channel pick.
* [Fallback on terminal DLR](/concepts/multi-channel-dlr-fallback) — the per-request fallback plane that fires after acceptance, downstream of where the preview stops.
