> ## Documentation Index
> Fetch the complete documentation index at: https://docs.orbit.devotel.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Wallet spend-channel attribution: how each debit gets its channel

> The wallet ledger stores no channel column. Every spend-by-channel surface derives each debit's channel from its reference stamp through one shared rule. This page documents the canonical vocabulary, the first-match-wins ordering, the Other catch-all, and how to reconcile the spend surfaces against statements and usage records.

# Wallet spend-channel attribution: how each debit gets its channel

Every spend surface in Orbit answers "how much did we spend per channel?"
from the same wallet ledger: the **Spend by channel** tiles on Billing →
Usage, the burn-rate forecast, spend anomalies, the statement's
usage-by-channel breakdown, and the AI usage split. This page explains how
a ledger that stores no channel column still answers that question
identically on every surface: each debit carries a `reference` string that
stamps what was charged, and one shared rule reads that stamp back into a
channel.

## 1. Why channel is not a ledger column

A wallet debit row carries three things you can rely on: the tenant it
belongs to, an idempotency key that collapses a retried charge into one row
(see [wallets, credits, and
charges](https://docs.orbit.devotel.io/concepts/wallets-credits-and-charges.md)), and a `reference` naming
the charge, for example `charge:sms_mccmnc:msg_4f8c1d`. There is no
`channel` column on the row. Instead, every charge writer stamps the
channel into the `reference` it records, and the spend surfaces derive the
channel back out of that stamp at read time.

That derivation lives in exactly one place: the billing package's
`channel-bucket` module, as a TypeScript function and its SQL twin, a
single CASE expression over the reference. The Usage spend-by-channel
query, the burn-rate forecast, and the spend-anomaly scope all call that
one classifier. The customer-visible consequence is a parity contract:
every spend surface buckets identically **by construction**, so two tiles
on the same dashboard can no longer disagree about which channel a dollar
belongs to. If a debit's reference changes how it is stamped, every
surface moves it together, because there is only one rule to change.

## 2. The canonical channel vocabulary

Each debit lands in exactly one of these buckets:

* **sms** — per-segment SMS sends, the SMPP preflight charge, and the
  retry/scheduled arms. `charge:sms_mccmnc:msg_4f8c1d`, `charge:smpp…`,
  `sms_retry:…`
* **mms** — MMS sends. `charge:mms_sent:msg_…`
* **whatsapp** — WhatsApp sends, including WhatsApp Business Calling
  (`whatsapp_calling:…`). Business Calling is WhatsApp spend, not voice:
  one product stays on one row.
* **voice** — call charges, holds, and dialer calls. `charge:voice…`,
  `voice_hold:…`, `charge:dialer…`, `dialer_…`
* **email** — `charge:email_sent:…` and other `email_…` references.
* **rcs** — `charge:rcs_sent:…` and other `rcs_…` references.
* **telegram** — `charge:telegram_sent:…` and other `telegram_…`
  references.
* **viber** — `charge:viber_sent:…` and other `viber_…` references.
* **fax** — `charge:fax_sent:…` and other `fax_…` references.
* **push** — `charge:push_sent:…` and other `push_…` references.
* **verify** — OTP sends: `verify_sms`, `verify_email`, `charge:verify…`.
  A verification is billed as verify whatever transport carried the code;
  an OTP sent over SMS shows under verify, not sms, because you bought a
  verification, not a message.
* **agents** — LLM and agent spend: `agent_llm:…`, `charge:llm…`,
  `charge:ai_…`, and the exact reference `ai_compose_campaign`.
* **numbers** — number purchases, lookups, and rentals:
  `number_purchase…`, `number_lookup…`, and other `number_…` references.
* **marketplace** — marketplace installs: `marketplace_install…`.
* **api** — reserved for metered API usage: `meter:api_…` descriptors from
  the reseller metering bridge. No live debit mints these references
  today, so the bucket exists in the vocabulary but carries no spend
  figure until such a writer ships.
* **other** — the deliberate catch-all for any reference shape newer than
  the vocabulary above.

## 3. Ordering and the catch-all rule

The classifier is a CASE evaluated top to bottom; the first matching arm
wins, exactly as in SQL. Three rules fall out of that ordering:

* **Mid-reference channels are decomposed first.** Scheduled sends carry
  the channel in the middle of the reference
  (`charge:scheduled_message:whatsapp:msg_…`), so that shape is unpacked
  before the prefix arms run; a scheduled WhatsApp send buckets as
  whatsapp, not as something else.
* **The generic send shape is matched per channel, never as a wildcard.**
  `charge:<channel>_sent:…` is checked once per known channel. A reference
  from a channel the vocabulary does not know therefore lands in `other`
  instead of inventing a bucket name out of the reference text.
* **`other` stays last, and rows are never dropped.** Every debit lands in
  exactly one bucket, so the per-channel rows always sum back to the
  period's total spend and to the balance movement on the statement.

Read "money under Other" with that in mind: it is not lost money. It means
"a reference shape newer than the current vocabulary" — visible on every
spend surface and reconcilable against the transactions feed. A metadata
channel tag, where a writer stamps one, can only move a row out of
`other`; it never moves a row between two named channels.

## 4. The surfaces one classifier feeds

* **Billing → Usage, Spend by channel** — the per-channel tiles for the
  current period, the most direct rendering of the vocabulary above.
* **Burn-rate forecast** — the `by_channel` rows of the month-end
  projection, same buckets ([billing burn
  rate](https://docs.orbit.devotel.io/concepts/billing-burn-rate.md)).
* **Spend anomalies** — the wallet scope the anomaly detector watches is
  bucketed by the same rule ([anomaly insights
  model](https://docs.orbit.devotel.io/concepts/anomaly-insights-model.md)).
* **Statements** — the usage-by-channel JSON and CSV fold the same
  attribution into statement line types for the invoice-facing contract
  ([billing documents model](https://docs.orbit.devotel.io/concepts/billing-documents-model.md)): smaller
  messaging channels share a `messaging` line and agent spend reports on
  an AI line, but the underlying attribution is the one above.
* **AI usage split** — the drill-down of the agents bucket by agent and
  model ([AI usage breakdown](https://docs.orbit.devotel.io/guides/billing-ai-usage-split.md)).

Contrast that with `GET /api/v1/usage-records`, which carries an
authoritative `channel` column: metering writes it at send time, and
nothing is derived. When you reconcile a ledger reference to a usage
record, join on the record id embedded in the reference:
`charge:sms_mccmnc:msg_4f8c1d` names the record `msg_4f8c1d`, whose own
`channel` says `sms`.

## 5. Worked reconciliation example

Three ledger rows, read top to bottom by the first-match rule:

| Ledger `reference` | Derived bucket |
| - | - |
| `charge:sms_mccmnc:msg_9f2e` | sms |
| `ai_compose_campaign` | agents |
| `verify_sms` | verify |

In the **statement CSV**, the same three debits appear on the statement's
invoice-facing lines: an `sms` usage line, an AI usage line, and a verify
line. The line types differ from the dashboard bucket names by design;
the attribution underneath is the same, and the lines still sum to the
period's usage total.

In **usage records**, the first row joins by the id in its reference to
the record `msg_9f2e`, whose authoritative `channel` agrees with the
derived bucket. The AI compose debit has no per-message row there; its
drill-down is the AI usage split. The verify debit is a product-level
charge: `verify_sms` names the transport, not a record id, so it
reconciles against the verify line and the window total rather than a
row-level join.

## 6. Boundaries

* This page is advisory. It documents how to read the spend surfaces;
  there is no per-tenant channel mapping to configure.
* The vocabulary above is what ships today. The `api` bucket is
  declarative-only, so do not build reporting on the expectation of
  channels or reference shapes that are not listed here.
* An `other` row in a period is expected whenever a reference shape is
  newer than this vocabulary. It is a prompt to contact support only if
  it persists across the next release; a one-period appearance is the
  catch-all doing its job.

## Related

* [The billing model map](https://docs.orbit.devotel.io/concepts/billing-model-map.md) — which billing
  page owns which question.
* [Wallets, credits, and charges](https://docs.orbit.devotel.io/concepts/wallets-credits-and-charges.md) —
  the ledger this attribution reads from.
* [Billing burn rate](https://docs.orbit.devotel.io/concepts/billing-burn-rate.md) — the `by_channel`
  forecast rows.
* [Channel cost rollup vs the billing ledger](https://docs.orbit.devotel.io/concepts/cost-channel-rollup-model.md) —
  analytics channel vs ledger-derived channel.
* [Billing documents model](https://docs.orbit.devotel.io/concepts/billing-documents-model.md) — statement
  usage-by-channel and the CSV.
* [AI usage breakdown](https://docs.orbit.devotel.io/guides/billing-ai-usage-split.md) — the agents bucket
  drilled down.
* [Usage records model](https://docs.orbit.devotel.io/concepts/usage-records-model.md) — the feed with the
  authoritative channel column.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.