> ## Documentation Index
> Fetch the complete documentation index at: https://docs.orbit.devotel.io/llms.txt
> Use this file to discover all available pages before exploring further.

# The scheduled-sending model: how queue-park, quiet-hours gating, and fire-time form one contract

> The one model behind every time-managed outbound send — a future send time parks in the `scheduled` state, dispatch-time gates re-evaluate it at fire time, quiet-hours re-parks rather than quietly sends, and billing and rate limits only ever see the fired send. Read this before any of the four pages that each cover one slice of the contract.

# The scheduled-sending model

Four pages in these docs each cover one slice of time-managed outbound sending, and none of them is the whole model:

* [The scheduled-send lifecycle](/concepts/send-at-scheduled-sends) — how a message with a future send time parks in the `scheduled` state and how the drain finds it at fire time.
* [Outbound send gating](/concepts/send-gating-and-quiet-hours) — the full gate chain every send walks, with quiet hours as one gate.
* [Delivery lifecycle](/concepts/delivery-lifecycle) — what happens after a send dispatches, with `scheduled` treated as one status among many.
* [Message status transition rules](/concepts/message-status-dag) — the legal `scheduled → queued | cancelled | expired` transitions and their webhook flavor.

This page is the missing anchor: one mental model that says *schedule → park → gate → fire → bill*, so a question like "how do I make sends fire at local business hours and never breach quiet hours" has a single answer, not four partial ones.

## Two message classes that need time management

Scheduling exists to solve two different problems, and they pull on different knobs in the same model:

**Update emission.** A send whose value comes from firing at a precise instant — appointment reminders, expiry-date renewal prompts, "your parcel ships tonight." Here the fire time is the product feature, and quiet-hours handling means "firing inside a closed window must be handled on purpose, not at random." Update emission users typically reject the defer-to-window-open behavior partially (the send must fire *then*, or it is worthless) — they park with a fast fire-time like "tomorrow at 14:00 recipient-local." If that instant falls within quiet hours, they either want the send cancelled (so it never goes stale) or explicitly shifted — your own code decides which by pre-computing the recipient-local instant before calling.

**Daily-part schedules.** A campaign or drip that must land "on a weekday, between 09:00 and 17:00, recipient-local" — marketing traffic, reminders where "sometime tomorrow" is acceptable, re-engagement flows. Here quiet hours is a *target*, not an obstruction: the schedule should land inside the window, and if the requested instant misses it, the platform shifts the fire time to next window-open rather than refuse. [Send-window optimization](/concepts/send-at-scheduled-sends#quiet-hours-and-timezones-at-fire-time) is built for exactly this shape: your requested time is treated as the earliest acceptable; a time outside the window is moved to window-open at acceptance, not fire time.

Both classes flow through the same state machine below — the difference is only *when* the defer happens (at acceptance for send-window, at dispatch for quiet-hours marketing SMS/MMS) and *who decides* deferral is acceptable.

## One contract, four moments

Every time-managed send moves through the same four moments, in this order. This is the unified contract the scattered pages document:

1. **Schedule.** The send endpoint validates the request (recipient, template, sender, send time ≤ 35 days ahead), stamps a fire time, and returns `202` — validation already happened at acceptance, so the row the platform parks is structurally clean.
2. **Park.** The row sits in `scheduled`, its fire time on a dedicated, drain-indexed `scheduled_at` column — not an in-memory timer. It remains editable and cancellable until the drain promotes it.
3. **Gate at fire.** The gate chain — wallet posture, recipient block lists, duplicate-content policy, frequency caps, throughput caps — runs *at dispatch*, again. Quiet hours (marketing channels) and, for US voice, the federal TCPA dialing window re-evaluate too. What would accept at request time may still fail at fire time; gates never grandfather.
4. **Fire or resolve.** On a clean gate walk, the row is promoted `scheduled → queued` and joins the normal [delivery lifecycle](/concepts/delivery-lifecycle). On a quiet-hours gate refusal (resp.: marketing SMS/MMS to US recipients, marketing email with an org window), the row is *re-stamped* to the next open instant and stays `scheduled`. On a terminal fail — operator cancel or expiry window lapse — it resolves to `cancelled` or `expired`. And then the billing feed and rate limiters see exactly one send: the fired one.

That is the whole model. Everything else is per-scope mechanics on top of it.

## Quiet-hours re-parks rather than quietly sends

The property every customer actually wants — *never breach quiet hours, and never silently drop the send* — is enforced by re-stamping, not refusal, on the channels and classes where that makes sense:

* **Marketing SMS/MMS to US recipients, TCPA quiet-hours window (21:00–08:00 recipient-local).** When the drain prods a parked SMS whose fire time falls in the window, it stamps a new fire time at the next 08:00 recipient-local (DST-aware), and the row stays parked. The send is never quietly dropped.
* **Marketing email with an org-configured quiet-hours window.** Same shape — re-parked to next window-open. A degenerate (fully closed) window fails open so a drip never re-parks forever.
* **Non-marketing email and SMS.** These channels either lack time-based gates or default to no gate — they dispatch when the drain prods them.

Where re-park is NOT the behavior: **voice** (the one TCPA rule that is always on, hence not yours). An outbound voice call to a US recipient must land inside the federal 8:00–21:00 recipient-local window; outside it, the send is refused with `422 TCPA_FEDERAL_DIALING_WINDOW_BLOCKED`, not re-parked. The voice gate lives in [TCPA federal voice guard](/concepts/tcpa-federal-voice-guard); the send pipeline re-checks it at dispatch for any deferred send, so a campaign voice call scheduled in the evening can't slip through.

The DST-aware behavior matters for both: the platform resolves recipient-local time by `recipientTimezone` first, the contact's stored timezone second, then your org default — see [Send-gating concept — timezone resolution](/concepts/send-gating-and-quiet-hours#where-each-gate-fires) and the [quiet-hours guide timezone section](/guides/quiet-hours-configuration). The result of this re-park policy is a flagship invariant: a scheduled send resolves to the earliest allowed instant *inside the window*, never to a fired-inside-closed-window accident and never to a silent cancel.

## The bill and the rate limiter see only the fired send

The billing model is deliberately simple here: a `scheduled` row never bills, a live dispatch-induced wallet check still applies, and the usage-record feed contains only fired sends. Concretely:

* **A parked row is not billable.** The [usage-records model](/concepts/usage-records-model) writes one record per *sent* message. A row that sits in `scheduled` for 23 hours accrues zero wallet debit while it is parked — charges land only when the drain fires the row and the provider accepts it. If you never fire (operator cancel, expiry window lapse), there is no billed event and no usage record to reconcile.
* **Rate limits apply where the drain prods.** API request rate limits (`POST /messages/<channel>`) fire at your acceptance call — they cap how fast you park, not how fast the drain fires. But the per-service and per-number throughput (MPS) caps, plus any frequency-cap slot, are claimed by the drain at dispatch-time — see [Rate-limit taxonomy family 4](/concepts/rate-limit-and-cooldown-taxonomy) for the per-family error codes and retry posture.
* **Campaign sends.** Campaign drips dispatch through the same drain mechanism as envelope sends. When a burst of scheduled sends fire at window-open, throughput caps (429 + `Retry-After: 1`, billed limiter family) throttle the drain's promoted rows at provider admission — not before parking, so your parked volume is never the blast radius.

The practical consequence for reconciliation: *the number that bills and the number that throttles is the fired rate, not the parked volume.* A scheduled volume of 100k parked rows represents zero consumption until window-open.

## What a fire-time failure looks like at each gate

Gates don't refuse the same way at fire time as at acceptance time. The state it lands in tells you which gate stopped it — the [send-gating page's full branch table](/concepts/send-gating-and-quiet-hours#what-to-branch-on) covers every code; the ones reachable after fire time are the meaningful ones here:

| Gate                                                        | At dispatch it resolves to                                                                                                                                                                     | Webhook event flavor                                                                                                                                                         |
| ----------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Quiet hours (marketing channels) / federal voice window** | Re-parked to next window-open — stays `scheduled` with a new fire time.                                                                                                                        | No event; poll `GET /messages/:id` to observe the new `scheduled_at`. (Voice out-of-window: refused with `422 TCPA_FEDERAL_DIALING_WINDOW_BLOCKED`, status stays pre-drain.) |
| **Wallet/billing pause** (`outbound_paused`)                | Refused whole-send with `402 SENDING_PAUSED` — row reaches terminal `failed` with `metadata.classified_error_code` set, one of the only fire-time failures your subscribers do see.            | `message.failed`                                                                                                                                                             |
| **Recipient opt-out / suppression list**                    | Terminal `rejected` or `failed` with `422 RECIPIENT_OPTED_OUT`-category code; on campaign sends the recipient is skipped (`status: "skipped"`, `reason: "opted_out"`) and the batch continues. | `message.failed`                                                                                                                                                             |
| **Duplicate-content policy (message suppression)**          | Skipped (`status: "skipped"`, `reason: "duplicate_content"`) and refunded — never a provider call.                                                                                             | No or `message.failed` (depending on channel).                                                                                                                               |
| **Frequency-cap over-claim**                                | Skipped (`status: "skipped"`, `reason: "frequency_capped"`) on campaigns, refused `429 FREQUENCY_CAP_EXCEEDED` on direct sends.                                                                | `message.failed` (direct) or no event (campaign-skipped).                                                                                                                    |
| **Throughput (per-service / per-number MPS)**               | `429` + `Retry-After: 1` — drain retries the promotion on its next pass; the row stays `scheduled` until the MPS budget clears.                                                                | No event.                                                                                                                                                                    |

Three failure modes only you can trigger: operator `POST /messages/:id/cancel` moves the row to terminal `cancelled` and fires **no webhook** (by design — cancel originates from your own API call), and a validity-window lapse moves it to terminal `expired` with `message.failed`. The state-anchor for all of these is [message status transition rules](/concepts/message-status-dag#precedence-when-updates-compete) — the DAG names `scheduled → queued | cancelled | expired` as the only three legal exits.

## Worked example: a quiet-hour send and a pre-fire cancel

Set the org quiet-hours window 21:00–08:00, make a marketing SMS send at 21:30 recipient-local, and watch the four moments play out.

**1. Acceptance (21:30).** Your `POST /messages/sms` with `scheduled_at: "2026-09-01T02:00:00+02:00"` (a recipient in Europe/Berlin) validates as a clean send: the recipient is on no block list, the wallet is fine. Returns `202` and the row parks in `status: "scheduled"` with `scheduled_at` stamped in UTC. Gate gates *bypass at request time* — acceptance is never the verdict.

**2. Parked (21:30 onward).** Subscribers see `message.created` with `status: "scheduled"` (same `message.created` event as an immediate `queued` send — the status field is the branch point). The row is editable (`PATCH /messages/:id`) and cancellable (`POST /messages/:id/cancel`) until the drain prods it down. If the recipient opts in for a send-window (your own daily-part window), that fire time was already shifted to window-open at acceptance; if not, it is honored as-is.

**3. Fire time (02:00 recipient-local).** The drain finds the row — indexed lookup over `scheduled_at ≤ now`, oldest first, row-locked so an overlapping replica skips it. At dispatch promotion the gate chain re-walks: recipient block lists, wallet, duplicate-suppression if enabled, frequency-cap slots, MPS caps. Marketing *SMS/MMS to a US recipient* inside 21:00–08:00 recipient-local would *not* dispatch here — instead the drain re-stamps `scheduled_at` to next 08:00 recipient-local and keeps the row parked. But this recipient is in Europe/Berlin and quiet hours do not apply to non-marketing sends or to sends your org never enabled a window for — so the row is promoted `scheduled → queued`.

**4. After fire.** From `queued` the row joins the normal delivery pipeline: `sending → sent → delivered`. Subscribers see a second `message.created` (`status: "queued"`) for the promotion, then `message.sent`, then the terminal event. If instead you had cancelled it at 02:15 (a pre-fire cancel), the row resolves to `cancelled`, no webhook fires, and the drain never fires it; if you let its 35-day validity window lapse, it resolves to `expired` with a `message.failed` event. If at dispatch the wallet paused, the opt-out list caught the recipient, or duplicate-content suppressed the body — the table above names which terminal status and webhook flavor each gate produces.

The point the worked example carries: every acceptance you made — the request, the fire-time, the cancel, the gate walk — is observable through exactly the statuses above, and only one send ever bills.

## See also

* [The scheduled-send lifecycle](/concepts/send-at-scheduled-sends) — drain mechanics, index shape, and edit/cancel semantics in depth
* [Outbound send gating](/concepts/send-gating-and-quiet-hours) — the full gate chain with default postures and branch-on signals
* [Delivery lifecycle](/concepts/delivery-lifecycle) — what happens after the drain promotes `scheduled → queued`
* [Message status transition rules](/concepts/message-status-dag) — allowed `scheduled` transitions and late-event merge rules
* [TCPA federal voice guard](/concepts/tcpa-federal-voice-guard) — why US voice refuses instead of re-parking
* [Quiet hours configuration](/guides/quiet-hours-configuration) — knob surface and timezone resolution
* [Usage records model](/concepts/usage-records-model) — the per-record billing feed for fired sends
* [Rate-limit and cooldown taxonomy](/concepts/rate-limit-and-cooldown-taxonomy) — which limiter fires when, and its error code
* [Message status lifecycle reference](/api-reference/messages-status-lifecycle) — per-status semantics and webhook-event map
