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

# Troubleshooting: a scheduled message that never fired

> A scheduled message whose send_at time has passed yet the row still shows scheduled — or never sends at all — splits two ways: a gate that refuses to clear at fire time, or a fire time the wall clock has genuinely not reached. Work the decision checklist on this page before you re-send anything.

# Troubleshooting: a scheduled message that never fired

You scheduled a send with `send_at` (or `scheduled_at` on the per-channel
endpoints), the clock has now passed that time, and the row still reads
`scheduled` — or no webhook, no Delivery Log entry, no delivery ever
materialised. The [scheduled-send lifecycle](/concepts/send-at-scheduled-sends)
explains the mechanism that should have fired the row; this page is the
triage for when it did not.

The fault splits two ways: either the **wall clock has not really reached
fire time** for the gate that would promote the row, or a **gate held the
row at fire time and refused to clear**. The cause table below maps each
split to its check and fix; the decision checklist then tells you when to
escalate.

## Symptoms

* The dashboard Delivery Log shows the row in `scheduled` with a `send_at`
  time visibly in the past, and no `message.sent` or terminal webhook ever
  arrives.
* `GET /api/v1/messages/:id` returns `status: "scheduled"` long after the
  fire time passed.
* For a scheduled push send: `GET /api/v1/push/scheduled/:id` still shows
  the row as scheduled past its fire time, and no push webhook fired.

## Cause table — wall-clock vs held gate

| Branch | What holds the row | The check | The fix |
| - | - | - | - |
| **Wall clock never reached** | You compared against the wrong clock — the row carries a future `send_at`, or your local clock is ahead of the stored UTC instant | Re-read `GET /api/v1/messages/:id` and compare the stored `send_at` (UTC) against an accurate clock, not the timestamp on your watch | Expected behaviour — the drain fires at the stored instant; do not re-send a duplicate |
| **Gate held at fire time — quiet hours** | A marketing send whose fire time fell inside the recipient-local quiet-hours window re-parks to the next window open | Check your organization's quiet-hours window and the recipient's local time against the original fire time | Expected behaviour — the row re-fires at window open; widen the window if the hold is wrong |
| **Gate held at fire time — compliance profile** | The sender or number a regulated destination requires a verified compliance profile for stays held at the pre-send gate | Verify the compliance profile link for the sender (`pending_compliance` holds it open) | Attach the missing compliance profile or complete its verification, then resend or wait for the drain retry |
| **Gate held at fire time — sender resolve fail** | The named pool or sender does not resolve when the drain reaches the row | Check the sender pool membership and the sender's registration state on **Settings → Channels** | Add members to the pool, register the sender, or re-point the send at a sender that resolves |
| **Cancelled before fire** | A `POST /api/v1/messages/:id/cancel` (or `POST /api/v1/messages/cancel-scheduled`) took the row to `cancelled` before the drain reached it | Read the row's current status; a `409 MESSAGE_NOT_CANCELLABLE` on a later cancel means it already left `scheduled` | Terminal — the row was deliberately cancelled; re-create the send if you need it |

All of the held-gate branches are tenant-owned controls — the row holds
because a control you configured has not cleared, not because the platform
lost the row. Only after every one of these checks passes do you treat the
row as a platform defect.

## From an error code to this page

One scheduled-send error has a dedicated code worth naming:

| Code | HTTP | Surface | What it tells you |
| - | - | - | - |
| `SCHEDULED_PUSH_NOT_CANCELLABLE` | 409 | `DELETE /api/v1/push/scheduled/:id` | The push row already left `scheduled` — the drain promoted it (or it expired), so the cancel is refused. Reconcile against the row's current status; the failure is not the non-fire you are diagnosing |

Every other scheduled message surface routes its edit/cancel race through
the generic `409 MESSAGE_NOT_CANCELLABLE` — a cancel or `PATCH` that
returned 409 means the row left `scheduled`, which narrows the diagnosis
away from a never-fired row and toward the post-dispatch lane. When the
code does not map here, fall back to the
[scheduled-send lifecycle concept](/concepts/send-at-scheduled-sends)
timeline: a `scheduled` → `queued` → `sending` → `sent` ladder, where a
`cancelled` or `expired` terminal closes the row.

## Decision checklist

1. **Re-request the timeline.** Read `GET /api/v1/messages/:id` (or the
   scheduled-push read endpoint) and compare the stored `send_at` UTC
   instant against an accurate clock. A future fire time means the page
   does not apply yet — wait for it.
2. **Verify a tenant gate has moved.** Check the held-gate rows in order:
   quiet hours, compliance profile link, sender pool resolution. Confirm
   the gate you fixed actually cleared — a row the gate still holds past
   fire time is normal.
3. **Verify a cancellation did not move the row.** A
   `cancel`/`cancel-scheduled` call that returned 409 means the drain
   already won the race; a successful cancel is terminal and you should
   not expect a fire.
4. **Only then escalate.** If the row sits at `scheduled` more than a few
   minutes past its `send_at`, no gate in the table holds, no cancel raced
   it, and it still never promoted, that is a drain-side defect — open a
   ticket.

## Full-error sample to paste in a ticket

A held row past fire time looks like this on the wire:

```json theme={null}
{
  "id": "msg_01HZZC8E3W4T2QZ0R2N3Y4ABCD",
  "status": "scheduled",
  "to": "+14155550134",
  "from": "+14155559876",
  "send_at": "2026-09-30T14:00:00Z",
  "created_at": "2026-09-30T12:00:00Z"
}
```

Paste the message id alongside the row, the `send_at`, and the clock on
your side, so support sees the hold precisely.

## Fixes not to try

* **Do not bulk re-send the same `send_at` request without diagnosing
  the gate.** If a quiet-hours or compliance gate is holding the original
  row at fire time, the duplicate inherits the same hold and both rows
  park; the moment the gate clears you deliver twice. Cancel the original
  (`POST /api/v1/messages/:id/cancel`) before you recreate a send you no
  longer trust, and only after the gate check comes back clean.
* **Do not re-fire a cancel-scheduled batch to unstick a row.** A batch
  cancel (by ids or `(to, scheduled_before, channel)`) that 409s on one
  member means that member already promoted; re-issuing the batch does
  not un-cancel the rest.
* **Do not treat a missing webhook as evidence the row never fired.** The
  drain promotes the row synchronously with the webhook fan-out; a
  subscriber that dropped the delivery does not stop the dispatch. Read
  the row — do not drive your diagnosis off the webhook alone.

## What to send support

When the checklist ends at escalation, bundle:

* The message id and the full row (`GET /api/v1/messages/:id` output) with
  the stored `send_at` in UTC.
* The channel, sender, and (if a pool was routed) the pool id.
* The gate you already ruled out — quiet-hours window, compliance profile
  verification, sender pool membership — and the timestamp of the clock
  you compared against.
* The response code and id of any cancel or edit attempt
  (`meta.request_id`) so support can trace the row's last transition.

## See also

* [The scheduled-send lifecycle](/concepts/send-at-scheduled-sends) — the
  park, drain, and dispatch mechanics this page assumes
* [Message parked before sending](/troubleshooting/message-queued) — the
  pending state table for the other pre-send holds, including `scheduled`
* [Message stuck in queued](/reference/troubleshooting) — the owner of the
  `queued` lane, for a row that promoted but never dispatched
* [Send gating and quiet hours](/concepts/send-gating-and-quiet-hours) —
  the full dispatch-time gate chain the drain walks
* [Sender resolution and pool errors](/troubleshooting/sender-resolution-errors) —
  the sender-resolve branch's sibling codes
* [Error Code Reference](/reference/error-codes) — the fallthrough for any
  code this page does not name
