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

# Burn rate and wallet runway

> How GET /api/v1/billing/burn-rate forecasts wallet exhaustion from your average daily spend, how the forecast differs from spend caps and usage anomaly alerts, and how to poll it so a depleted wallet never surprises you.

# Burn rate and wallet runway

Orbit bills from a prepaid wallet. Every message, voice minute, and AI token
debits the balance as it happens, and every debit lands in the transaction
ledger behind `GET /api/v1/billing/transactions`. Because the wallet drains
with every send, the question a finance operator actually asks is not "what
did we spend this month?" but "at this pace, when does the wallet hit zero?"
Burn rate is that answer as a number you can poll.

`GET /api/v1/billing/burn-rate` returns your average daily spend over a
lookback window, the live wallet balance, and a projection of how many days
of balance remain at that pace. It is a read-only forecast: it reports the
pace, and it never throttles sending, never tops up the wallet, and never
sends a notification by itself. When you want the platform to act, pair it
with [spend caps](/billing/spend-caps) and
[usage anomaly alerts](/billing/usage-anomaly-alerts), covered below.

## 1. Why burn rate is a first-class metric

A prepaid wallet makes runway an operational concern. A wallet holding
$1,500 that spends $42.50 a day has about 35 days of sending left; when the
balance reaches zero, sends start failing for insufficient balance,
mid-campaign if nobody was watching. Spend reports tell you where the money
went after the fact. Burn rate tells you how long the current balance lasts
before the hard stop, while there is still time to top up or slow down.

It is deliberately different from the two guards that can act on their own:

* A **spend cap** is a hard threshold you set. When spend crosses it, Orbit
  pauses outbound sending. It is deterministic and enforced, but it fires
  only at the line you drew, not before.
* A **usage anomaly alert** notifies you when a metric crosses a threshold
  or deviates sharply from your learned baseline. It tells you something
  unusual is happening, not how long the wallet lasts.
* **Burn rate** is a forecast. It divides the balance by the recent daily
  pace every time you ask, so a traffic increase shows up as a shorter
  runway on the next poll, days before any cap or alert would trip.

## 2. The computation window

The endpoint aggregates wallet debits over a caller-selected lookback
window:

| Parameter | Values | Default | Meaning |
| - | - | - | - |
| `days` | 1 to 90 | 30 | Lookback window for the average daily spend. |

Within that window:

* **Only debits count as spend.** Top-ups and grants are excluded, refunds
  are excluded, and operator adjustments applied to your balance are
  excluded, so a large top-up yesterday does not read as "spending that
  much per day".
* **The divisor adapts to young accounts.** If your first spend inside the
  window is more recent than the full window, the average divides by the
  days actually elapsed (at least one) instead of the whole window. The
  response's `lookback_days` tells you which divisor was used, and
  `first_spend_at` gives the first spend timestamp in the window.
* **The projection is balance divided by pace.** `projected_days_remaining`
  is the live balance divided by the average daily spend, truncated to one
  decimal place. It is `null` when there is no recent spend, because there
  is no pace to divide by; dashboards render that as "no recent activity".
* **Results are cached for about a minute.** Polls inside that window
  return the same snapshot, so a dashboard that re-fetches on focus stays
  cheap and consistent. A poll also never observes a partially computed
  answer: concurrent polls share one computation.
* **A data blip degrades to zeros, never to an error.** If the billing data
  behind the endpoint is briefly unavailable, the endpoint still returns
  200 with zeroed projections rather than a 5xx, and the degraded snapshot
  is not cached, so the next poll recomputes and self-heals. Treat an
  all-zero response right after a healthy one as "retry in a minute", not
  as "spend stopped".

## 3. How the balance enters the forecast

The two halves of the ratio come from different places, and both are the
same numbers you see elsewhere in Orbit:

* `current_balance_minor` is the live wallet balance, the same value
  `GET /api/v1/billing/balance` and the dashboard balance widget report. If
  that live read is itself unavailable, the response reports a zero
  balance, and therefore zero days remaining, rather than an error.
* `avg_daily_spend_minor` is computed from the transaction ledger, the same
  ledger `GET /api/v1/billing/transactions` pages through.

Because the balance is read live on every poll while the spend average is
the cached window aggregate, a top-up extends `projected_days_remaining` on
the next poll even while the spend side of the ratio is still within its
cache minute.

All `*_minor` fields are integer cents of the wallet currency (USD);
divide by 100 for dollars. Top-ups paid in another currency are converted
into the wallet on the way in (see
[wallet currency conversion](/billing/wallet-currency-conversion)), so the
runway math is always single-currency.

## 4. Endpoint contract

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

Requires an owner, admin, or billing role. An off-spec `days` value
degrades to the 30-day default rather than rejecting the request.

Response fields, inside the standard `{ data, meta }` envelope:

| Field | Type | Meaning |
| - | - | - |
| `avg_daily_spend_minor` | integer | Average daily spend over the window, in cents. |
| `current_balance_minor` | integer | Live wallet balance, in cents. |
| `projected_days_remaining` | number or null | `current_balance_minor / avg_daily_spend_minor`, one decimal; `null` when there is no recent spend. |
| `lookback_days` | integer | Divisor actually used for the average (see section 2). |
| `first_spend_at` | string or null | ISO timestamp of the first spend inside the window. |
| `month_to_date_minor` | integer | Spend since the start of the current month (UTC). |
| `last_7d_total_minor` | integer | Spend over the trailing 7 days. |
| `last_7d_avg_minor` | integer | `last_7d_total_minor / 7`, rounded. |
| `last_month_total_minor` | integer | Previous calendar month's total spend. |
| `days_remaining_in_month` | integer | Days left after today in the current month. |
| `projected_month_end_minor` | integer | Month-to-date spend plus each channel's last-7-day daily pace times the days left in the month. |
| `pct_change_vs_previous` | number or null | Percent change of the projected month-end versus the previous month, one decimal; `null` when the previous month had no spend. |
| `by_channel` | array | Per-channel rows, only channels with non-zero activity. |

Each `by_channel` row carries `channel`, `month_to_date_minor`,
`last_7d_total_minor`, and `projected_month_end_minor` (the same
month-to-date plus last-7-day-pace formula, per channel). Channel names are
`sms`, `mms`, `whatsapp`, `voice`, `email`, `rcs`, `agents`, `verify`,
`numbers`, `marketplace`, and `other`; the rows always sum back to the
headline `projected_month_end_minor`.

```json 200 theme={null}
{
  "data": {
    "avg_daily_spend_minor": 4250,
    "current_balance_minor": 15000,
    "projected_days_remaining": 3.5,
    "lookback_days": 30,
    "first_spend_at": "2026-09-15T09:24:00.000Z",
    "month_to_date_minor": 61200,
    "last_7d_total_minor": 28700,
    "last_7d_avg_minor": 4100,
    "last_month_total_minor": 121000,
    "days_remaining_in_month": 16,
    "projected_month_end_minor": 126800,
    "pct_change_vs_previous": 4.8,
    "by_channel": [
      {
        "channel": "sms",
        "month_to_date_minor": 40200,
        "last_7d_total_minor": 18900,
        "projected_month_end_minor": 83400
      },
      {
        "channel": "voice",
        "month_to_date_minor": 21000,
        "last_7d_total_minor": 9800,
        "projected_month_end_minor": 43400
      }
    ]
  },
  "meta": {
    "request_id": "req_01J9ZK3M4N5P6Q7R8S9T0V1W2X",
    "timestamp": "2026-10-15T12:00:00.000Z"
  }
}
```

## 5. How it relates to spend caps and anomaly alerts

The three instruments answer different questions, and a mature billing
setup uses all three: burn rate to see the runway, anomaly alerts to hear
about a sudden change in pace, and spend caps as the hard stop that enforces
the limit you are comfortable with.

| Surface | Trigger type | What it answers | Acts on its own |
| - | - | - | - |
| [Spend caps](/billing/spend-caps) | Deterministic: a hard threshold you set | "Did we cross the line?" | Yes: pauses outbound sending at the cap. |
| [Usage & anomaly alerts](/billing/usage-anomaly-alerts) | Threshold or learned-baseline deviation, including spend velocity spikes | "Is today unusual?" | Notifies you in-app; never throttles. |
| Burn rate (this page) | None: a forecast you poll | "At this pace, when does the wallet run out?" | No: read-only, never notifies or throttles. |

The division of labor matters. A spend cap set at \$2,000/month says
nothing about whether the wallet survives to month end; a burn-rate poll
on day 20 saying "6 days of balance left" says nothing about whether the
pace is anomalous. An anomaly alert on a spend surge catches the compromise
or misconfiguration that a steady, legitimate ramp will not trip, while the
burn-rate projection catches the legitimate ramp that no baseline will
ever flag. Because burn rate never acts, nothing on this page can pause
your traffic: the forecast is advisory until you, or a cap you configured,
decides otherwise.

## 6. Example: chart runway in Grafana

The endpoint is built to be polled. A common pattern is an hourly cron that
publishes `projected_days_remaining` as a Prometheus gauge via the node
exporter textfile collector, then a Grafana panel and alert rule on top:

```bash theme={null}
#!/usr/bin/env bash
# /etc/cron.hourly/orbit-wallet-runway
body=$(curl -fsS \
  "https://api.orbit.devotel.io/api/v1/billing/burn-rate?days=30" \
  -H "X-API-Key: $ORBIT_API_KEY")

# null (no recent spend) becomes -1 so the panel can distinguish
# "no pace to project" from "zero days left".
days=$(printf '%s' "$body" | jq '.data.projected_days_remaining // -1')

tmp=/var/lib/node_exporter/textfile/orbit_wallet.prom.$$
echo "# HELP orbit_wallet_days_remaining Projected days of wallet balance at the current spend pace." > "$tmp"
echo "# TYPE orbit_wallet_days_remaining gauge" >> "$tmp"
echo "orbit_wallet_days_remaining $days" >> "$tmp"
mv "$tmp" /var/lib/node_exporter/textfile/orbit_wallet.prom
```

In Grafana, alert when the gauge stays below your replenishment lead time
(for example 7 days) for a few consecutive hours, so a single zero-spend
poll or a brief degraded snapshot cannot page anyone. The same poll can
feed `projected_month_end_minor` into a month-end forecast panel next to
your committed spend budget.

Remember the contract while wiring alerts: a zeroed response during a
billing-data blip is a 200, and `null` means "no recent spend", not
"exhausted". Alert on sustained values, and keep the real hard stop where
it belongs, in a [spend cap](/billing/spend-caps).

## Related

* [Billing overview](/billing/overview): the whole wallet surface, including
  the burn-rate summary section.
* [Spend caps](/billing/spend-caps): the deterministic hard stop.
* [Usage & anomaly alerts](/billing/usage-anomaly-alerts): threshold and
  learned-baseline notifications.
* [Wallet currency conversion](/billing/wallet-currency-conversion): how
  foreign-currency top-ups land in the wallet the forecast divides.


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