The outbound pause gate and the free-channel exemption
Every send — a 1:1 API message, a flow step, a campaign blast, a dialer call, or a number purchase — pre-flights two flags on your organization:outboundPaused (soft pause) and outboundBlocked (hard block). To poll them,
call GET /api/v1/billing/balance and read the outbound_paused and
outbound_block_reason fields on the response. When either flag is set, the
send rejects with 402 SENDING_PAUSED or 402 SENDING_BLOCKED.
One question decides what happens next: is the hold trying to protect the
wallet, or to hold the account? Balance-driven pauses exempt the $0-price
channels; account-lifecycle holds exempt no channel. A null reason fails safe
and is treated as a hold.
How a pause is set and cleared
Two pause controls are tenant-owned — you can set them up, trip them, and clear them yourself:- Auto-pause on an empty wallet. A platform balance monitor flips
outboundPausedwhen your balance reaches zero (reasonwallet_empty). It clears itself: top up under Dashboard → Billing → Overview, and issued credits resume outbound immediately instead of waiting out the balance-sync cycle. - A threshold you configured. Under Dashboard → Billing → Alerts you can
create a spend or balance threshold whose action is
pause_outbound(or the harderblock_outbound). When the threshold trips, outbound pauses and the reason carries the alert’s id and display name (billing_alert:<alert_id>:<name>).
PATCH /api/v1/billing/alerts/:id). Platform-owned holds — dunning,
subscription suspension, an open payment dispute, an admin or fraud review —
are not tenant-liftable: they clear when you update the payment method or the
review closes.
A paused send, end to end
While the pause is active, the balance endpoint names it:402 SENDING_PAUSED — the same code
you’d see for wallet_empty — and the error message states the actual cause
plus the resume step (reset your billing alert, top up, update your payment
method). That rejected send is held at admission, not queued: treat 402 as
terminal and retry after the poll below flips.
Recovery is one poll: top up or reset the offending alert, then re-read
GET /api/v1/billing/balance until outbound_paused is false.
Free-channel exemption in detail
The BYO-credential messaging channels are **$0 on the platform**: WhatsApp, Instagram, Messenger, Telegram, LINE, Apple Business Messages, Push, and the web widget (`web_chat`). The reason is structural: Devotel is a Meta/channel tech-provider for these channels, not a BSP or aggregator. You bring your own provider credentials, you pay the upstream provider directly, and the Devotel wallet is never debited — the $0 platform price falls out of the default rate card, not out of a promo. The eligibility predicate is two conditions, both:isChannelFreeOnPlatform(channel) — the channel is priced at $0 on the
platform rate card — AND isBalanceDrivenOutboundPause(reason) — the reason is
exactly wallet_empty or starts with billing_alert:. When both hold, the
send rides through the pause. There is nothing to elect into: the exemption
is automatic, matching the wallet pre-flight that already skips a balance check
on free channels. If your connected-channel setup uses only the channels above,
a wallet_empty or billing-alert pause never blocks your sends.
Every other channel is paid and always gated: SMS, MMS, RCS, email, voice, fax,
and Viber are wallet-debited and reject on any pause.
Two classes of holds
Balance-driven — “your wallet can’t pay for this send”
wallet_empty— auto-pause-on-zero, flipped by the platform balance monitor.billing_alert:<alert_id>:<name>— a threshold you set under Dashboard → Billing → Alerts, as above.
Account-lifecycle — “the account may not send at all”
subscription_suspended/parent_subscription_suspended— the account (or, under a reseller, the parent account) is suspended.dunning_failed_3x/dunning— repeated failed payments.payment_dispute_active— a card dispute or chargeback is open.null/ unknown reason — an admin or fraud-console suspend writes the hold without a reason label. Absent a recognizable label, the gate fails safe and keeps blocking every channel.
Audit and observability — what the gate reports
The gate decision is never silent, on either side of the line:- Held send — a rejected send returns the 402 error code and lands in the message-detail failure timeline with the reason-aware cause (top up / reset the alert / update your payment method), and the dashboard shows the outbound-paused banner and a per-send toast with the same copy.
- Exempt send — a free channel that rides through a balance pause writes an
info-level log entry tagged with the tenant, the channel, and the pause
reason, and increments the
billing.outbound_flag_free_channel_bypasscounter withtenant_schemaandchanneltags. - Clear — a wallet credit that clears a
wallet_emptypause emits thebilling.wallet_credit_outbound_resumedlog + metric, and the pause caches clear so the banner drops and sending resumes on the next read.
wallet_empty window is exactly the WhatsApp/Meta traffic the exemption is
designed to keep alive. Alert configurations and their state changes — like
every billing-settings mutation — land in the organization’s audit trail.
Scope, and what’s exempted from what
The pause flags live on the organization, so one pause covers every launch path at once — per-channel (a 1:1 send), per-sender (any sender registered to the org), per-flow (automation steps that send), and per-campaign (blasts). Launch-path equivalents all read the same flags: messaging, MT voice, and paid number purchase share the one pause vocabulary and the same exemption test.
There is one deliberate boundary: the reseller per-subaccount spend cap
(
subaccount_spend_cap:…) is nothing to do with this gate. It is a separate
posture, re-checked authoritatively against the same send, and free channels do
not bypass it (see the posture map below).
Edge cases
- Inbound is unaffected. A pause gates MT dispatch only. Replies and new inbound messages land in the inbox, and inbound webhooks keep firing — your customers can still reach you while outbound is held.
- Campaigns and flows mid-flight. The gate sits at admission per send, not at schedule time, so a campaign or flow that trips a billing-alert pause stalls per item as each one comes up for dispatch — it does not hang the remainder silently. New items 402 until the hold clears; items already handed to the provider complete normally and their delivery receipts still arrive. Re-launch or retry once the poll shows the pause cleared.
- Clock and read skew at the transition. Two different writers collaborate
on the flag: sends read a cached copy of the pause flags (staleness bounded
to seconds), and the platform balance monitor flips
wallet_emptyon its own sync tick. In the transition window a send can land on either side of a just-set or just-cleared hold depending on which read it got — both directions self-correct, wallet credits bust the cached flag immediately on the resume path, and a rejected 402 is always safe to retry once the poll shows the hold lifted. TreatGET /api/v1/billing/balanceas the operator source of truth when the banner and a send disagree.
Five-minute tenant checklist
- Know your hold surface.
curl GET /api/v1/billing/balanceand readoutbound_paused+outbound_block_reason— this is the same poll your integration should branch on after a 402. - Set one guard before you need it. Create a billing alert (Dashboard →
Billing → Alerts, or
POST /api/v1/billing/alerts) with a threshold and thepause_outboundaction so spend trips pause before the wallet empties. - Name your notification path. Put a monitored address in the alert’s
notify_emailsso the trip reaches you; a pause you don’t notice is an outage you didn’t plan. - Exploit the exemption deliberately. If you run WhatsApp (or another $0 BYO channel) beside paid channels, a balance pause takes only the paid traffic down — your Meta-channel sends keep flowing. Plan campaign mixes with that split in mind.
- Rehearse the resume. Top up or reset the tripped alert, poll
GET /api/v1/billing/balanceuntiloutbound_pausedflipsfalse, then resend. Lifecycle holds (dunning, suspension, dispute) don’t lift from the wallet — fix the payment method or contact support.