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

# Scheduled messages: Twilio parity recipes

> Drop-in recipes for teams porting Twilio scheduled-message flows to Orbit: list the pending queue like Messages.list?Status=scheduled, edit body or schedule like Message.update on a scheduled message, and bulk-cancel by ids or filter. Includes the 409 race, idempotency, and the audit-log shape.

# 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](/guides/messages-scheduled-console) 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

```bash theme={null}
curl "https://orbit.devotel.io/api/v1/messages/scheduled?limit=200&channel=sms" \
  -H "Authorization: Bearer dv_live_sk_xxxx"
```

Query filters narrow the queue before it returns:

| Parameter | Effect |
| - | - |
| `to` | recipient address (E.164 for SMS/WhatsApp, email address for the email channel) |
| `channel` | one channel (`sms`, `whatsapp`, `email`, …); omit for the cross-channel queue |
| `scheduled_before` | ISO timestamp — only rows firing before this instant |
| `limit` | page size, capped at 200 (same cap the dashboard page uses) |

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

```bash theme={null}
curl -X PATCH "https://orbit.devotel.io/api/v1/messages/msg_01HXYZ…" \
  -H "Authorization: Bearer dv_live_sk_xxxx" \
  -H "Content-Type: application/json" \
  -d '{
    "body": "Reminder moved: your appointment is now at 16:00.",
    "scheduled_at": "2026-10-04T16:00:00Z"
  }'
```

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

Twilio-side mapping: this replaces `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

```bash theme={null}
curl -X POST "https://orbit.devotel.io/api/v1/messages/msg_01HXYZ…/cancel" \
  -H "Authorization: Bearer dv_live_sk_xxxx"
```

Moves the row to `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):**

```bash theme={null}
curl -X POST "https://orbit.devotel.io/api/v1/messages/cancel-scheduled" \
  -H "Authorization: Bearer dv_live_sk_xxxx" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: pause-afternoon-batch-2026-10-03" \
  -d '{
    "mode": "ids",
    "ids": ["msg_01HXYZ…", "msg_01HXYY…", "msg_01HXZZ…"]
  }'
```

**Filter mode (tooling-level sweep):**

```bash theme={null}
curl -X POST "https://orbit.devotel.io/api/v1/messages/cancel-scheduled" \
  -H "Authorization: Bearer dv_live_sk_xxxx" \
  -H "Content-Type: application/json" \
  -d '{
    "mode": "filter",
    "channel": "sms",
    "scheduled_before": "2026-10-03T23:59:00Z"
  }'
```

* `ids` accepts up to 1000 rows. `filter` requires at least one of `to`, `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's `cancelled` list.
* The response names the exact outcome: `cancelled_count` and the per-row `cancelled_ids`. Reconcile a race by diffing your collected ids against that list — the delta is precisely what dispatched meanwhile.

### Idempotency and retries

Send `Idempotency-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](/concepts/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_ids` in 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.

Tenant-owned means the audit table in *your* schema is the system of record — a TCPA / DLT / GDPR review can show who ran the sweep, with which filter, and which exact ids were voided, without touching platform-internal logs.

## Related

* [The Scheduled console](/guides/messages-scheduled-console) — the dashboard surface over these endpoints.
* [Schedule one-off sends with `scheduled_at`](/guides/message-scheduling) — the create-side field that parks a row into this queue.
* [The scheduled-sending model](/concepts/scheduled-sending-model) — park → gate at fire → fire → bill once.
* [Migrate from Twilio to Orbit](/guides/migration-from-twilio) — the full concept-mapping guide.
* [Messaging API reference](/api-reference/endpoints/messaging) — the exact request/response schemas.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.