Skip to main content

The usage metering pipeline

Every billable thing in Devotel Orbit — a message, a completed call, an AI request — flows through the same pipeline: resolve the sender, resolve the destination cost, write one usage record, debit the wallet once, and roll the row into the aggregates your invoices and dashboards read. Read this page when you need the whole loop in a single model — the detail pages (sender and routing, pricing and rate resolution, the usage-records feed, the channel cost rollup, and the wallet ledger) each zoom in on one stage of what is laid out end to end here.

1. The chain: sender resolution → destination cost → per-message quote

A send becomes a billable event at the moment the platform can state who you are sending as and what the destination costs.
  1. Sender resolution. The send path picks the sender identity it will bill on — which owned number, sender-ID, or channel identity carries the message. The resolution order is fixed, documented on sender resolution and sender and routing.
  2. Destination cost resolution. For SMS the per-operator lookup (MCC/MNC) runs first — override verbatim, operator cost × your markup, then the platform-default markup — before falling through to the generic per-country rate ladder. Voice resolves a per-minute rate the same way. The full precedence is on pricing and rate resolution.
  3. Per-message cost quote. One shared resolution step returns the full price breakdown: the per-unit price, the quantity multiplier (SMS segments, call minutes, AI tokens), the total, and the attribution label naming the rule that produced the price. This quote is stamped on the usage record before the wallet moves. The wallet pre-flight then checks the balance against the quote, and only then does the send proceed.
Because every channel funnels through that one resolution step, the price you can read on a rate card is the price the ledger charges — the display-matches-charge invariant guards exactly this equality.

2. The usage-record write: one row per billed thing, append-only

The rated send writes exactly one usage record — the row-level ledger fact you reconcile against. One record per message, one per completed call (see the usage-records feed for the full field set):
  • The record carries the billed units (SMS segments per message, one unit otherwise) and the price the resolution quote computed, in the wallet currency.
  • A record that failed to rate stays on the feed with an empty price rather than disappearing, so the feed always counts the same rows the aggregate counters count.
  • The feed is append-only in effect: a reversal or correction is a new row, never an in-place edit — the same property the wallet ledger relies on.
The same single-debit property holds on the money side. Each usage record produces exactly one wallet debit whose ledger reference names the underlying record id, and every balance mutation carries an idempotency key derived from the per-send identifier, so a retried charge collapses to one ledger row instead of double-billing (the key mechanism is on wallets, credits, and charges). Out-of-wallet metering — AI agent cost attribution and reseller-facing usage metering — reports its own stores; the wallet-drawdown chain on this page describes the channels the prepaid wallet actually charges.

3. Aggregation lifetime: raw records → rollups → invoice lines

The usage record is the only source of truth. Every higher-order number is an aggregate over it:
  • Row feeds. GET /api/v1/usage-records and its CSV export are the raw window — one page or file of records, ungrouped.
  • Counter rollups. The aggregate counter surface (GET /api/v1/messages/usage/records) rolls the same rows up per channel and direction — sms-inbound, sms-outbound, voice, and so on — over a configurable window, optionally split per country.
  • Channel cost rollups. GET /api/v1/analytics/costs groups priced rows by channel and currency over a day/week/month granularity — the Insights → Costs dashboard reads this projection.
  • Invoice lines. An invoice is assembled by summing the filtered rows for the billing period — the invoice is built from the ledger, never from a standing counter.
Crucially, none of these rollups is a mutable accumulator. Each one is a function of the raw usage records in the window, so it can always be re-derived: drop the window’s filters onto GET /api/v1/usage-records, sum units × price, and you rebuild any rollup the period contains. The rollup vs ledger reconciliation model documents why the projections can legitimately disagree (filters, not missing data) and how to join them back to the raw rows.

4. Metering vs billing: recording is unconditional, charging depends on the plan

Metering and billing are decoupled by design:
  • Metering is the recording of the event. The usage record is written whenever the billable thing happens — before any plan evaluation and independently of what your subscription includes.
  • Billing is the decision to charge. Whether the record debits the wallet depends on what the rating pipeline resolved: included quantity, graduated tiers, free-tier, or a zero-price rule.
The consequence matters for plan changes: usage already exists before a plan upgrade. When you move from a free tier to a usage plan, the records for everything you sent do not appear retroactively — they were there all along, with the price column empty or unrated per the old plan’s rules. After the upgrade, only new events resolve a price. This is why the feed never “catches up” after an upgrade: the records were never missing, only unpriced. Use this distinction in reconciliation as well: the usage-records feed counts every billed event, including ones the plan priced at zero, while a cost rollup that filters to price > 0 reports only the charged subset.

5. Backfill and replay: re-aggregation must be idempotent

Two replay guarantees define the pipeline:
  • Ledger replay collapses. A retried charge carrying the same idempotency key resolves to the same ledger row — the debit lands exactly once, and the replayed response is flagged so side effects (webhooks, notifications) do not refire.
  • Re-aggregation re-derives. Any rollup is additive over the raw records, so backfilling or recomputing a window is a re-read of those records, not a re-charge. Rebuilding the channel rollup for last month cannot move money twice; it recomputes sums from the same append-only rows.
The practical consequence for your own pipelines: treat the usage-records export as a re-runnable pull. Re-pull a window, dedupe on the record id, and re-aggregate — idempotency holds at the row level, the same way the wallet holds it at the debit level.

6. Currency and units: cents, smallest unit, no float

All money arithmetic is integer, always:
  • The wallet stores whole cents; the charge ledger tracks sub-cent precision in integer micro-cents (1 USD = 100,000,000 micro-cents), so a million sub-cent debits sum exactly.
  • Every usage record and rollup row carries an ISO-4217 currency, and multi-currency sums stay split per currency. Never merge currencies; the dashboard merges only for display and the API remains the source of truth.
  • Field-scale representations like balance_usd are display-only floats. Do any comparison or projection in balance_cents or balance_micro_cents — a float projection will disagree with the ledger on small amounts.
The units ladder and the three-representation balance response are spelled out on wallets, credits, and charges.

7. Where to go next