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 astatus(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’sexternal_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 ormcc/mncplus 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 sameowner, 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: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:
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):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>:
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: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 ledgerpublic.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 ormcc/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:
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.
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 everypreferredChannelsinpreferredChannels ∪ reachableChannels. Decide per-contact whether to skip or escalate; the mint is not attempted. - Pay-by-link 422 on
amountMinorfloor / 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
limitmonths (default 24, max 60). For a tenant with zero wallet history, each row is still present with zeroed values. IfGET /billing/statements/:periodreturns a404-shaped detail, pass the month asYYYY-MMexactly (e.g.2026-8is rejected;2026-08is 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_ofdate 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.
Related
- Billing overview — wallet, statements, and committed-use contracts mapping.
- Revenue recognition — the per-period schedule semantics for matching an invoice to a ledger.
- What-if pricing simulator — the deeper guide on candidate-lane replay.
- Volume tiers and revenue recognition — the combined monthly close workflow.
- Wallets, credits, and charges — how a prepaid debit is attributed to a channel.
- Pricing and rate resolution — how the calculator resolves the base / tier / per-operator price.