Skip to main content

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:
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:
Get the invoice’s hosted PDF / hosted page URL to hand to a finance system:
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):
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>:
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_itemstopup, usage, adjustment — that by construction sum to the closing-balance delta. Use period in YYYY-MM form; the endpoint validates the month regex.
Mint a hosted checkout URL plus the rendered channel message for a finance user to send:
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 pendingpaid / 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 for how a wallet debit is attributed to a channel, and 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:
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 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.
  • 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 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.