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. The two-cost-attribute axes: analytics rollup over priced sends vs row-level ledger
To understand reconciliation, separate the two axes along which usage costs are measured:
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 by
metadata.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. Reconciliation walkthrough: a per-period, per-channel, per-currency join
Reconciliation between the analytics rollup and the billing ledger cannot be done by matching row for row. Attempting a 1:1 row match fails because:- Rollups aggregate multiple SMS segments into a single billed message count while tracking segment units separately.
- Inbound messages create ledger rows but contribute zero spend to the outbound channel rollup.
- Unrated or failed rows remain visible on the ledger but are intentionally absent from rollup totals.
Step 1: Define the window boundaries and reader parameters
Align the temporal boundaries precisely. If querying June 2026:- Rollup query:
GET /api/v1/analytics/costs?start_date=2026-06-01&end_date=2026-06-30&group_by=month - Ledger query:
GET /api/v1/usage-records?start_date=2026-06-01T00:00:00Z&end_date=2026-06-30T23:59:59Z
Step 2: Filter the ledger stream
Before aggregating the raw ledger rows, apply the platform projection predicates:direction = 'outbound'(exclude inbound traffic, which is unpriced on outbound rollup cards).price IS NOT NULL AND price > 0(exclude failed delivery attempts and unrated rows).- Group rows by
(channel, currency).
Step 3: Compute the join metrics
For each(channel, currency) bucket, compute:
ledger_units = SUM(units)(for SMS,unitsrepresents carrier segments; for voice, billable duration/increments).ledger_spend = SUM(price)ledger_priced_events = COUNT(*)
Step 4: Evaluate the join equality
Join the computed bucket toby_channel[] from the analytics payload on channel and currency:
- Verify
by_channel[channel].total_spend == ledger_spend. - Verify
by_channel[channel].total_segments == ledger_units(for SMS). - Verify
by_channel[channel].total_messages == ledger_priced_events.
3. Two worked scenarios: divergence resolution and the deliberate guard
Scenario A: Month-end close divergence resolution
During the June month-end financial close, an enterprise tenant reconciles SMS traffic inUSD:
- Analytics Rollup (
GET /api/v1/analytics/costs):channel: "sms",currency: "USD"total_spend: $102.48total_messages: 11,760total_segments: 13,560
- Billing Ledger (
GET /api/v1/usage-records):- Total records returned in window:
12,240rows. - Raw row status breakdown:
- 11,760 outbound records with
price > 0, summing to $102.48 and 13,560 segments. - 240 outbound records with
price = null(unrated carrier batch awaiting tariff table update). - 240 inbound SMS records with
direction: "inbound",price: null.
- 11,760 outbound records with
- Total records returned in window:
- Filter out the 240 inbound rows: remaining count is 12,000 outbound rows.
- Isolate the 240 unrated rows: these represent outbound sends whose carrier destination rate was pending resolution at query time.
- Apply the join: the 11,760 rated outbound records match the rollup’s
total_messages(11,760),total_segments(13,560), andtotal_spend($102.48) to the exact cent. - Close action: the finance ledger books $102.48. The 240 unrated records are placed in the billing accrual hold until rated, at which point an adjusting debit entry posts to the ledger and increments the subsequent rollup window.
Scenario B: Chargeback batch divergence and the deliberate guard
An organization usesmetadata.cost_center tags to allocate marketing and transactional messaging costs across departments. A monthly chargeback report is run via GET /api/v1/billing/chargeback-rollup?days=30:
- Department rollups:
dept_marketing: $4,200.00 (420,000 SMS segments)dept_support: $1,800.00 (180,000 SMS segments)unallocated: $350.00 (35,000 SMS segments)- Chargeback Total: $6,350.00
- Analytics Rollup (
GET /api/v1/analytics/costs):- Reports
total_spend: $6,000.00across 600,000 segments. - Divergence: the chargeback rollup reports $350.00 more than the analytics rollup.
- Reports
metadata.cost_center: "dept_marketing" while the outbound rating pipeline temporarily queued supplier DLRs. The chargeback report walked all debit entries including retroactive carrier surcharges, whereas the cached analytics rollup view had not yet refreshed its hourly rollforward window.
The Deliberate Guard:
The platform enforces a deliberate guard: The ledger is the immutable single source of financial truth; the analytics rollup is non-authoritative.
Under no circumstances may an invoicing or chargeback system write back an adjustment to the ledger based on an analytics rollup mismatch. When divergence occurs:
- The chargeback engine reads the
unallocatedbucket to identify missing tags. - The ledger debit stream dictates the actual cash balance deduction from the prepaid wallet.
- The analytics cache auto-refreshes within its bounded synchronization interval without altering ledger state.
4. The ‘rollup pays for sends the ledger hasn’t debited yet’ scenario
A common operational condition occurs in high-throughput pipelines during near-real-time checks: the analytics rollup displays spend for sends that have not yet debited the wallet ledger.The Lifecycle Race
- Pre-send quoting and submission: When a message is accepted by the API, Devotel Orbit verifies the tenant’s wallet balance against the route price quote.
- Telemetry emission: The messaging pipeline immediately emits a priced event to the analytics ingest stream (
price = $0.008). - Rollup aggregation: The streaming analytics engine incorporates the event into the current hourly aggregate bucket within seconds.
- Asynchronous carrier DLR and ledger posting: In accordance with the two-phase billing lifecycle and usage metering pipeline, final wallet balance debiting occurs upon downstream carrier handoff and segment settlement. Under high message queues or SMPP window backpressure, carrier debit reconciliation can lag telemetry by seconds to minutes.
Operational Interpretation
If you pullGET /api/v1/analytics/costs simultaneously with GET /api/v1/billing/balance, you may observe:
- Analytics rollup shows 50,000 sends costing $400.00.
- Wallet ledger transactions reflect only 46,000 debits totaling $368.00.
- Unsettled difference: 4,000 messages ($32.00) currently in transit.
5. 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.
6. 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.
7. 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.
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.
- Request-metrics reader gates — the boundary surfaces and window rules governing telemetry reads.
- LLM spend attribution — feature × model × conversation cost attribution and provider reconciliation.