Skip to main content

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

6. Worked request — POST the preview in a loop over your channel/volume mix

Run POST /api/v1/billing/whatif-pricing/preview end to end once, then shift the channel and volume emphasis per iteration. The loop below is the shape a negotiating session usually takes: one candidate card, several read frames.
  1. POST the candidate card. Send the channel/volume mix you want to test — one row per lane pattern, e.g. outbound WhatsApp to Nigeria at a +5% markup the way a negotiated volume sheet would quote it:
  1. Read the 200 envelope. A successful preview answers with the standard Orbit envelope: data carries window_days, the echoed label, the per-lane replay list, and rolled-up totals; meta carries request_id and timestamp.
Read the lane list for coverage before you read the totals. With no sub_type on the row above, it re-prices every outbound WhatsApp-to-Nigeria lane — marketing, utility, and untyped traffic alike — so those 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.
  1. Iterate with label as your run marker. Vary one dimension per POST — a different window, a second row for another destination, or a revised rate_per_unit — and keep a distinct label on each so the runs stay separable in your own logs. Read totals.delta_cents / totals.delta_percent for the whole-window impact on your mix, and check matched_lane_count against lane_count to confirm the card actually covered the lanes you intended; every lane outside the card shows 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.

7. Fallbacks — what the reply means when a lane cannot be priced normally

Two independent fallback paths keep a preview honest instead of failing the request, and both are visible in the lane fields you read:
  • Current price resolved on the parity floor. Each lane’s baseline is resolved through the same ladder the live resolver uses — your org’s rows, then the global rows, most-specific-wins. When no rate row matches a lane at all, or a transient pricing-read failure hits mid-preview, the lane is priced at a conservative per-channel fallback floor — the same floors the live resolver uses on a miss, so the simulator never reports a baseline of $0 for traffic that genuinely costs money. You see it as a plausible current_total_cents on a lane you may not have seeded rates for yet; send volume in a new country or on a new channel before the rate card exists and the floor stands in for the missing row.
  • Candidate card recruited no row for the lane. A lane the candidate card does not match keeps its current cost untouched and reports candidate_matched: false with delta_cents: 0 — the contrast to the floor above is that the baseline was priced normally, the candidate side simply declined to re-price it.
Read the two apart and you can always answer “is this number a projection of my real rates, a conservative stand-in, or traffic the card never touched?” — totals.matched_lane_count against totals.lane_count is the quick coverage check before you trust delta_percent.