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. For the request/response shapes of every endpoint named here, see the Messaging API reference.
What scheduled_at does vs what the platform does
A scheduled_at value is a park instruction, not a dispatch timer you own:
- 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. - The message persists in the
scheduledstate with its fire time stamped on the row. It survives restarts and stays editable and cancellable. - A continuously running platform scheduler promotes due rows
scheduled → queuedand 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.
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 below.
Use it per channel
SMS, WhatsApp, Email, Facebook Messenger, and Instagram DM all acceptscheduled_at on their dedicated send endpoints. A few payload examples (endpoint base: https://api.orbit.devotel.io/api/v1):
SMS
Facebook Messenger
to carries a numeric PSID, not a phone number.
Instagram DM
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"— adatetime-localshape with no offset. JavaScript constructs this as local browser time, so what it lands on depends on the caller’s own server or browser.
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 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. Until the platform scheduler promotes the row fromscheduled to queued, the row is mutable:
- List pending scheduled messages.
GET /api/v1/messages/scheduledreturns every still-scheduled row, earliest fire time first, capped at 200 per call. Narrow with theto,channel, orscheduled_beforequery parameters. For larger exports, use the generic message list withstatus=scheduledand 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/orscheduled_aton a still-scheduled row. The scheduler simply finds it at the new time. - Cancel one.
POST /api/v1/messages/{id}/canceltransitions a scheduled row tocancelled, permanently. Scheduled rows never bill, so cancel before dispatch is free. - Bulk cancel.
POST /api/v1/messages/cancel-scheduledvoids a set by explicit ids, or by a filter (to,channel,scheduled_before). Useful for a leaky scheduled fan-out.
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 and the Messaging API reference 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.
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:30means different instants depending on the client’s timezone. - You mix browser-collected values with server-computed values without fixing the offset on one side.
-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 withscheduled_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/scheduledfor pending notification rows. See Push API reference. - 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.
- 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.