Channel cost rollup vs the billing ledger model
Devotel Orbit exposes two surfaces for the same metered usage:- Insights → Costs — the aggregate view.
GET /api/v1/analytics/costsreturns totals, a time series, and a per-channel breakdown over a date window, grouped bygroup_by(day, week, or month). - Billing → Usage records / Cost-center chargeback — the ledger view.
One row per priced message or completed call, and the
metadata.cost_centerrollup that attributes spend to a department.
1. What “resolves” means
A row resolves when the price stamped on it at send time flows into the wallet ledger as a single debit. The Insights → Costs page reads those priced rows and rolls them up per channel; the Billing → Cost-centers page reads the same priced rows and folds them bymetadata.cost_center. Nothing
moves between the two views — they are two projections of the same ledger.
The distinction matters when you reconcile: a row that failed to rate has an
empty price, so it stays out of the billed messages count (and does not
dilute avg_cost_per_message) but it still appears on the usage-records
feed. “Resolves” means: priced → ledger debit → one of the two projections
above.
2. Why the insights page and the billing/cost-centers page disagree
They read different projections by design:- Insights → Costs is a channel aggregate. It groups by
channel(sms, whatsapp, voice, …) and by currency, and it only counts rows whereprice > 0. Failed sends, free-tier sends, and unrated rows are excluded from the billed count. The totals tile and the billed-messages tile both come from that filter. - Billing → Usage records / Cost-centers is ledger-owned. It walks
the raw rows (inbound and outbound, priced and unpriced) and attributes
spend by
metadata.cost_center— or reports it asunallocatedwhen the tag is missing.
price > 0 and the ledger page filters
only to outbound usage, the two can legitimately disagree on a given day —
especially if you have inbound traffic, failed sends, or unrated rows. That
is not a bug; it is the filter working as documented.
3. The numerics — currency, rounding, and segment attribution
Currency. Every rollup row carries an ISO-4217currency. Multi-currency
tenants get one row per (channel, currency) pair in by_channel, and one
point per (period, currency) in time_series. The dashboard merges those
per-channel rows when it renders the donut — summing spend and messages and
recomputing the average — so the API is the source of truth when you need
exactly one currency.
Rounding. avg_cost_per_message is total_spend / billed_messages, with
both computed over rows where price > 0, rounded to four decimal places.
Treat it as a display hint, not a ledger value.
Segments. SMS bills per segment, not per message. The rollup reports
total_segments alongside total_messages for exactly this reason: a
multipart SMS counts as one “billed message” but several segments, and the
segment count is what the price was computed from. When you reconcile
against the usage-records feed, join on segments (units) for SMS, not on
message count.
4. When to use one surface vs the other
Use the rollup for planning and trend reading; use the ledger for invoicing
and audit. Never treat the rollup
total_spend as an invoice line — the
invoice is built from the ledger, not the aggregate.
5. Which response field maps to which ledger row
GET /api/v1/analytics/costs returns an aggregate object; the ledger is the
row feed behind it. The mapping is per filter window, not row-level:
When you reconcile against
GET /api/v1/usage-records, apply the same date
filter and group by channel + currency — the sum of the rollup’s
by_channel rows will match the sum of the ledger’s filtered rows, with the
segments caveat above.
Worked example: SMS to Russia
You send 12,000 outbound SMS to Russia in June. 1,800 of them are multipart (two segments each), so the raw segment count is 13,800, and 240 rows failed to rate and carry an emptyprice.
-
Insights → Costs over the same window:
by_channelshowschannel: "sms",total_messages: 11,760(only rows withprice > 0),total_segments: 13,560(the billed segments), andtotal_spend— say **0.0075 per segment. -
Billing → Usage records over the same window: the filtered ledger
lists all 12,000 rows (inbound filtered out), both priced and unpriced,
and the sum of
priceacross the priced rows matches the $102.48 above. -
Billing → Cost-centers (
chargeback-rollup): if the sends were taggedmetadata.cost_center: dept_russia, that department line absorbs the full $102.48; the 240 unrated rows land inunallocateduntil they rate.
price > 0 filter removing unrated
rows — not missing data.
Related
- Usage records: the per-record billing feed — the ledger rows behind every aggregate on this page.
- Wallets, credits, and charges — how each resolved row turns into a wallet debit.
- Cost-center chargeback — how
metadata.cost_centertags feed the department rollup. - Read the cost-intelligence dashboards — the operator walkthrough for the Insights → Costs page itself.