Skip to main content

Pricing and rate resolution

How billing meters your usage covers the pipeline up to the rated amount — the prepaid wallet, FX on converted top-ups, graduated tiers, reseller margin, and tax. Where that page stops is where this one starts: per-send price attribution — for an SMS priced per destination operator (MCC/MNC), which pricing rule supplied the number you are charged. Read this page when you need to explain a per-operator your_rate on the rate card, or to reconcile the rate card against the ledger.

The rate-resolution precedence

For a given destination operator, Orbit resolves the customer price by consulting rules in a fixed order. The first rule that matches wins:
  1. A per-operator rate override — a negotiated price for your organization on that specific operator. It applies verbatim: no markup is layered on top. This is the strongest rule and beats every markup-based rule, even when a markup override also exists.
  2. Your organization’s own markup override — when no per-operator override exists but the operator has a published cost, the price is the operator cost multiplied by your organization’s markup.
  3. The platform-default markup — when neither override exists, the price is the operator cost multiplied by the platform default (20%).
  4. No published rate — when the operator has no published cost row at all, there is no resolved price. The rate card shows a Contact sales row and nothing is attributable.
The send path and the display path both run this same precedence. The send path (packages/billing/src/rate-resolver.ts and mccmnc-rate-resolver.ts) resolves the price you are charged; the rate card handler for GET /pricing/mccmnc-rates (apps/api/src/routes/pricing/mccmnc-rate-card-source.ts) classifies each row into absolute_override, org_markup, default_markup, or null using the same inputs. Because both paths agree, the label on the card names the rule the wallet is actually billed under.

How rating reaches the send pipeline

Rate resolution is not a side calculation the rate card runs for display — every channel’s send path funnels its charge through the same per-send resolution step, so a price you can read on the rate card is the same price the wallet is debited.

One resolution step for every channel

SMS, voice, video, AI tokens, and the usage-metering rollup all resolve their customer price through one shared resolver before any wallet movement happens. Each send passes its channel, destination, and quantity to that step and receives back the full price breakdown: the per-unit price, the quantity multiplier (segments, minutes, tokens), the total charge in your wallet currency, and the attribution labels naming the rule that produced the price. For an SMS send that carries a resolved destination operator (the MCC/MNC pair the network lookup returned), the per-operator precedence above runs first: override verbatim, then operator cost × markup. Only when no operator-level rule matches does the resolution fall through to the generic rate-card ladder — an organization-specific rate row, then the global per-country rate, then the published fallback for that channel.

The envelope threads one declaration through the router

Above the resolver, the channel-agnostic message envelope carries your send as a single declaration: one recipient, one content bag (a universal text body plus optional per-channel variants), and an ordered channel_chain fallback preference. The router walks that chain top-down, attempts the first channel that has both a provider and a renderable content variant, and falls through on transient failure. Pricing rides along unchanged: whichever channel wins the walk is rated through the same resolution step described above, and its declared channel is what determines which rate-card ladder and which per-operator rules apply. Setting costAware: true on the envelope re-ranks your declared chain cheapest-first using the platform rate card (or your own per-send cost overrides) before the walk starts — the preference set is yours, the rate card only re-sequences it.

The resolved price is what lands on the ledger

The charge the resolution computed is the amount debited from your wallet, and the ledger row carries the attribution with it — which rate row, which override, the carrier cost, the markup applied, and the FX leg when your wallet currency differs from the pricing currency. That is what makes the reconciliation loop closed: a ledger debit can be traced back to the exact pricing rule that produced it, and the message record exposes that attribution so a message charged on the generic fallback is never rendered as if a per-operator rate had resolved. How balance, ledger, and debits behave once the price is resolved is the subject of Wallets, credits, and charges.

Where usage metering (reseller rating) fits

The Usage Metering API on How billing meters your usage is a reseller-facing rating surface: you POST your own subaccounts’ usage events with a meter definition (unit, included quantity, graduated tiers) and receive a rated rollup you can mark up and invoice to your end customers. It is deliberately stateless — it previews the debit descriptors rather than moving money. It does not re-run the per-operator resolution on this page: it rates the quantities you supply against the meter you define. Orbit’s own real-time debits — the ones that appear in your wallet ledger — all resolve through the single per-send step above, and only those debits ever touch your wallet.

What the per-operator rate card shows

GET /api/v1/pricing/mccmnc-rates returns one row per operator: the operator code, operator name, country, your your_rate, its currency, and a pricing_source label:
The pricing_source label is exactly one of the precedence outcomes:
  • absolute_override — a per-operator override was applied verbatim.
  • org_markup — operator cost × your organization’s markup.
  • default_markup — operator cost × the platform default.
  • null — no published rate; the row is Contact sales, not a price.
An operator with no published cost row still appears on the card (it is not silently dropped) with your_rate: null and pricing_source: null. Treat that row as “no published rate” — it is never a signal that sending is free. In the dashboard, this same card renders under Billing → SMS rates.

How overrides persist

Three tables hold the state that the resolver reads:
  • mccmncRateOverrides — a per-(organization, operator) rate override, applied verbatim (absolute_override).
  • orgPricingOverrides — the organization-wide markup override (org_markup).
  • pricingChangeLog — the append-only audit table. Every write to an override or a rate row records the before and after state, so every change of a pricing rule is journal-traceable. The tenant-facing GET /pricing/changelog route exposes the public projection of that log (which rule changed, its scope, and when — never internal cost or margin fields), and pricing-history.ts derives the same history for rate rows that were seeded without a recorded entry, so the audit view reflects the full rate card’s provenance rather than only later API mutations.
Manage the per-operator overrides yourself through POST /api/v1/billing/mccmnc-overrides and the sibling list/update endpoints on that page. Organization-wide markup overrides are set by the platform on your organization.

Display matches charge — the one invariant

The rate card is not decorative. For every row it shows, the displayed pricing_source names the same pricing rule the send path charges you under. The recorded error case this invariant guards against is a card attributed to one rule (for example a 0.01 display rate) while the wallet was charged under another (a 0.075 actual charge); that misattribution is what the display-vs-charge check exists to catch. Two consequences for reading the card:
  • A null source row is not a price. It means no published rate exists for that operator. Never treat it as a zero cost, and never quote it to your own customers as a rate.
  • A labeled row is authoritative. When pricing_source is non-null, the same rule produced the charge in the ledger, so you can attribute a debit to the override, your markup, or the default with confidence.