> ## 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.

# Schedule one-off sends across channels with scheduled_at

> Pass a future time on any send endpoint, and the message parks in the scheduled state until a platform scheduler dispatches it. Covers the accepted format, edit/cancel/inspect before dispatch, the WhatsApp 24h window probe, browser local time vs UTC, and sibling scheduling surfaces.

# Schedule one-off sends with `scheduled_at`

Every channel send endpoint accepts an optional `scheduled_at` field in the request body. Send it with a future time and the message waits until then; leave it out, or pass a time that has already passed, and the message dispatches immediately. This page covers everything end-to-end: what the field does, the accepted format, per-channel examples, how to inspect/edit/cancel before dispatch, and the edge cases that bite.

For the model behind scheduled sending — park, gate at fire time, fire, bill once — see [The scheduled-sending model](/concepts/scheduled-sending-model). For the request/response shapes of every endpoint named here, see the [Messaging API reference](/api-reference/endpoints/messaging).

## What `scheduled_at` does vs what the platform does

A `scheduled_at` value is a **park instruction**, not a dispatch timer you own:

1. You pass a future time. The send validates like an immediate send — recipient, template, sender, channel — and the API answers **`202 Accepted`**. That means accepted: the message is not sent yet.
2. The message persists in the **`scheduled`** state with its fire time stamped on the row. It survives restarts and stays editable and cancellable.
3. A continuously running platform scheduler promotes due rows `scheduled → queued` and dispatches them through the same claim path immediate sends use. The 202 tells you the platform owns the timing from here; your process does not need to stay alive.

Until the row promotes to `queued`, you hold a live handle on it: read it, rewrite it, or void it. Once it promotes, those handles return 409 — see [Inspect, edit, and cancel](#inspect-edit-and-cancel-before-dispatch) below.

## Use it per channel

SMS, WhatsApp, Email, Facebook Messenger, and Instagram DM all accept `scheduled_at` on their dedicated send endpoints. A few payload examples (endpoint base: `https://api.orbit.devotel.io/api/v1`):

### SMS

```bash theme={null}
curl -X POST https://api.orbit.devotel.io/api/v1/messages/sms \
  -H "X-API-Key: dv_live_sk_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "to": "+14155552671",
    "body": "Your appointment is tomorrow at 10 AM.",
    "scheduled_at": "2026-04-16T14:30:00Z"
  }'
```

### WhatsApp

```bash theme={null}
curl -X POST https://api.orbit.devotel.io/api/v1/messages/whatsapp \
  -H "X-API-Key: dv_live_sk_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "to": "+14155552671",
    "type": "template",
    "template": { "name": "appointment_reminder" },
    "scheduled_at": "2026-04-16T14:30:00Z"
  }'
```

### Email

```bash theme={null}
curl -X POST https://api.orbit.devotel.io/api/v1/messages/email \
  -H "X-API-Key: dv_live_sk_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "to": "customer@acme.io",
    "subject": "Invoice due tomorrow",
    "html": "<p>Your invoice is due tomorrow.</p>",
    "scheduled_at": "2026-04-16T14:30:00Z"
  }'
```

### Facebook Messenger

```bash theme={null}
curl -X POST https://api.orbit.devotel.io/api/v1/messages/messenger \
  -H "X-API-Key: dv_live_sk_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "to": "2457123456789012",
    "message": { "text": "Your order ships tomorrow." },
    "scheduled_at": "2026-04-16T14:30:00Z"
  }'
```

Meta's Messenger Platform addresses recipients by a page-scoped ID (PSID), so `to` carries a numeric PSID, not a phone number.

### Instagram DM

```bash theme={null}
curl -X POST https://api.orbit.devotel.io/api/v1/messages/instagram \
  -H "X-API-Key: dv_live_sk_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "to": "2457123456789012",
    "message": { "text": "Your order ships tomorrow." },
    "scheduled_at": "2026-04-16T14:30:00Z"
  }'
```

In every case the response is `202` with the message's `id` and `status: "scheduled"`. Keep the id; it is your handle for edit and cancel.

## Accepted formats and why strict ISO validation broke

`scheduled_at` accepts either a full ISO 8601 string with a timezone suffix or a partial ISO string without one:

* `"2026-04-16T14:30:00Z"` (or `+02:00`, or any explicit offset) — parsed as that exact instant.
* `"2026-04-16T14:30"` — a `datetime-local` shape with no offset. JavaScript constructs this as **local browser time**, so what it lands on depends on the caller's own server or browser.

Validation checks that the value parses to a real date; it does not require a timezone marker. Earlier validation required strict ISO-with-offset, which rejected the `datetime-local` values HTML pickers produce, so scheduling from a browser form rejected silently at the parse edge. The current rule accepts both forms — but an offset-less value is still ambiguous on the wire. Always prefer an explicit `Z` or offset when you send scheduling input from your own code. See [Browser local time vs UTC](#browser-local-time-vs-utc) below.

Two correctness invariants regardless of format: a scheduled time must be **in the future** at acceptance, and it may not be more than **35 days ahead**. A past instant acts like a missing field — the message sends immediately.

## Inspect, edit, and cancel before dispatch

All of these surface through the [Messaging API reference](/api-reference/endpoints/messaging). Until the platform scheduler promotes the row from `scheduled` to `queued`, the row is mutable:

* **List pending scheduled messages.** `GET /api/v1/messages/scheduled` returns every still-scheduled row, earliest fire time first, capped at 200 per call. Narrow with the `to`, `channel`, or `scheduled_before` query parameters. For larger exports, use the generic message list with `status=scheduled` and a cursor.
* **Read a specific message.** `GET /api/v1/messages/{id}` returns the current status and the stored fire time.
* **Edit body or fire time.** `PATCH /api/v1/messages/{id}` rewrites the message body and/or `scheduled_at` on a still-scheduled row. The scheduler simply finds it at the new time.
* **Cancel one.** `POST /api/v1/messages/{id}/cancel` transitions a scheduled row to `cancelled`, permanently. Scheduled rows never bill, so cancel before dispatch is free.
* **Bulk cancel.** `POST /api/v1/messages/cancel-scheduled` voids a set by explicit ids, or by a filter (`to`, `channel`, `scheduled_before`). Useful for a leaky scheduled fan-out.

Every one of those operations is gated on the row still being in `scheduled` when you touch it. If the scheduler already promoted the row, you'll get `409 MESSAGE_NOT_EDITABLE` or `409 MESSAGE_NOT_CANCELLABLE` — treat that as "this message left the scheduled state," not as a retryable error.

## WhatsApp: the 24h window probe runs at fire time

Meta's 24-hour customer-service window allows free-form (non-template) WhatsApp messages only within 24 hours of the recipient's last inbound message. For a scheduled send, the send path evaluates that window at **fire time**, using the recipient pair's latest inbound at the moment the scheduler dispatches — not the moment you accepted the request.

The consequence: scheduling a free-form WhatsApp message does not defer the window check — the check lands when the message actually dispatches. If the recipient's window will close before your fire time, the free-form send rejects then. Two ways to work with that:

* **Probe before scheduling.** `GET /api/v1/messages/whatsapp/window-status?to=<phone>` reports whether the recipient/sender pair is currently inside the window. Treat a closed window as "use a template." See the [24h window probe parity allowance](/channels/whatsapp) and the [Messaging API reference](/api-reference/endpoints/messaging) for the probe's exact shape.
* **Use an approved template** whose approval doesn't expire with the window. Templates pass at fire time regardless of the window.

The probe runs against the thread time — the latest inbound message timestamp on that recipient/sender pair — so it cannot be fooled by an unrelated inbound under a different contact linkage.

## Browser local time vs UTC

A browser scheduling form typically feeds an `<input type="datetime-local">` value — the `"2026-04-16T14:30"` shape with no offset. JavaScript reads that value as local browser time, which is fine if you schedule in the browser and send what you read verbatim away.

It goes wrong when any of the following happen:

* You store or pass an offset-less string and later reinterpret it as UTC. The same `14:30` means different instants depending on the client's timezone.
* You mix browser-collected values with server-computed values without fixing the offset on one side.

Pattern to follow: either **append the browser's offset** (`-07:00`, `+02:00`, …) or **convert to UTC** before the value goes on the wire. Never round-trip an offset-less scheduled time through a server as if it were UTC.

## Sibling scheduling surfaces

Scheduling one-off sends with `scheduled_at` is one of several timing surfaces. Use the right one for the shape of what you're scheduling:

* **Push notifications** have their own queue-side scheduling endpoint and management surface — `GET/DELETE /api/v1/push/scheduled` for pending notification rows. See [Push API reference](/api-reference/push).
* **Quiet hours** decide when *any* send is allowed to fire, independent of how its fire time was set. A scheduled send landing inside an org- or campaign-quiet-hours window re-parks to the next allowed instant (on the channels where re-park applies). See [Quiet hours: org-wide channel gates vs. the campaign fallback window](/guides/quiet-hours-configuration).
* **The scheduled-sending model** puts all of the above on one conceptual page: park → gate at fire → fire → bill once. See [The scheduled-sending model](/concepts/scheduled-sending-model).
