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

# Billing ops: invoices, statements, pay-by-link, and the price calculator

> The finance-ops workflow for the four sibling Billing surfaces — fetch invoices and their PDFs, export monthly statements, mint cross-channel pay-by-link checkout URLs, and preview unit prices before go-live.

# Billing ops: invoices, statements, pay-by-link, and the price calculator

Four dashboards under **Billing** carry the finance-ops workflow — **Invoices**
(`/billing/invoices`), **Statements** (`/billing/statements`), **Pay by link**
(`/billing/pay-by-link`), and the **Pricing calculator** (`/billing/calculator`).
Two of them cover the two ways a billing artifact appears: **invoices** are
paid receipts of subscription-style usage, and **statements** are periodic
summaries of the prepaid wallet. **Pay by link** mints a one-time checkout URL
to top up a balance without needing a dashboard seat, and the **calculator**
runs a rate simulator over any channel before the traffic goes live.

This guide walks through the read/export/mint/list lifecycle across all four
surfaces with the exact curl shapes your API clients send — gated to the same
`owner`, `admin`, or `billing` role as the dashboard.

***

## 1. Where billing artifacts live

* **Invoices** (`/billing/invoices`) — the paid receipt for usage Orbit
  issued as a subscription-style invoice. Each invoice lists its own line
  items and carries a `status` (`draft` / `issued` / `paid` / `void` /
  `synced`), an optional link to the hosted Orbit invoice page, and an
  optional PDF download URL. Orbit issues no subscription invoices itself;
  this surface reads the downstream billing-provider invoices the account
  has (for example Orb in the tenant's `external_customer_id`-keyed
  provider account).
* **Statements** (`/billing/statements`) — the periodic summary of the
  prepaid wallet, aggregated from the append-only ledger every top-up and
  charge writes into. Each statement covers one calendar month (`YYYY-MM`) and
  carries opening balance, total top-ups, per-channel usage, closing balance,
  and the reconciling line items (topup / usage / adjustment) that sum to the
  closing-balance delta.
* **Pay by link** (`/billing/pay-by-link`) — mints a hosted one-time
  checkout URL for a customer payments request, then renders the request-to-
  pay message across SMS / RCS / WhatsApp / email / voice. The URL settles
  back into the wallet asynchronously when the payment clears. Use this to
  let a finance user top up against an invoice or open a collection to a
  customer without a dashboard seat.
* **Pricing calculator** (`/billing/calculator`) — the rate simulator. Given
  a country code or `mcc`/`mnc` plus a channel, it returns the unit-price
  predictions before you ship traffic through those lanes. This page also
  runs the dashboard's four dedicated preview calculators — AIaaS pure-usage,
  marketplace rev-share, resolution usage-meter, and margin rollup — on
  caller-supplied counters without moving money.

### Role gate

Every endpoint and page in this guide requires the same `owner`, `admin`, or
`billing` role — read surfaces (`/preview`, statements) use a billing-read
limit; the money-mint (`/pay-by-link`) uses the money rate limit. Permission
failures arrive as `403` with the org's role system.

***

## 2. Invoice lifecycle — list, fetch, and hand off a dispute

List the org's invoices with the same query params the dashboard sends:

```bash theme={null}
curl "https://api.orbit.devotel.io/api/v1/billing/invoices?status=issued&limit=25" \
  -H "X-API-Key: dv_live_sk_your_key_here"
```

Valid `status` values are `draft`, `issued`, `paid`, `void`, and `synced`.
Take the cursor the reply carries (`next_cursor`) to page further. For
pay-as-you-go tenants with no provider-side invoices, the list returns `[]`
and the dashboard shows "No invoices yet" — that is a legitimate state, not
an error.

Fetch one invoice's line items by id:

```bash theme={null}
curl "https://api.orbit.devotel.io/api/v1/billing/invoices/inv_abc123" \
  -H "X-API-Key: dv_live_sk_your_key_here"
```

Get the invoice's hosted PDF / hosted page URL to hand to a finance system:

```bash theme={null}
curl -L "https://api.orbit.devotel.io/api/v1/billing/invoices/inv_abc123/download" \
  -H "X-API-Key: dv_live_sk_your_key_here"
```

The download route is a redirect — either to the invoice's `pdf_url` (a PDF
attachment) or the hosted invoice URL — so it arrives as `302` plus a
`Location` header; the accounting export follows the redirect.

Disputes are tenant-owned controls: when an invoice's totals disagree with
your internal reconciliation, escalate to the billing provider's disputes
queue. The invoices surface is read-only — no mutation endpoint marks one
invoice disputed in Orbit, so the customer manages that flow in the
downstream provider. Treat the in-progress, per-period invoice
(`GET /api/v1/billing/invoices/upcoming`) as the ledger preview, and read
`GET /api/v1/billing/invoices/:id` for the exact disputed amount.

***

## 3. Statement export — period filter and CSV/JSON

List the monthly statements histories (up to 60 months, default 24):

```bash theme={null}
curl "https://api.orbit.devotel.io/api/v1/billing/statements?limit=12" \
  -H "X-API-Key: dv_live_sk_your_key_here"
```

The list carries `period`, inclusive `period_start`, exclusive `period_end`,
`currency`, `opening_balance`, `total_topups`, `total_usage`, and
`closing_balance` per row — the fields a period-end match against your own
ledger uses.

Export one month as CSV (default), or JSON for machine reading. The response
is a `Content-Disposition: attachment` download with the filename
`statement-<YYYY-MM>.<csv|json>`:

```bash theme={null}
curl -o statement-2026-08.csv \
  "https://api.orbit.devotel.io/api/v1/billing/statements/2026-08/download?format=csv" \
  -H "X-API-Key: dv_live_sk_your_key_here"
```

```bash theme={null}
curl -o statement-2026-08.json \
  "https://api.orbit.devotel.io/api/v1/billing/statements/2026-08/download?format=json" \
  -H "X-API-Key: dv_live_sk_your_key_here"
```

The detail endpoint `GET /billing/statements/:period` returns the same
`StatementSummary` plus `total_usage_by_channel` (per-channel transaction
count and positive-amount sums) and the signed `line_items` — `topup`,
`usage`, `adjustment` — that by construction sum to the closing-balance
delta. Use `period` in `YYYY-MM` form; the endpoint validates the month
regex.

***

## 4. Pay by link — mint a one-time checkout URL

Mint a hosted checkout URL plus the rendered channel message for a finance
user to send:

```bash theme={null}
curl -X POST "https://api.orbit.devotel.io/api/v1/billing/pay-by-link" \
  -H "X-API-Key: dv_live_sk_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "amountMinor": 5000,
    "currency": "USD",
    "description": "Balance top-up for August",
    "preferredChannels": ["email", "sms", "whatsapp"],
    "reachableChannels": ["email", "sms"],
    "successUrl": "https://orbit.devotel.io/billing/pay-by-link?status=success",
    "cancelUrl":  "https://orbit.devotel.io/billing/pay-by-link?status=cancel",
    "contactId": "contact_fin_ops",
    "merchantName": "Acme Corp",
    "locale": "en-US"
  }'
```

The mint is money-moving — the request body must carry `amountMinor`
(integer minor units, at least the provider's per-currency minimum of 50),
a 3-letter ISO `currency`, a bounded `description`, one or more
`preferredChannels` / `reachableChannels` in priority order, and both
`successUrl` / `cancelUrl` pointing at a trusted origin (Orbit does not mint
a checkout URL pointing elsewhere). The reply returns the minted
`payByLink_id`, the hosted `checkout_url`, the chosen `channel`, and the
rendered request-to-pay `message` — forward those into your sending
pipeline (Orbit does not send anything itself; the message renderer only
builds the body).

The preview route
`POST /api/v1/billing/pay-by-link/preview` composes channel selection and
rendering for an already-known URL without minting — the dashboard preview
uses this to sanity-check the body a contact will read before any session is
opened.

Settlement comes back asynchronously — the Stripe / provider webhook
handler reconciles the completed session keyed on the `pay_by_link_id`
stamped in its metadata, and the wallet balance rises when the capture
settles. The Pay by link object rides a `pending` → `paid` / `failed` /
`expired` lifecycle.

***

## 5. How wallet debits hit invoices and statements

Both artifacts draw from the same prepaid ledger `public.credit_transactions`
— top-ups are positive `topup` entries, every usage charge is a negative
`usage` entry against a named channel, and residual credits / clawbacks roll
into an `adjustment` line. By construction the line items sum to
`closing_balance − opening_balance` — that is the reconciliation invariant
every statement-based export preserves. See
[Wallets, credits, and charges](/concepts/billing-and-wallet) for how a
wallet debit is attributed to a channel, and
[Billing overview](/billing/overview) for where the live balance is read
from.

When the wallet is the funding source of a subscription-style invoice (for
example Orb), the invoice's `issued` state reflects one of the two intake
paths — subscription lines issued against the wallet, or a pay-as-you-go
statement that closes its own period. In either case the statement's
`topup` lines reflect only positive wallet increases and the `usage_by_channel`
rollup covers only negative debits — a normal paid subscription does not
raise the invoice page's `status=issued` subset.

***

## 6. Calculator — simulate a rate before go-live

The pricing calculator simulates before shipping a lane: given a country or
`mcc`/`mnc` plus channel, it returns unit-price predictions; given past
usage and a candidate rate card, it returns the delta between that lane's
cost and your live spend. Preview routes are read-only and require the
billing role:

```bash theme={null}
curl -X POST "https://api.orbit.devotel.io/api/v1/billing/whatif-pricing/preview" \
  -H "X-API-Key: dv_live_sk_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "window_days": 30,
    "label": "SMS august candidate",
    "candidate_card": [
      { "channel": "sms", "country_code": "US", "direction": "mt", "rate_per_unit": 0.0080 }
    ]
  }'
```

For a live per-operator rate, per channel+country or `mcc`/`mnc`, read the
pricing-resolution from the calculator page — see
[Pricing and rate resolution](/concepts/pricing-rate-resolution) for how
base pricing, tier override, and channel coupled rates resolve. The
dashboard's calculator page also runs four read-only preview calculators —
AIaaS pure-usage, marketplace rev-share, resolution usage-meter, and margin
rollup — over caller-provided counters; each mirrors its own production
calculator's header.

***

## 7. Troubleshouting — failed pay-by-link, missing statement period, stale invoice total

* **Pay-by-link fails with `NO_REACHABLE_CHANNEL` (422).** The chosen
  channel intersection is empty — the contact has opted out of every
  `preferredChannels` in `preferredChannels ∪ reachableChannels`. Decide
  per-contact whether to skip or escalate; the mint is not attempted.
* **Pay-by-link 422 on `amountMinor` floor / ceiling.** Below the provider
  min charge (50 minor units) or above the per-link ceiling
  (99,999,999 minor units). Keep within the bounds.
* **Statement export is empty / the period-filter row is missing.** The
  list endpoint returns up to `limit` months (default 24, max 60). For a
  tenant with zero wallet history, each row is still present with zeroed
  values. If `GET /billing/statements/:period` returns a `404`-shaped
  detail, pass the month as `YYYY-MM` exactly (e.g. `2026-8` is rejected;
  `2026-08` is correct) — the validator month regex is strict.
* **Invoice total differs from the statement same period.** Invoices read
  the provider's per-subscription view; statements read the prepaid ledger;
  the two will only converge once both closes are run to the same `as_of`
  date in the revenue-recognition surface. Use the
  [Revenue recognition](/billing/revenue-recognition) page for the schedule
  rather than comparing raw totals.
* **Invoice list shows "No invoices yet" when you know Orb issued one.**
  When the provider's live read fails (5xx / timeout), the surface degrades
  to the durable audit trail of issued invoices; a genuinely empty trail
  combined with a live-read error re-surfaces the provider error rather
  than a mislabeled empty state.

***

## Related

* [Billing overview](/billing/overview) — wallet, statements, and
  committed-use contracts mapping.
* [Revenue recognition](/billing/revenue-recognition) — the per-period
  schedule semantics for matching an invoice to a ledger.
* [What-if pricing simulator](/guides/whatif-pricing-simulator) — the deeper
  guide on candidate-lane replay.
* [Volume tiers and revenue recognition](/guides/billing-revenue-and-volume-tiers)
  — the combined monthly close workflow.
* [Wallets, credits, and charges](/concepts/billing-and-wallet) — how a
  prepaid debit is attributed to a channel.
* [Pricing and rate resolution](/concepts/pricing-rate-resolution) — how the
  calculator resolves the base / tier / per-operator price.
