Skip to main content

Outbound send gating

Every outbound send on Orbit — API, flow, campaign, drip, journey, or a 1:1 send from the dashboard — is admitted through the same chain of gates before it is handed to a provider. Each gate is a control you configured (or chose to leave off): your wallet posture, your quiet-hours window, your frequency caps, your duplicate-content policy, your throughput ceilings. When a gate says no, the send stops there and reports the reason through a documented error code or a skipped status. This page is the map the individual guides assume: which gates exist, the order a send walks them, and what to branch on in your integration — so “when will my send actually leave” is one page, not four.

The gate layers — each one tenant-owned

Four families of controls shape whether a send leaves, and all of them are your configuration, not platform mandates. Compliance posture belongs to the tenant: Orbit carries the settings and evaluates them at send time. The only gate the platform owns and cannot be disabled is the TCPA federal voice dialing-window guard for US recipients (no tenant toggle, fail-closed on an unresolvable recipient timezone) — the quiet-hours guide covers that carve-out in its voice section. They stack independently. A contact who never opted out can still be caught by a frequency cap; a send inside your quiet-hours window still respects your duplicate-content policy.

Admission order — the chain a send actually walks

A single send is evaluated in the order below. The first gate that refuses ends the attempt — later gates are never reached, so a duplicate never burns a frequency-cap slot and an opted-out recipient never reaches either check.
  1. Outbox admission. The API validates the request, resolves the sender, and opens an outbox/inbox-visibility ledger entry for the send attempt.
  2. Pre-approval: quiet hours + duplicate-content claim. The quiet-hours evaluator runs against recipient-local time; a blocked send reports next_allowed_at. The message-suppression policy claims a content hash for <channel, recipient, body> inside your window — a second identical body in that window drops here.
  3. Outbound-flags fast path (wallet/billing). The org’s outbound_paused / outbound_blocked gate is consulted next. A paused org is refused with 402 SENDING_PAUSED; free channels (WhatsApp, Telegram, …) bypass a balance-driven pause but stay subject to account-lifecycle holds.
  4. Per-recipient block lists. Opt-out and suppression-list membership is checked; a blocked recipient is refused with 422 RECIPIENT_OPTED_OUT (direct sends) or skipped (campaign sends).
  5. Frequency-cap slot claim. consumeSlot test-and-increments atomically; over the cap, direct sends get 429 FREQUENCY_CAP_EXCEEDED, campaign sends skip with reason frequency_capped.
  6. Throughput caps. The per-messaging-service MPS counter, then the per-number MPS counter (independent of each other), each return 429 with Retry-After: 1 when the current second’s budget is exhausted.
  7. Provider dispatch. The send reaches the carrier/provider; the row advances into the delivery lifecycle. From here the outcome is the network’s, not a gate’s.
The resilience posture differs per gate — worth knowing when you model worst cases: the quiet-hours evaluator fails closed for marketing traffic (a 503 INTERNAL_COMPLIANCE_ERROR you can retry explicitly) and open for transactional, while billing-flag, frequency-cap, suppression, and MPS checks fail open so an infra blip never silently black-holes your outbound.

Where each gate fires

Gates attach at different points in the send pipeline:
  • At send admission, before a segment is counted or any quota slot is held: quiet hours, the wallet/billing flags, block lists, duplicate suppression, frequency caps. A refusal here means no ledger debit and no provider call.
  • At the pre-dispatch claim stage: the duplicate-content hash and the frequency-cap slot are claimed, not just checked — concurrent senders can’t both pass by racing a read.
  • At throughput admission: the per-service and per-number MPS counters run at the messaging-service / number resolution step, so a burst throttles where the ceiling is defined.
  • At dispatch time, not request time: a scheduled_at send bypasses quiet hours at request time and is re-evaluated when the scheduler fires — a send accepted today is not grandfathered into a window that closed by dispatch time. Gates always evaluate now, against your current settings.

Suppression and skipping vs. pausing — two different “no”s

The gates resolve to two superficially similar but semantically different outcomes, and your integration should branch them distinctly: A suppressed or skipped send reached a gate that says “this send, this recipient, this body — no.” The pipeline returns a success-shaped result with status: "skipped" and a reason (duplicate_content, opted_out, frequency_capped, erasure_pending), so a campaign batch keeps moving through the rest of its recipient list. Depending on the channel and gate, a suppressed row can also persist as an operational sentinel — a settled, non-delivery status operators can see in the timeline and that your reporting must exclude from delivery-rate math. Skipped/suppressed sends refund any quota slot the pipeline pre-claimed — you never lose an allowance for a message that was never handed to a provider. A paused send never got a body-level answer at all. The wallet/billing gate refuses the entire send with 402 SENDING_PAUSED (or SENDING_BLOCKED); no message row is created, and nothing is queued. There is no skipped status to poll — the send simply does not exist until you resolve the pause and resubmit. Read the pause state and its reason (outbound_paused, outbound_block_reason, payment_failure_count) off GET /billing/balance; treat 402 as terminal for that send rather than retrying in a hot loop. In short: suppression is per-message and usually recoverable for the rest of a batch; a pause is org-wide and recoverable only by resuming the org.

What to branch on

Model it once, truth it everywhere

If you need one sentence to take away: a send leaves only after every gate you enabled says yes, in admission order — and each rejection is surfaced as a specific signal you can branch on. You own every posture in this chain; turning a gate off is your call, and the defaults above are the starting point.

See also