Skip to main content

The outbound billing gate chain

When a paid send comes back with HTTP 402, the cause lives in one of four stages — and today those stages are described on three separate pages. The wallet, rating, and ledger page covers the balance pre-flight and the authorize-before-use model, the pause gate and free-channel exemption page covers the flags gate, and wallets, credits, and charges covers the atomic check-and-decrement. To reconcile a 402 you assemble the order from all three. This page narrates the whole chain once. The stages run in the same order for every channel, and the order is the point:
  1. Pre-flight balance check — at $0 the reject is INSUFFICIENT_BALANCE (402).
  2. Pause/block flags gateSENDING_PAUSED (soft) or SENDING_BLOCKED (hard), 402, with the free-channel exemption.
  3. Per-channel fund path — the voice hold of up to 2 minutes at dial; per-segment SMS debit; per-token AI debit.
  4. Atomic check-and-decrement — the wallet never overdraws, and idempotent ledger rows collapse retries.
A send that clears the first gate proceeds only to the next — the refusal code names how far the chain got.

Stage 1 — pre-flight balance check

Every paid channel reads the wallet before it sends. An exhausted wallet rejects with INSUFFICIENT_BALANCE (402). An account-level hold still wins by construction — a blocked tenant is refused even with money in the wallet, because the next gate checks those flags too.

Stage 2 — pause/block flags gate

Right after the money check, the send hits two flags on your organization: outbound_paused (soft) and outbound_blocked (hard). Balance-driven causes — wallet_empty from the balance monitor, or billing_alert:<id>:<name> from a threshold you set — flip the soft flag; account-lifecycle causes (suspension, dunning, dispute, admin hold, any unknown null cause) flip the hard flag. Both reject with 402. The balance-driven class has one deliberate exception: the $0-on-platform free channels (WhatsApp, Instagram, Messenger, Telegram, LINE, Apple Business Messages, push, web widget) are bypassed, because the send costs the platform nothing and you pay the upstream provider directly. The account-lifecycle class exempts no channel, and the gate never guesses an unknown cause into “probably just the balance.” The reason taxonomy is spelled out on the pause gate and free-channel exemption.

Stage 3 — per-channel fund path

A send that clears both gates pays in its own unit:
  • Voice — one authorized hold. At dial, Orbit debits two minutes’ worth up front (minimum 2¢). An empty wallet means no hold, and no hold means the call never dials. At call end, Orbit debits only the delta above the hold (minimum 1¢ per call); an unanswered call refunds the hold in full. The FX rate locked at hold time is reused at settlement, so the two computations never disagree on currency. This is the only hold/reservation in the billing model — every other channel pre-flights without escrowing funds.
  • SMS and messaging — per segment. The debit posts when the message is sent, one segment’s worth per billable segment, so a long message debits more than one.
  • AI agents — per token. Each model call debits its token usage as it runs.

Stage 4 — atomic check-and-decrement

The wallet check and the decrement happen inside one server-side operation, so concurrent debits can’t race each other into an overdraft. Every movement carries an idempotency key naming the real-world event it pays for (message id, call id, checkout session id). The key caches the result for 24 hours, drives the ledger row’s deterministic identifier, and collapses retries to one row — concurrent attempts with the same key either collapse or get a 409 DEDUCT_IN_FLIGHT to back off with. Insufficient-balance outcomes are never cached, so top-up plus a same-key retry works cleanly.

Worked examples — one SMS, one call, one AI conversation

Three sends walk the chain, each labelled at the gate that consumed it:
  • SMS send — clears the balance pre-flight, clears the flags gate, debits one ledger row per billable segment at send time (type: debit, reference: charge:sms_mccmnc:msg_…). A retry replays the same key and moves no money twice.
  • Voice call — clears the same two gates, then escrows up to two minutes before the provider leg is dialed. At hangup the settle computes the true charge and debits only the delta; an unanswered call releases the whole hold. The FX rate locked at hold time is the rate the settle reuses.
  • AI-agent conversation — clears the same two gates, then each model call debits its tokens as it runs, per token. A refusal at the wallet mid-conversation surfaces at stage 1 of the next model call, not as a post-hoc debt.

Boundary — what is NOT in this chain

The chain decides whether funds move, and in what order the refusals fire. It deliberately excludes:
  • Rate resolution — how a destination’s per-unit price is attributed (pricing tables, tier lookup). Lives on pricing and rate resolution.
  • Usage-metering rollups and committed-use meters — rollup engines and enterprise monthly-commit drawdown. Lives on committed-use drawdown.
  • What-if previews — stateless rate simulations over your usage lanes; nothing persists, nothing gates. Lives on what-if pricing preview.

Integration rules

  • Treat 402 as terminal. INSUFFICIENT_BALANCE, SENDING_PAUSED, and SENDING_BLOCKED are all 402 — do not hot-retry; resolve the cause.
  • Poll GET /api/v1/billing/balance. Read outbound_paused / outbound_blocked and outbound_block_reason until the flag flips false, then resume.
  • A degraded balance read never gates yours. The endpoint can return a last-known value flagged degraded: true; send-time checks run server-side regardless, so an exhausted wallet can never send on a stale display value.
  • Voice FX is locked at hold, reused at settle. The same rate computation runs twice with no currency drift.