> ## 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.

# Cost-center chargeback: attribute spend to a department

> Tag sends with a metadata.cost_center label, then pull the department-first chargeback rollup to see allocated vs unallocated spend, per-channel splits, and each department's share of the bill.

# Cost-center chargeback

Internal chargeback starts with one habit: tag every send with the department
that should carry its cost. Devotel Orbit turns those tags into a
department-first statement — every cost center as a line item with its
per-channel split and share of the period total, plus the totals for spend
that is still untagged.

This guide covers the full loop:

1. Tag sends with a `metadata.cost_center` label.
2. Read the rollup at `GET /api/v1/billing/chargeback-rollup`.
3. Cross-check a single tag on `GET /api/v1/billing/usage-by-channel`.
4. Export the period with the statement exporter.

***

## 1. Tag sends with a cost center

Set a free-form string on the request `metadata` object. The same field works
on every messaging send endpoint (`/sms`, `/mms`, `/rcs`, `/whatsapp`,
`/email`, …) and on outbound voice calls — any entry you put there is stored
on the message or call row.

```bash theme={null}
curl -X POST "https://api.orbit.devotel.io/api/v1/sms" \
  -H "X-API-Key: dv_live_sk_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "to": "+14165550100",
    "from": "+14167770000",
    "text": "Your invoice is ready.",
    "metadata": { "cost_center": "dept_sales" }
  }'
```

Pick a naming convention for the value and hold to it. The tag is a plain
string — no case-folding, no canonicalisation — so `dept_sales` and
`Dept_Sales` are two different cost centers. A short, stable convention like
`dept_<name>` or `<name>_<region>` keeps the rollup readable.

Tagging works from today onward. Only outbound rows carry price, so inbound
traffic never enters the totals.

***

## 2. The chargeback rollup

`GET /api/v1/billing/chargeback-rollup` folds the tagged sends into a
department-first statement over a rolling window:

```bash theme={null}
curl "https://api.orbit.devotel.io/api/v1/billing/chargeback-rollup?days=30" \
  -H "X-API-Key: dv_live_sk_your_key_here"
```

| Query parameter | Notes                                                                                        |
| --------------- | -------------------------------------------------------------------------------------------- |
| `days`          | Lookback window, 1–90 (default 30). Invalid values return a `400` with a Zod error envelope. |

The response is department-first: a grand total, the split of allocated
(tagged) against unallocated (untagged) spend, and one line per cost center
with its per-channel breakdown and its share of the period:

```json theme={null}
{
  "grand_total_minor": 47729,
  "grand_total": "477.29",
  "allocated_minor": 37042,
  "allocated": "370.42",
  "unallocated_minor": 10687,
  "unallocated": "106.87",
  "allocated_pct": 77.6,
  "cost_center_count": 2,
  "cost_centers": [
    {
      "cost_center": "dept_sales",
      "unallocated": false,
      "total_messages": 128010,
      "total_cost_minor": 31015,
      "total_cost": "310.15",
      "pct_of_total": 65.0,
      "by_channel": [
        { "channel": "sms", "total_messages": 120400, "total_cost_minor": 24012, "total_cost": "240.12" },
        { "channel": "whatsapp", "total_messages": 7610, "total_cost_minor": 7003, "total_cost": "70.03" }
      ]
    },
    {
      "cost_center": "dept_support",
      "unallocated": false,
      "total_messages": 42210,
      "total_cost_minor": 6027,
      "total_cost": "60.27",
      "pct_of_total": 12.6,
      "by_channel": [
        { "channel": "voice", "total_messages": 950, "total_cost_minor": 4110, "total_cost": "41.10" },
        { "channel": "sms", "total_messages": 41260, "total_cost_minor": 1917, "total_cost": "19.17" }
      ]
    },
    {
      "cost_center": "untagged",
      "unallocated": true,
      "total_messages": 51022,
      "total_cost_minor": 10687,
      "total_cost": "106.87",
      "pct_of_total": 22.4,
      "by_channel": [
        { "channel": "sms", "total_messages": 51022, "total_cost_minor": 10687, "total_cost": "106.87" }
      ]
    }
  ]
}
```

Amounts come back in both integer minor units (`*_minor`, cents) and a
two-decimal string. Channel lines sum exactly to their cost-center total, and
the cost-center lines sum exactly to the grand total — the rollup uses
integer arithmetic, so there is no rounding drift between the field groups.

Reading the response:

* `allocated` is spend carrying a real tag; `unallocated` is the untagged
  pool; `allocated_pct` tells you how much of the bill is ready to charge
  back. Watch that percentage rather than the raw totals.
* `cost_centers` is ordered by spend descending, with the `untagged` line
  always sorted last — real departments lead the report.
* The sourcing is your own metered usage ledger (message and voice prices),
  so figures exclude subscription fees, credits, discounts, and tax. Treat
  the rollup as a proportional allocation of usage spend, not invoice math.

***

## 3. Slice one cost center with `usage-by-channel`

The companion axis on `GET /api/v1/billing/usage-by-channel` answers the
inverse question: break a single cost center (or the whole tag dimension)
down by channel, country, campaign, or sender.

```bash theme={null}
curl "https://api.orbit.devotel.io/api/v1/billing/usage-by-channel?group_by=cost_center" \
  -H "X-API-Key: dv_live_sk_your_key_here"
```

Two behaviours matter:

* `group_by=cost_center` with no `cost_center` filter returns one bucket per
  tag — with no upper bound on the bucket list (other axes return the top
  100\). Use it to enumerate tags.
* Adding `cost_center=dept_sales` narrows the response to that tag, split by
  the axis you chose (e.g. `group_by=channel&cost_center=dept_sales`). The
  filter is only valid when `group_by` is one of the tag-bearing axes
  (`country`, `campaign`, `sender`, `cost_center`); on `group_by=channel` the
  filter is rejected with a `400`.

Both endpoints derive the tag the same way, so a given cost center totals to
the same figure on either surface.

***

## 4. Untagged spend and the `untagged` bucket

A send without a `metadata.cost_center` entry (or with an empty one) lands in
the `untagged` bucket on both surfaces. It is not dropped — it totals
separately and shows up as the `unallocated` totals in the rollup, so the
rollup always reconciles to the full bill.

Best practice is to require the tag at the send layer in your integration:
reject outbound sends that miss it, or stamp a fallback department server-side.
Drive `allocated_pct` toward 100 and treat what is left as the exception
queue. Before changing tags, remember the tag is a plain string: renaming a
value re-buckets history going forward, so settle the convention before
sends scale.

***

## 5. Access and rate limits

Both endpoints are read-only and gated to the billing role: owner, admin, or
billing session tokens (and API keys carrying the `billing:read` scope where
Billing is scope-able). They run under the standard billing read limit, and
responses are cached for 60 seconds, so a dashboard or polling script is not
running the aggregation on every load.

***

## 6. Where this fits with Billing overview and the statements feed

The passing mention of cost centers on the [Billing overview](/billing/overview)
page lands behind this guide: the `cost_center` axis and the rollup are the
attribution surfaces; overview then frames them against usage, the ledger,
and spend alerts.

For the month-end packet, the statement exporter serves the same period as
CSV or JSON:

* `GET /api/v1/billing/statements` — list monthly summaries.
* `GET /api/v1/billing/statements/:period` — a month's detail rows.
* `GET /api/v1/billing/statements/:period/download` — CSV or JSON export.

Tag the sends, watch the `allocated_pct`, export when the period closes.
