Skip to main content

Inbound cost metering

Outbound sends and inbound traffic run through the same pricing engine but live on different planes: a send resolves a price and debits your wallet in the same breath, while received traffic is priced and reported without ever touching the wallet. This page is the model for that second plane — what GET /api/v1/usage/inbound-cost reports, why it can never move money, and what to reconcile it against.

1. A reporting plane, not a metering plane

Spend tracking in Orbit grew up outbound-first. Every send resolves a charge through the shared rate-resolution step and writes it through the wallet ledger, so the usage and billing dashboards always knew what you spent sending. Received traffic never got the same treatment: SMS and MMS that arrive on your numbers, and voice minutes that terminate to them, are billed by carriers per message and per minute — but nothing priced that volume, so any “total spend” view you built silently excluded it. The inbound-cost report closes that gap by reading what already happened instead of metering as it happens:
  • Outbound meters. The send path writes one usage record and debits the wallet synchronously — the loop described on the usage metering pipeline.
  • Inbound reports. The report aggregates the volume your tenant already persisted — received messages from the messages table and received calls from call_logs — and prices that volume on demand for the window you ask for. Nothing is written and nothing is charged; the figure you get is a read of recorded volume at current rates.
Because the report is computed at request time from persisted rows, it covers received traffic regardless of when the rating features shipped: last month’s received messages price today, the moment you ask.

2. The response contract

One request covers one window and answers with one total plus one line per channel that carried received traffic:
  • period labels the window (a YYYY-MM calendar month by default).
  • range.from / range.to are the exact half-open interval the lines aggregate, so two reports can always be compared by interval.
  • currency is the ISO-4217 code of your wallet currency — the same currency every other billing surface reports in, so this figure can never disagree with your balance about which currency it speaks.
  • lines[] carries one entry per channel with received volume: the channel (sms, mms, voice), the billing unit (next section), the quantity in that unit, the per-unit unit_price_cents, the line total cost_cents, and the pricing_source attribution (section 4). Lines are ordered most-expensive first.
The money invariant: the lines always foot to the total. Every monetary figure at the contract is integer cents, total_cost_cents is the exact sum of the per-line cost_cents values, and there is no sub-cent drift between the breakdown and the total. Per-unit prices may carry sub-cent precision (0.8 above); each line rounds to whole cents once, and the total adds those rounded lines. The window itself is chosen with a calendar month, a rolling window, or an explicit from/to pair — the same options as the rest of usage reporting; the API reference carries the full parameter contract.

3. The two billing units received traffic uses

Inbound traffic bills on exactly two units:
  • per_message — received SMS and MMS. Each received message counts as one, exactly as the carrier bills termination. There is no segment arithmetic here: SMS segments and encoding can multiply the cost of a long outbound message because the platform pays per segment, but a received message is a single billable event regardless of its length or encoding.
  • per_minute — inbound voice. Each received call rounds up to the next whole minute (a 61-second call bills as two minutes), mirroring how carriers bill termination; quantity is the sum of those rounded minutes over the window.
Outbound, the same pricing ladder works in per-segment quantities for SMS and per-minute quantities for voice — the ladder is shared, only the measured quantity differs. One deliberate scope note: free or bring-your-own inbound channels (WhatsApp, email, and similar) carry no per-event carrier termination cost, so they do not appear in the report; including them would over-state your spend.

4. The same resolution chokepoint, with direction “mo”

Pricing a received message uses the same rate-resolution step as pricing a send — resolvePrice(), the chokepoint documented on pricing and rate resolution — with the direction set to "mo" (mobile-originated / inbound) and the country wildcarded to *, which resolves the global inbound rate for the channel. The report also shares the batched ladder read (resolveBaseRatesBatch()): every channel’s rate shape resolves in one pass instead of one lookup per line, and org markup overrides still apply per channel exactly as they do for sends. Because both passages share the ladder, each line’s pricing_source names the rung that produced its unit price, using the same attribution labels as outbound rating:
  • org_markup — the base inbound rate marked up by your organization’s own override.
  • default_markup — the base inbound rate with the platform-default markup.
  • fallback — no inbound rate row matched the channel yet, so the per-channel fallback price was used; the report therefore works before per-country inbound rate sheets exist.
  • absolute_override — an absolute per-unit price set for your organization, verbatim.
The symmetry stops at rating. An inbound price never walks the outbound billing gate chain and never consults the pause flags that can hold a send: there is nothing to gate, because an inbound price never becomes a charge.

5. Reporting-only invariants

Three guarantees define the plane:
  1. It never mutates the wallet. No debit row, no credit deduction, no authorize/capture hold — a report request cannot change your balance.
  2. It never calls a provider. The report is a pure read of persisted volume plus the rate ladder.
  3. It reconciles to deliverables, not to ledger rows. You will not find a wallet transaction matching an inbound line, because none exists — this is the explicit contrast with the wallet ledger audit trail, where every outbound debit is a row you can point at. To reconcile an inbound line, re-derive it: filter your received messages and calls to the same range, count messages and carrier-rounded minutes, and multiply by the line’s unit_price_cents.
Re-pulling a past window is a re-read of append-only rows at current rates — a recompute, never a re-charge — the same replay property the outbound rollups rely on.

6. Where this page sits in the billing family

  • The usage metering pipeline — the outbound loop. Its aggregation stage already counts inbound rows (its counter rollups name “sms-inbound, sms-outbound” alongside voice), but stops at counts; this page is the cost side of those inbound counts.
  • Pricing and rate resolution — the precedence ladder both directions resolve through, and the home of the attribution labels.
  • Wallets, credits, and charges — the append-only ledger the inbound plane deliberately never writes.

7. Where to see it live

  • Dashboard. The Billing → Usage page shows received-traffic cost next to your outbound spend.
  • API. GET /api/v1/usage/inbound-cost returns the report for your tenant.
  • Guide. Inbound traffic cost metering walks worked endpoint calls and reads a breakdown end to end.