Skip to main content

The invoice, statement, and subscription model

Orbit surfaces three kinds of billing artifact, each with a precise meaning:
  • Invoice — a paid-style receivable document issued by the downstream billing provider (Orb for metered usage, Stripe for card checkouts). Orbit reads and renders it; it never issues it.
  • Statement — a calendar-month summary of your prepaid wallet, derived purely from the append-only wallet ledger. This is the document Orbit owns end to end.
  • Credit note / refund — reversals (wallet credit entry or provider refund) that adjust a prior document; in Orbit they ride the same ledger channels rather than being separate artifact types.
Billing-recurring subscriptions enter two different senses of the word — the tenant’s own Orbit plan subscription (held on Stripe), and a commerce “subscribe & save” recurring schedule an agent renews against a buyer’s payment mandate. This page models both, then the statement balance reconciliation, then the tax semantics.

Documents — invoice vs statement vs adjustment

Invoices. The invoices surface (/billing/invoices, and GET /api/v1/billing/invoices*) projects the billing provider’s read of your receivable documents. An invoice carries a lifecycle statusdraftissuedpaid | void — plus synced for provider-side finality. Orbit itself creates no subscription-style invoices; it lists whatever the provider holds against your tenant id (external_customer_id). When the provider read fails (or was never provisioned — a new tenant sees a legitimately empty state), the surface degrades to the durable recorded-invoice trail rather than a misleading empty list. Statements. A statement is a per-month (YYYY-MM) rollup computed from the wallet ledger. Every top-up is a positive ledger entry; every usage charge is a negative one; credits and clawbacks roll into an adjustment line. The statement is therefore derived, not issued — deleting the ledger rows is impossible, so a regenerated statement always agrees with the ledger. Adjustments instead of credit notes. Orbit does not mint separate credit-note documents. Anything that reverses value — a card refund against unused balance, a crypto payout, a residual credit — lands as a signed ledger entry of type topup (positive) or usage (negative), and the statement groups it under adjustment. Downstream providers (Orb / Stripe) can issue their own credit notes on their side of the integration; the Orbit statement reconciles them as ordinary signed lines.

Generation — provider invoices, plus a cost breakdown rollup

Two generation paths converge on the same surface.
  1. Provider invoice detail. A live fetch validates the provider’s envelope and required money fields (status, currency, totals) at the boundary before projecting onto the summary/detail shapes — a partial provider body fails closed rather than surfacing undefined financial figures. The download route redirects to the provider-hosted PDF or hosted invoice page, so the accounting system follows a 302 to the provider’s rendered document.
  2. Usage cost breakdown. Because provider line items carry no channel/destination attribution, each invoice detail is enriched with a cost_breakdown — the same channel × country aggregation the live usage-by-channel surface runs, scoped to the invoice’s billing period (line-item span first, then the subscription period). Sources: your tenant’s message ledger plus voice call log. It is a proportional showback of usage for the period and intentionally excludes plan fees, minimums, discounts, and tax — so its rows need not sum to the invoice total. The enrichment is fail-open: a missing period or a database fault returns the invoice without a breakdown, never a 500.
Provider-rendered PDFs stay provider-rendered; Orbit supplies the raw lines but does not re-render the document.

Recurring subscriptions — two shapes, one gate

The tenant’s plan subscription. The subscription-management service (dashboard self-service, plan checkout, pay-by-link mint, portal) treats the Stripe subscription as authoritative. Lifecycle actions are cancel (schedules cancel_at_period_end), reactivate, and change_plan; each mutation carries a deterministic idempotency key so a retried operation cannot re-apply the change. Cancel sets a projected *-cancel:* key; a price-swap uses *-change:*. The “Scheduled for cancellation” bit is read from the subscription itself, not from a mutable org flag — a webhook-only flip still renders correctly. Commerce “subscribe & save”. An agent can renew a recurring schedule (weekly / biweekly / monthly / quarterly) on behalf of a buyer — the commerce layer used by agentic checkouts. The schedule is a snapshot the caller round-trips: activepaused (freeze) → active (re-anchor, never fire a backlog) → canceled (terminal). Every renewal executes against the SAME payment mandate the buyer consented to at subscribe-time — a scoped, spend-capped, SHA-256–committed authorization. A mandate is active, and leaves active only to one of the terminal states exhausted, revoked, or expired — after which no further cycle is ever authorized. A renewal that the mandate declines (per-transaction cap, total cap, non-allowed merchant/category, expired/revoked) leaves the schedule untouched so the caller can retry after fixing the mandate. Mandate checking is the reason no outbound loop fires on a schedule the buyer revoked: the scheduler can mint the hosted pay link, but the charge authorization re-verifies the consent digest first. (The rendered re-order message rides RCS or WhatsApp outbound per the commerce layer — never voice/SMS per the platform’s outbound invariant.)

Statement balance arithmetic — reconciling the period

A statement row carries opening_balance, total_topups, total_usage, and closing_balance. By construction the signed line items (topup, usage, adjustment) sum to the delta:
The same line-item decomposition is what a reconciliation export preserves: given the month’s statement you can tie each row back to a ledger entry (reference-prefixed charge lines, or the top-up session id). The usage_by_channel rollup covers only negative debits — statement topup lines never include usage, so a normal paid provider invoice does not raise the wallet’s issued-invoice subset. When reconciling against the provider-side receivable (an invoice issued against the wallet, or a pay-as-you-go statement), use the revenue recognition schedule rather than comparing raw totals — the two views converge once both period closes are run to the same as-of date. Balance-owed on a customer account is not a live computed figure; it is the closing-balance delta over the invoice snapshot at period close.

Tax semantics — scoped, never estimated

Orbit never estimates tax, and this is deliberate.
  • Hosted checkouts (top-up / pay-by-link). Automatic tax and Adaptive Pricing are OFF by default. The buyer pays exactly the subtotal; the wallet credits exactly the subtotal. A “wrong total” is never silently inflated — if a tenant enables the optional provider tax flag, the buyer pays subtotal + tax, the wallet is still credited the subtotal, and the tax line is stamped onto the ledger entry so wallet↔ledger reconciliation stays exact.
  • Provider invoices. The per-invoice tax total comes straight from the provider’s document (tax_amount); Orbit never synthesizes it. A nil tax field means the provider assessed none — not that Orbit estimated zero.
  • Reseller/metered surfaces. Where a tax line is modeled (usage- metering rollups a platform resells to its customers), it is a scoped optional parameter with an optional exemption certificate — only an approved certificate matching the jurisdiction suppresses it. Even there, the amount is computed from explicit inputs, never estimated from address or geography.
The rule of thumb: an Orbit surface either carries the provider-assessed tax, an explicitly scoped tax parameter, or no tax line at all. It never fills the gap with an estimate.