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

# Building a usage dashboard

A usage dashboard typically combines several views of a customer's account: the bill they're on track to be charged, the credits they have left, where their spend is coming from, and how usage is trending. Each view maps to a different Orb endpoint, with different performance, caching, and grouping characteristics. Choosing the right endpoint for each view, and composing them carefully on the page, is what keeps the dashboard fast and the numbers consistent with the invoice.

## What a usage dashboard usually contains

Common dashboard sections:

* A current period total — the bill the customer is on track to be charged at the end of the period.
* A credit balance — for prepaid plans, the remaining credits and the rate of consumption.
* A breakdown by group — top-N workspaces, projects, regions, or any cost-driving dimension. A specialized dimension in Orb is the [license scope](/product-catalog/license-pricing).
* A time series — usage or cost over the current billing period.
* An ad-hoc analytics surface — a freeform query workspace for power users and support.

Each section is best served by a different endpoint. A common source of bugs is using an endpoint optimized for one section in another — for example, using the upcoming invoice for a per-workspace breakdown, or running `prices/evaluate` on first paint.

## Bill preview vs. cost breakdown

There are two framings for what a customer sees:

* **Bill preview** shows what they'll be charged: line items, discounts, minimums, taxes, credits drawn down. The relevant number is `amount_due`, and the source of truth is the upcoming invoice.
* **Cost breakdown** shows where spend is coming from: which prices, which groups, which days. The relevant number is the subtotal, not the amount due.

Discounts, minimums, and maximums don't decompose by group, so avoid showing these in any breakdown concept. A 10% account-level discount can't be attributed to "this workspace contributed \$X to the discount," and a \$1,000 minimum can't be split across regions in any non-arbitrary way. A breakdown that shows "amount due by group" will produce numbers that don't reconcile to the invoice total.

In practice:

* The headline "what will I be charged" tile sources from the upcoming invoice.
* All breakdowns (by group, by day, by price) source from cost or subtotal endpoints, and the UI labels them as subtotals.
* Breakdown cards link out to the upcoming invoice for the full bill.

## Endpoints by dashboard section

| Dashboard section   | Endpoint                                                                                                                                    | Cacheable on first paint |
| ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------ |
| Current bill total  | [Upcoming invoice](/api-reference/invoice/fetch-upcoming-invoice)                                                                           | Yes (cache header)       |
| Credit balance      | [Credit ledger](/api-reference/credit/fetch-customer-credits-ledger) / [credit blocks](/api-reference/credit/fetch-customer-credit-balance) | Yes (cache header)       |
| Breakdown by group  | Grouped costs *(private preview)*                                                                                                           | Yes (precomputed)        |
| Cost by license     | [License usage](/api-reference/license/get-usage-for-all-licenses-of-a-type-on-a-subscription)                                              | No                       |
| Time series (cost)  | [Subscription costs](/api-reference/subscription/fetch-subscription-costs) / [customer costs](/api-reference/customer/fetch-customer-costs) | Yes (cache header)       |
| Time series (usage) | [Subscription usage](/api-reference/subscription/fetch-subscription-usage)                                                                  | Yes (cache header)       |
| Freeform analytics  | [`prices/evaluate`](/api-reference/price/evaluate-multiple-prices)                                                                          | No (runs live)           |

### Current bill total — upcoming invoice

[`GET /v1/invoices/upcoming?subscription_id=…`](/api-reference/invoice/fetch-upcoming-invoice) returns the draft invoice for the current period: line items, subtotal, total, and amount due. This is the only endpoint that runs the full bill computation (discounts, minimums, maximums, taxes), so it is the only correct source for the headline total.

The endpoint is scoped to a single subscription. For customers with multiple active subscriptions, call it once per subscription and render the results separately; discount and minimum math does not compose across subscriptions either.

Calling against a subscription whose status is `ended` returns a 400. Calling against a subscription that has no pending invoice (for example, a plan with no recurring fees) returns a 404; render a "no upcoming invoice" empty state rather than an error.

### Credit balance — credit ledger

[`GET /v1/customers/{id}/credits/ledger`](/api-reference/credit/fetch-customer-credits-ledger) returns the customer's credit ledger, scoped to a pricing unit, in newest-first order. The first entry's `ending_balance` is the current balance. Each entry includes a `type` (`increment`, `decrement`, `expiration_change`, `credits_expiry`, `void`, `void_initiated`), an `amount`, and a timestamp, so the same response can power the headline balance, a recent-activity feed, and a simple burn-rate projection.

If the customer has no ledger for the requested pricing unit, the endpoint returns an empty paginated list. Treat this as "no credits," not as an error.

For block-level detail — per-block expiry dates and cost basis — use [`GET /v1/customers/{id}/credits`](/api-reference/credit/fetch-customer-credit-balance). This returns a paginated list of credit blocks, useful for warnings such as "X credits expiring on date Y."

### Cost breakdown: grouped costs

<Info>
  **This feature is in private preview.**

  Grouped costs are currently available to a limited set of customers. Reach out to your Orb contact to get access.
</Info>

`POST /v1/subscriptions/{id}/grouped_costs` returns subtotals over a bounded window, grouped by a registered event property. Subtotals are read from precomputed data in Orb's system, so the endpoint is fast enough to call on first paint even at moderate cardinality.

The request body for `POST /v1/subscriptions/{id}/grouped_costs`:

```json theme={null}
{
  "grouping_keys": ["workspace_id"],
  "timeframe_start": "2026-05-01T00:00:00Z",
  "timeframe_end":   "2026-05-23T00:00:00Z",
  "currency": "usd",
  "limit": 10
}
```

The response echoes the requested timeframe and grouping, and returns the top buckets ordered by `subtotal` descending. `grouping_key` and each bucket's `grouping_value` are parallel lists, so composite groupings line up positionally. Subtotals are strings, and `has_more` is `true` when there were more groups than `limit`:

```json theme={null}
{
  "timeframe_start": "2026-05-01T00:00:00Z",
  "timeframe_end": "2026-05-23T00:00:00Z",
  "currency": "usd",
  "full_subtotal": "21043.78",
  "grouping_key": ["workspace_id"],
  "has_more": true,
  "groups": [
    { "grouping_value": ["ws_4f8a2c"], "subtotal": "4821.55" },
    { "grouping_value": ["ws_91be07"], "subtotal": "3190.00" },
    { "grouping_value": ["ws_2d7c14"], "subtotal": "2756.83" },
    { "grouping_value": ["ws_a03f9b"], "subtotal": "1944.20" },
    { "grouping_value": ["ws_6e1d80"], "subtotal": "1502.97" },
    { "grouping_value": ["ws_b7c350"], "subtotal": "1188.40" },
    { "grouping_value": ["ws_0c9a26"], "subtotal": "947.11" },
    { "grouping_value": ["ws_5fd418"], "subtotal": "612.06" },
    { "grouping_value": ["ws_e84b71"], "subtotal": "433.89" },
    { "grouping_value": ["ws_77a2cd"], "subtotal": "208.50" }
  ],
  "next_cursor": ["ws_77a2cd"]
}
```

For a composite grouping (for example `["org_id", "workspace_id"]`, which requires enablement), `grouping_key` and each `grouping_value` carry two parallel entries: `"grouping_value": ["org_a", "ws_4f8a2c"]`.

The response also returns `full_subtotal`—the subtotal of *all* groups over the queried period, not just the returned top `limit`—so you can display the total spend, the top groups, and any residual.

The endpoint is paginated. When `has_more` is `true`, pass the response's `next_cursor` back as the request's `cursor` to fetch the next page of groups, ordered by `subtotal` descending; `next_cursor` is `null` once there are no further groups. For a composite grouping, `cursor` and `next_cursor` are positional lists that line up with `grouping_keys`. Note that this endpoint does not group by day by default, unlike other cost endpoints.

The endpoint has several constraints; these are quotas that may be adjusted:

* The window between `timeframe_start` and `timeframe_end` cannot exceed 32 days by default.
* `timeframe_start` cannot be more than 100 days in the past.
* Each grouping key needs to be enabled per-account; a key that isn't allowlisted returns a 400.
* A single grouping key is allowed by default; composite grouping is supported but requires enablement.
* This endpoint will return a validation error if there are more than 5,000 distinct groups in the defined window for the subscription (regardless of how many are returned in the response).

Please reach out to Orb support if you have a use case that requires extended quotas.

<Note>
  For [license prices](/product-catalog/license-pricing), grouped subtotals are computed as a per‑group `quantity × rate` and **do not account for per‑license pool drawdowns**. The grouped value is a usage‑attribution estimate at the marginal rate, not a reconciliation of the invoice. On license‑priced plans, the sum of per‑group subtotals will not match the line item's billed amount, which reflects the aggregate overage after drawdowns. To understand allocation usage per license, use the dedicated [Cost by license](#cost-by-license) endpoint instead.
</Note>

### Cost by license

If you're using Orb's [licenses feature](/product-catalog/license-pricing), you should fetch [usage per license](/api-reference/license/get-usage-for-all-licenses-of-a-type-on-a-subscription) using the dedicated, paginated API. Note that this endpoint accepts a `group_by`, allowing you to group usage by license, day, or both.

Day buckets and date windows are evaluated in the customer's timezone, matching license activation and deactivation dates. `remaining_credits` is never negative; when usage exceeds a license's allocation, it returns `0` while overage is reflected in `consumed_credits`.

Just like other cost data, license usage does not necessarily correlate to the `amount_due` on your invoice because of the presence of other adjustments and discounts. Relatedly, usage beyond a license's allocation may be deducted from other grants on the credit ledger, accrue as overage on the invoice, or be written off entirely depending on your license configuration. Therefore, we recommend you use this endpoint to understand how much each license has used of its allocation, and consider displaying the amount owed for the full customer separately.

### Time series — costs or usage

For a daily cost chart, use [`GET /v1/subscriptions/{id}/costs`](/api-reference/subscription/fetch-subscription-costs) for a single subscription, or [`GET /v1/customers/{id}/costs`](/api-reference/customer/fetch-customer-costs) to roll up across subscriptions that overlap the requested timeframe (overlapping days are summed). The response is one entry per day, each with `subtotal`, `total`, and a per-price breakdown.

The `view_mode` parameter controls how each datapoint is computed:

* `cumulative` (default) — each datapoint's `timeframe_start` is the start of the billing period, and `timeframe_end` advances by one day. Each datapoint represents "what the bill would be if the period ended today," including any minimums and discounts.
* `periodic` — each datapoint is the increment for that day, with a one-day window.

Use `cumulative` when the pricing involves minimums or discounts. Use `periodic` for a day-over-day bar chart, when the pricing doesn't have period-level adjustments that need to be reflected per-day.

For a daily usage chart (raw quantity, independent of pricing), use [`GET /v1/subscriptions/{id}/usage`](/api-reference/subscription/fetch-subscription-usage) with `granularity=day`. Usage can be grouped by an event property if `billable_metric_id` is also passed.

Grouping behavior depends on whether the metric is decomposable:

* Decomposable metrics (sums, counts) can be grouped by any event property.
* Non-decomposable metrics (unique counts) can only be grouped by the price's `invoice_grouping_key`. Per-day "uniques" do not sum to the period total, so arbitrary grouping is not mathematically meaningful.

For non-decomposable metrics where arbitrary grouping is required, use grouped costs (which operates in subtotal-space) instead.

Day buckets are demarcated by the customer's local midnight, including daylight savings. Both endpoints handle this server-side; do not re-bucket UTC timestamps on the client.

### Freeform analytics: prices/evaluate

[`POST /v1/prices/evaluate`](/api-reference/price/evaluate-multiple-prices) runs rating math dynamically over ingested events. The request body accepts:

* A `customer_id` or `external_customer_id`.
* A `timeframe_start` and `timeframe_end` (start cannot be more than 100 days in the past).
* A `price_evaluations` list of up to 100 prices. Each entry specifies one of `price_id`, `external_price_id`, or an inline price (a floating-price definition for what-if scenarios).
* A boolean filter expression per price evaluation, with computed properties supported.
* A `grouping_keys` list of properties or computed-property expressions (e.g. `hour_floor_timestamp_millis(timestamp_millis)` to bucket hourly).
* Optional `metric_parameter_overrides` for parameterized metrics.

Because evaluation is dynamic, there is no precompute and no cache. High-cardinality grouping or long timeframes can take on the order of seconds. Place this endpoint behind an explicit "Run query" affordance in the UI (a tab or button), not on the first-paint path.

For what-if flows that should evaluate against synthetic events rather than ingested usage (for example, proposal or quoting tools), use [`POST /v1/prices/evaluate_preview_events`](/api-reference/price/evaluate-preview-events). The body is the same as evaluate, plus an `events` array of synthetic preview events.

## Caching

Dashboards are read-heavy and first-paint-sensitive. The [cache header](/api-reference/cached-responses) is the main lever for keeping load times low:

```
Orb-Cache-Control: cache
Orb-Cache-Max-Age-Seconds: 300
```

When sent on a supported endpoint, Orb returns a precomputed snapshot if one is fresher than `Orb-Cache-Max-Age-Seconds`. The response includes an `Orb-Cache-Updated-At` header, which can be surfaced in the UI as "Updated N minutes ago." Without `Orb-Cache-Max-Age-Seconds`, any precomputed value is acceptable.

Cache support varies by endpoint:

| Cache behavior                  | Endpoints                                                                                                                                                      |
| ------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Honors the cache header**     | Upcoming invoice, subscription costs, subscription usage, customer costs, credit ledger, and credit balance.                                                   |
| **Fast for a different reason** | Grouped costs reads precomputed metric values internally, so it is first-paint-safe regardless of headers. The cache header itself does nothing on this route. |
| **Never cached, by design**     | `prices/evaluate` runs live every time.                                                                                                                        |

## Composing the dashboard

A typical per-customer dashboard, organized by load tier:

* **First paint, in parallel:**
  * Upcoming invoice for each active subscription (headline total).
  * Credit ledger (current balance, recent activity).
  * Subscription costs with `view_mode=cumulative` or `periodic` (time series card).
  * Grouped costs with the relevant grouping key (top-N breakdown).
  * "Updated N minutes ago" indicator sourced from `Orb-Cache-Updated-At`.
* **On user-initiated refresh:**
  * Re-issue the first-paint calls without the cache header (or with a small `Orb-Cache-Max-Age-Seconds`).

This gives roughly five or six cache-aware or precompute-backed calls on first paint, no dynamic rating queries on the load path, and no breakdown card attributing amount due.

## Rate limits

Orb's rate limits are [documented separately](/api-reference/rate-limits).

Think of these rate limits as the number of users who can concurrently do a fresh load of a usage dashboard, rather than those who can be browsing their usage. These rate limits are calibrated against our experience; in practice, we find that for accounts with millions of end-users, a small subset are active at any given time, a much smaller subset need a live view of their usage, and a fraction of those end users are re-loading their usage simultaneously.

## Notes on specific cases

* **Multi-subscription customers.** Most endpoints are subscription-scoped. Render per-subscription cards rather than collapsing across; [`GET /v1/customers/{id}/costs`](/api-reference/customer/fetch-customer-costs) is the exception for rolling up across subscriptions that overlap a timeframe.
* **Credit-denominated plans.** A currency value can be a [credit pricing unit](/product-catalog/credit-systems) rather than ISO 4217. Render credits as "1,200 credits," not "\$1,200.00."
* **Webhooks for lifecycle, cache for freshness.** [`invoice.issued`, `invoice.edited`, and `invoice.payment_succeeded`](/integrations-and-exports/webhooks) are appropriate signals for refreshing the headline tile when discrete invoice events occur. There is no webhook that fires when the upcoming invoice total changes by a small amount; that kind of continuous freshness is the cache header's job. `subscription.usage_exceeded` and `subscription.cost_exceeded` fire when a configured threshold is crossed, and are appropriate for alert UIs rather than general invalidation.

## Common pitfalls

* **Do not display a grouped breakdown of `amount_due`.** Discounts, minimums, and maximums do not decompose by group. Breakdown cards work in subtotal-space; link out to the upcoming invoice for the bill.
* **Do not put `prices/evaluate` on the first-paint path.** It is a dynamic endpoint. Place it behind an explicit tab and "Run" button.
* **Do not re-bucket timestamps client-side.** Day boundaries respect the customer's timezone on the server. Client-side re-bucketing breaks during DST and for non-UTC customers.
* **Do not expect grouped costs to honor the cache header.** The endpoint is fast for a different reason; sending the headers has no effect.
