Scheduled messages: Twilio parity recipes
Orbit’s scheduled-message queue mirrors the two Twilio shapes a migrating team usually ports first —Messages.list?Status=scheduled to read what is still pending, and Message.update on a scheduled message to change body or fire time before dispatch. The Scheduled console guide walks the same surface in the dashboard; this page gives the API recipes.
All examples assume Authorization: Bearer dv_live_sk_xxxx and JSON bodies. The status gate is uniform: every mutation is conditional on the row still being in scheduled; once the scheduler promotes it, calls return 409 MESSAGE_NOT_EDITABLE or 409 MESSAGE_NOT_CANCELLABLE.
1. List the pending queue — Twilio Messages.list?Status=scheduled parity
For larger sweeps, use the main messages list with
status=scheduled and cursor pagination — the same rows, without the 200-row window.
Twilio-side mapping: this replaces the Messages.list?Status=scheduled read. The response rows carry the same lifecycle vocabulary (status: "scheduled", scheduled_at, to, channel, body), so a port that consumed the Twilio list shape typically needs only auth and field-name handling changed.
2. Edit body or fire time — Twilio Message.update parity
- Send
body,scheduled_at, or both. Both are re-validated with the create-path rules: body length, a future fire time, and the 35-day scheduling horizon. - Channel, recipient, sender identity, and template are not editable on purpose. Changing them is a new send — cancel and create.
- Unchanged fields are a client-side no-op in the dashboard dialog; raw API calls that change nothing still succeed and leave the row untouched.
Message.update against a scheduled message. Same verb, same gate, same refusal modes when the row already left the scheduled state.
3. Cancel one row
cancelled, permanently. Scheduled rows never bill, so a cancelled scheduled send is free end-to-end. A 409 means the row already dispatched — treat it as “this message left the scheduled state,” not a retryable transport error.
4. Bulk-cancel — the sweep recipe
Two modes, one endpoint:POST /api/v1/messages/cancel-scheduled.
Explicit ids (what the dashboard selection sends):
idsaccepts up to 1000 rows.filterrequires at least one ofto,channel,scheduled_before— a bare filter is a 422, not an empty success.- The update is conditional on
status='scheduled', so rows the scheduler picked up between your read and your write are left alone and simply do not appear in the response’scancelledlist. - The response names the exact outcome:
cancelled_countand the per-rowcancelled_ids. Reconcile a race by diffing your collected ids against that list — the delta is precisely what dispatched meanwhile.
Idempotency and retries
SendIdempotency-Key on sweeps your tooling may retry. The conditional update already makes a blind retry safe (cancelled rows no longer match the gate), and the key collapses retry-into-response-replay into one semantics. This is a deliberate extension over the Twilio shape: the loop-and-dedupe scaffolding a Twilio client builds around Message.update is unnecessary here — see idempotency and safe retries for the platform-wide rule.
5. The audit posture
Every mutation above lands in your tenant’s audit log.- Single edit / single cancel → one audit entry per row, stamped with the acting user and surface.
- Bulk cancel → one summary entry per call (mode, filter shape or id count, cancelled count), with
cancelled_idsin the API response for per-row joins. - Refusals are recorded refusals, not silent failures: a 409 on an already-dispatched row is the audit-visible evidence the queue is honest.
Related
- The Scheduled console — the dashboard surface over these endpoints.
- Schedule one-off sends with
scheduled_at— the create-side field that parks a row into this queue. - The scheduled-sending model — park → gate at fire → fire → bill once.
- Migrate from Twilio to Orbit — the full concept-mapping guide.
- Messaging API reference — the exact request/response schemas.