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

# What-if pricing simulator: back-test a rate card against your own traffic

> Replay your recorded usage over the last 1–90 days through a candidate rate card and read the exact per-lane and total cost delta before you commit to a rate change.

# What-if pricing simulator

Orbit's what-if pricing simulator answers one operator question: **what would this rate card have cost on the traffic I already sent.** You pick a look-back window, model a candidate rate card, and the simulator replays your own recorded usage through it — per channel, destination country, direction, and message sub-type — and returns the exact cost delta versus what you paid under your live rates. The traffic is held fixed; only the rates change. Use it to back-test a negotiated sheet, a new plan, or a competitor's published rates before you commit.

Two ways in, same engine:

* **Dashboard** — **Billing → What-if pricing simulator** (`/billing/simulator`) is the interactive surface: pick a window, drag a global ±% adjustment, or type a per-channel rate, and read the projected spend.
* **API** — `POST /api/v1/billing/whatif-pricing/preview` is the programmatic path when you need per-country, per-direction, or per-sub-type granularity, or want to run comparisons from your own tooling.

Both require the `owner`, `admin`, or `billing` role — the same gate as the other billing surfaces, because the reply carries your real per-channel spend.

## 1. What the simulator does

A preview run has three steps, all read-only:

1. **Aggregate your usage.** The usage events your own organization recorded over the look-back window (1–90 days) are grouped into billing lanes: channel × country code × direction (`mt` outbound, `mo` inbound) × sub-type, with quantities summed per lane.
2. **Price the baseline.** Each lane is priced under your current live rates with the same pricing resolver that produces your invoice — the number on the bill and the number in the simulator come from the same math.
3. **Replay the candidate.** Every lane is re-priced through your candidate rate card, and the reply reports the per-lane and rolled-up delta between the two.

Because the replay uses the real resolver, the projection matches how you are actually billed — down to most-specific-country-wins matching and the round-up, never-under-bill convention on per-lane totals. Only lanes a per-unit rate card can meaningfully re-price are replayed (messaging sends and voice minutes); meters like recording or AI-agent tokens are skipped.

## 2. Where it lives

Open **Billing → What-if pricing simulator** in the dashboard. The page is role-gated to `owner`, `admin`, and `billing`, matching the gate on the sibling billing drill-downs.

The page has two input groups:

* **Traffic window** — `24h`, `7d`, `30d`, or `90d`. This maps to a `window_days` of 1, 7, 30, or 90 on the API.
* **Adjustments** — one global slider (`−50%` to `+50%`) applied across every channel, plus an editable candidate rate per channel row. A typed rate overrides that channel and ignores the global slider; channels you do not touch follow the slider.

Results render as three headline cards — current spend, projected spend, and the signed delta with its percentage — above a per-channel replay table: volume, current spend, observed blended rate, the editable candidate rate, projected spend, and the change. A positive (red) delta means the candidate card costs more on your mix; a negative (green) delta means it costs less.

## 3. Reading a what-if preview

Every reply — dashboard row or API lane — is a current-vs-candidate comparison:

| Field                                                  | Meaning                                                                                                                            |
| ------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------- |
| `quantity`                                             | Units in the lane for the window (messages, or minutes for voice).                                                                 |
| `current_unit_price_cents` / `current_total_cents`     | What you pay today, per unit and in total, under your live rates.                                                                  |
| `candidate_unit_price_cents` / `candidate_total_cents` | What the same traffic would cost under the candidate card.                                                                         |
| `candidate_matched`                                    | `true` when the card had a row pricing this lane; `false` when the lane kept its current cost untouched.                           |
| `delta_cents` / `delta_percent`                        | Candidate minus current, in cents and in percent. `delta_percent` is `null` when the current cost is `0` but the candidate is not. |

All money fields are in USD cents. Unit prices carry sub-cent precision; totals are whole cents, rounded up once per lane (the never-under-bill convention your invoice also uses).

A candidate row prices a lane through the same ladder your live rates use:

* `rate_per_unit` — wholesale base, USD per unit (per message, or per voice minute).
* `markup_bps` — proportional markup in basis points (`500` = 5%), layered on the base.
* `markup_fixed_cents` — flat markup added per unit, in cents.
* `absolute_price_cents` — a fixed per-unit price in cents. When set, it wins over the base-plus-markup math entirely.

Which row in a multi-row card applies follows most-specific-wins: an exact country match beats the `*` wildcard; within that, an exact sub-type match beats a sub-type-agnostic row. Direction must match too — an `mt` row never re-prices an `mo` lane.

### Dashboard vs API granularity

The dashboard models **one rate per channel**: a typed rate is sent as a wildcard (`*`) `mt` row for that channel, and the result is rolled up to one table row per channel. Two consequences to know:

* **Outbound-only.** A typed dashboard rate re-prices `mt` (outbound) lanes only; inbound lanes keep their current cost. To model `mo` pricing, use the API with `direction: "mo"`.
* **Truly global.** The wildcard row re-prices the channel across every destination and sub-type uniformly. To model per-country or per-sub-type pricing (e.g. WhatsApp `marketing` vs `utility` to Nigeria), use the API with explicit `country_code` / `sub_type` rows.

## 4. The API

`POST /api/v1/billing/whatif-pricing/preview` — requires the `owner`, `admin`, or `billing` role.

| Body field       | Type      | Notes                                                                                                                                                                                                                                                                       |
| ---------------- | --------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `window_days`    | integer   | Look-back window in days, `1`–`90`. Default `30`.                                                                                                                                                                                                                           |
| `candidate_card` | object\[] | At least one row, up to 5000. Each row carries `channel` (required) and `rate_per_unit` (required), plus the optional `country_code` (default `*`), `direction` (default `mt`), `sub_type`, `markup_bps`, `markup_fixed_cents`, and `absolute_price_cents` described above. |
| `label`          | string    | Optional human label echoed back in the response, max 120 characters.                                                                                                                                                                                                       |

The card is a **partial override**: lanes with no matching row keep their current cost and report `candidate_matched: false` with zero delta. You only model the lanes you intend to change.

```bash theme={null}
curl -X POST "https://api.orbit.devotel.io/api/v1/billing/whatif-pricing/preview" \
  -H "X-API-Key: dv_live_sk_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "window_days": 30,
    "label": "Plivo Growth 2026",
    "candidate_card": [
      { "channel": "sms", "country_code": "US", "direction": "mt", "rate_per_unit": 0.0065 },
      { "channel": "sms", "country_code": "*",  "direction": "mt", "rate_per_unit": 0.0080 }
    ]
  }'
```

```json theme={null}
{
  "data": {
    "window_days": 30,
    "label": "Plivo Growth 2026",
    "lanes": [
      {
        "channel": "sms",
        "country_code": "US",
        "direction": "mt",
        "sub_type": null,
        "quantity": 3420,
        "current_unit_price_cents": 0.8,
        "current_total_cents": 2736,
        "candidate_unit_price_cents": 0.65,
        "candidate_total_cents": 2223,
        "candidate_matched": true,
        "delta_cents": -513,
        "delta_percent": -18.75
      },
      {
        "channel": "voice",
        "country_code": "US",
        "direction": "mt",
        "sub_type": null,
        "quantity": 84,
        "current_unit_price_cents": 60,
        "current_total_cents": 5040,
        "candidate_unit_price_cents": 60,
        "candidate_total_cents": 5040,
        "candidate_matched": false,
        "delta_cents": 0,
        "delta_percent": 0
      }
    ],
    "totals": {
      "current_total_cents": 7776,
      "candidate_total_cents": 7263,
      "delta_cents": -513,
      "delta_percent": -6.6,
      "lane_count": 2,
      "matched_lane_count": 1,
      "total_quantity": 3504
    }
  },
  "meta": {
    "request_id": "req_whatif_001",
    "timestamp": "2026-07-02T12:00:00Z"
  }
}
```

Read the lane list for coverage before you read the totals: any lane with `candidate_matched: false` is traffic the candidate card does not price — those lanes stay at current cost in your totals, so a card that covers only part of your mix understates its own impact. `matched_lane_count` against `lane_count` in `totals` tells you the coverage at a glance.

## 5. Limits and the read-only guarantee

The simulator is a compute surface, safe to run as often as you like:

* **Nothing changes.** A preview writes no record, emits no billing event, and moves no money. Your live rates, your pricing overrides, and your invoice are untouched no matter what you model.
* **Window caps.** `window_days` is bounded at 1–90 days; the dashboard offers 1, 7, 30, and 90. Replies cover up to 200 distinct lanes (the largest by quantity first), and a single request accepts up to 5000 candidate rows.
* **A projection, not a forecast.** The replay holds your recorded traffic fixed and changes only the rates. Final invoices still vary with delivery outcomes and any negotiated overrides on your account.

The simulator differs from the per-card pricing cells you can edit under **Billing → Rate cards** (`/billing/rate-cards`): those cells ask "what would one unit cost at this price under this card?" and are instant lookups against a card's rows. The simulator asks "what would my last 90 days have cost under this card?" and re-prices your real usage lane by lane. For the rate-card library itself, see [Rate cards](/billing/rate-cards).

## 6. Worked scenario — price WhatsApp to Nigeria +5%

Model a +5% markup on outbound WhatsApp to Nigeria over the last 30 days, the way a negotiated volume sheet would quote it:

1. Send the candidate card as one row: `channel: "whatsapp"`, `country_code: "NG"`, `direction: "mt"`, your wholesale `rate_per_unit`, and `markup_bps: 500`.

```bash theme={null}
curl -X POST "https://api.orbit.devotel.io/api/v1/billing/whatif-pricing/preview" \
  -H "X-API-Key: dv_live_sk_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "window_days": 30,
    "label": "WhatsApp NG +5%",
    "candidate_card": [
      {
        "channel": "whatsapp",
        "country_code": "NG",
        "direction": "mt",
        "rate_per_unit": 0.004,
        "markup_bps": 500
      }
    ]
  }'
```

2. Read the per-lane results. With no `sub_type` on the row, it re-prices every outbound WhatsApp-to-Nigeria lane — `marketing`, `utility`, and untyped traffic alike — so the WhatsApp/NG lanes show `candidate_matched: true` with the +5% delta. To price sub-types differently, send one row per sub-type (e.g. `marketing` at 500 bps, `utility` at 200 bps); the exact sub-type match wins per lane.
3. Read `totals`. `delta_cents` and `delta_percent` are the whole-window impact of the change on your mix; `matched_lane_count` confirms the card actually covered the Nigeria lanes you intended, and every other channel's lanes show zero delta.

In the dashboard the same experiment is quicker but coarser: set the window to `30d` and type a candidate rate in the WhatsApp row. That models the \* wildcard across all destinations, so use the API when the question is country- or sub-type-specific, as above.

## Related

* [Billing API reference — What-if pricing preview](/api-reference/billing) — full request/response field list.
* [Rate cards](/billing/rate-cards) — the rate-card library and per-card pricing cells.
* [Billing overview](/billing/overview) — wallet, charges, and how lanes become line items.
