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 gate — SENDING_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.

Zero balance per send surface

The chain above is the direct API send, but it is not the only surface that originates a paid send. A zero balance behaves differently on each origination surface, and the refusal details differ with it:
  1. Direct API send — rejects with INSUFFICIENT_BALANCE (402). The response body carries your current balance as a balance_minor field, so you can render an “Add funds” prompt with the exact shortfall. Treat the 402 as terminal: do not hot-retry — top up first, then resend.
  2. Campaign launch — the launch pre-flights the projected cost against your wallet before the first message goes out. A genuinely zero (or below-cost) wallet rejects with INSUFFICIENT_CREDITS (402). The balance read itself is deliberately fail-open: if the read errors (a cache or connectivity blip), the launch skips the gate and proceeds instead of refusing a funded tenant on a false zero — per-recipient billing still enforces real charges during the send. This is not the balance endpoint’s degraded: true flag, which only marks a returned display value as last-known; the launch gate’s fail-open decides nothing from that display value.
  3. Inbound auto-reply — a keyword/auto-reply rule fires its reply through the canonical billed send path, so at zero balance the reply fails soft: the message records as failed with an insufficient-balance cause, and the inbound pipeline keeps going. Your inbound (MO) message is never thrown away, and inbound routing is never paused — only the outbound reply is skipped.
  4. What zero balance does NOT do — running dry never flips your organization’s pause flag on its own (the balance monitor’s soft pause is the dunning/monitoring flow described on dunning lifecycle, and the flag taxonomy lives on the pause gate and free-channel exemption); it never rejects inbound MO traffic; and it never freezes scheduled jobs that have not yet drawn funds — those settle against the wallet when they run.

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.