> ## Documentation Index
> Fetch the complete documentation index at: https://docs.lithoblocks.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Schedule a message

> Send a template later: schedule it through the API, then reschedule, cancel or send it now.

A scheduled message is a template send that happens later. You give the API a template, the data
for its placeholders, one destination and a time; the evaluator sends it when the time comes.
Creating and changing scheduled messages needs the `scheduled:write` scope, reading needs
`scheduled:read`. Both are available to members and up.

Credits are spent when the send happens, not when you schedule it. A scheduled send costs 2 credits.

## Schedule one

```bash theme={null}
curl -X POST https://api.lithoblocks.com/v1/scheduled-messages \
  -H "Authorization: Bearer $LITHOBLOCKS_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: reminder-2026-09-14-standup" \
  -d '{
    "template_id": "6b1e…",
    "data": { "date": "Monday 14 Sep", "updates": [{ "who": "Pat", "doing": "Billing migration" }] },
    "destination": { "channel_id": "C0AS92CQ04D" },
    "schedule": { "send_at": "2026-09-14T09:00:00+02:00", "timezone": "Europe/Brussels" }
  }'
```

| Field                 | Required | Meaning                                                                                                                           |
| --------------------- | -------- | --------------------------------------------------------------------------------------------------------------------------------- |
| `template_id`         | yes      | A template in your organization. The send renders the template as it is at send time.                                             |
| `data`                | yes      | Values for the template's placeholders, the same object a direct send takes.                                                      |
| `destination`         | yes      | Exactly one of `channel_id`, `recipient_email` or `recipient_slack_id`.                                                           |
| `schedule.send_at`    | yes      | The earliest send time, ISO 8601 with an offset (`2026-09-14T09:00:00+02:00` or `…T07:00:00Z`).                                   |
| `schedule.timezone`   | no       | An IANA zone such as `Europe/Brussels`. Used by [time-window conditions](/guides/delivery-conditions) that do not name their own. |
| `recurrence`          | no       | Repeat the message; see [Recurring messages](/guides/recurring-messages).                                                         |
| `delivery_conditions` | no       | Hold the message until conditions allow; see [Delivery conditions](/guides/delivery-conditions).                                  |

The response is `201` with the scheduled message:

```json theme={null}
{
  "id": "0f3c…",
  "status": "scheduled",
  "template_id": "6b1e…",
  "template_version_id": "9a2d…",
  "queue_policy_id": null,
  "destination": { "channel_id": "C0AS92CQ04D" },
  "data": { "date": "Monday 14 Sep", "updates": [{ "who": "Pat", "doing": "Billing migration" }] },
  "schedule": { "send_at": "2026-09-14T07:00:00.000Z", "timezone": "Europe/Brussels" },
  "recurrence": null,
  "repeat_count": 0,
  "delivery_conditions": null,
  "max_delay_at": null,
  "source": "api",
  "error_message": null,
  "created_at": "2026-09-06T10:12:44.318Z",
  "processed_at": null
}
```

`send_at` comes back normalised to UTC. `template_version_id` records the version that was current
when you scheduled. `queue_policy_id` is only set on messages created through the older
policy-driven path and is `null` for anything you create here.

### Retrying safely

Send an `Idempotency-Key` header with any unique string. Repeating the request with the same key
and body within 24 hours replays the original response, marked `Idempotency-Replayed: true`,
instead of scheduling a second message. The same key with a different body answers `409`.

### What can go wrong

| Status | `code`         | Why                                                                                                                 |
| ------ | -------------- | ------------------------------------------------------------------------------------------------------------------- |
| `404`  |                | No template with that id in your organization.                                                                      |
| `422`  |                | The body does not validate: a missing field, two destinations, a `send_at` without an offset, a presence condition. |
| `400`  | `invalid_data` | The database refused the row; `details` carries the reason.                                                         |
| `403`  | `tier_limit`   | The organization's tier does not include scheduling; the message names the tier.                                    |

## Lifecycle

| Status       | Meaning                                                                                                         |
| ------------ | --------------------------------------------------------------------------------------------------------------- |
| `scheduled`  | Waiting for `send_at`, or for its delivery conditions. The only status you can change.                          |
| `processing` | The evaluator has picked it up and is sending.                                                                  |
| `sent`       | Delivered; `processed_at` is when.                                                                              |
| `failed`     | The send did not happen; `error_message` says why, for example `insufficient_credits` or `no_slack_connection`. |
| `cancelled`  | You cancelled it before it was sent.                                                                            |
| `expired`    | Its conditions were still unmet at `max_delay_at` and `on_miss` was `expire`.                                   |

The last four are terminal. The evaluator runs every minute, so a message is sent within a minute
of becoming due.

## Manage it

| Do                 | Call                                                                                                          |
| ------------------ | ------------------------------------------------------------------------------------------------------------- |
| List               | `GET /v1/scheduled-messages`, soonest first; `?status=` and `?template_id=` filter, `limit` and `offset` page |
| Read one           | `GET /v1/scheduled-messages/{id}`                                                                             |
| Reschedule or edit | `PATCH /v1/scheduled-messages/{id}`                                                                           |
| Cancel             | `DELETE /v1/scheduled-messages/{id}`                                                                          |
| Send now           | `POST /v1/scheduled-messages/{id}/send-now`                                                                   |

All three changes are only possible while the status is `scheduled`. Afterwards the API answers
`409` with `code: "invalid_state"` and the current status in `details`.

### Reschedule

`PATCH` takes any of `data`, `destination`, `schedule`, `recurrence` and `delivery_conditions`;
send only what changes. `recurrence: null` removes the recurrence and `delivery_conditions: null`
removes the conditions. The deadline `max_delay_at` is recomputed from the new `send_at` and
conditions.

```bash theme={null}
curl -X PATCH https://api.lithoblocks.com/v1/scheduled-messages/{id} \
  -H "Authorization: Bearer $LITHOBLOCKS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "schedule": { "send_at": "2026-09-15T09:00:00+02:00" } }'
```

### Cancel

`DELETE` is a soft cancel: the row stays, with `status: "cancelled"`, and the response is the
message. Cancelling a recurring message stops the later occurrences too.

### Send now

`send-now` makes the message due immediately and removes its delivery conditions; the evaluator
sends it on its next tick, within a minute. The response is `202` with the message, still
`scheduled` until that tick.

## See what happened

Every attempt the evaluator made appears in `GET /v1/queue-executions`, filterable by
`scheduled_message_id`. A `scheduled_send` with `result: "success"` is the delivery; a
`constraint_check` with `result: "deferred"` is a tick where the conditions held the message back,
with the reasons in `error_message`.

## In the web app

The Scheduled page at `/scheduled` lists your organization's scheduled messages with a status
filter, shows each one's destination, recurrence and conditions, and offers reschedule (a new time
and zone), cancel and send now for anything still `scheduled`. It also shows the create request
above as a copy-paste curl. Creating a scheduled message is an API call; the page manages what the
API created.
