Skip to main content

Channel cost rollup vs the billing ledger model

Devotel Orbit exposes two surfaces for the same metered usage:
  1. Insights → Costs — the aggregate view. GET /api/v1/analytics/costs returns totals, a time series, and a per-channel breakdown over a date window, grouped by group_by (day, week, or month).
  2. Billing → Usage records / Cost-center chargeback — the ledger view. One row per priced message or completed call, and the metadata.cost_center rollup that attributes spend to a department.
Both read the same underlying rows, but they are not the same model. The rollup is a planning surface; the ledger is the invoicing surface. Know which one you are looking at before you treat a number as the answer.

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:
  1. Rollups aggregate multiple SMS segments into a single billed message count while tracking segment units separately.
  2. Inbound messages create ledger rows but contribute zero spend to the outbound channel rollup.
  3. Unrated or failed rows remain visible on the ledger but are intentionally absent from rollup totals.
Instead, execute a per-period, per-channel, per-currency join. Follow these steps:

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
For reader gating rules and window precedence across telemetry reads, see the request-metrics reader gates specification.

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, units represents 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 to by_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.
When all three assertions hold across all active channels, the period is balanced.

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 in USD:
  • Analytics Rollup (GET /api/v1/analytics/costs):
    • channel: "sms", currency: "USD"
    • total_spend: $102.48
    • total_messages: 11,760
    • total_segments: 13,560
  • Billing Ledger (GET /api/v1/usage-records):
    • Total records returned in window: 12,240 rows.
    • 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.
Resolution Procedure:
  1. Filter out the 240 inbound rows: remaining count is 12,000 outbound rows.
  2. Isolate the 240 unrated rows: these represent outbound sends whose carrier destination rate was pending resolution at query time.
  3. Apply the join: the 11,760 rated outbound records match the rollup’s total_messages (11,760), total_segments (13,560), and total_spend ($102.48) to the exact cent.
  4. 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 uses metadata.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.00 across 600,000 segments.
    • Divergence: the chargeback rollup reports $350.00 more than the analytics rollup.
Investigating the Divergence: Inspection of the raw usage records reveals that during a campaign blast, 35,000 SMS messages were sent with 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:
  1. The chargeback engine reads the unallocated bucket to identify missing tags.
  2. The ledger debit stream dictates the actual cash balance deduction from the prepaid wallet.
  3. 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

  1. 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.
  2. Telemetry emission: The messaging pipeline immediately emits a priced event to the analytics ingest stream (price = $0.008).
  3. Rollup aggregation: The streaming analytics engine incorporates the event into the current hourly aggregate bucket within seconds.
  4. 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 pull GET /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.
This transient delta is expected system behavior. The analytics surface reflects incurred demand, providing immediate visibility into burn rate, while the ledger reflects settled debits. Once carrier dispatch completes, the ledger catches up with zero loss of fidelity. For AI token expenditure, an identical dual-stream model operates across feature, model, and conversation dimensions. See LLM spend attribution for how model token reservations reconcile with actual provider settlements.

5. The numerics — currency, rounding, and segment attribution

Currency. Every rollup row carries an ISO-4217 currency. 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.