# Scheduled Messages

Scheduled messages let a user write a message now and have it sent later. The user picks a time, and Stream sends the message to the channel at that time on the user's behalf. Until then the user can edit the message, move it to another time, send it right away, or cancel it.

A scheduled message is private until it is sent. Other channel members see nothing, receive no events and get no push notifications until the message is actually sent. When it is sent, it is a normal message: it goes through the same checks, moderation, webhooks and push notifications as a message sent at that moment.

Scheduled messages are built for one user scheduling one message into one channel, direct message or thread. To send the same message to many users or channels, use [Campaigns](https://getstream.io/chat/docs/node/campaign-api/) instead.

>
> **Info:** Client-side support is available in the JavaScript SDK (and the React and React Native SDKs built on it). From a backend, use one of the server-side SDKs to schedule messages on behalf of your users.
>

## Overview

- **Off by default.** Scheduled messages are enabled per app by Stream. See [Enabling scheduled messages](#enabling-scheduled-messages).
- **Scheduled with the send message endpoint.** Add `scheduled_at` to a normal send message request. Anything you can send as a message, including thread replies, attachments, mentions and polls, can be scheduled. Slash commands cannot.
- **Sent on the minute.** A future send time is moved to the next whole minute, at least 1 minute from now. A send time that is now or in the past sends the message right away, so a request queued while the device was offline still goes out, as long as scheduled messages are still enabled for the app.
- **Checked twice.** The request is checked when it is scheduled (permissions, channel, content, limits), and the message goes through the full send path again at the scheduled time. A message that was valid when scheduled can still fail to send, for example when the user was banned or lost the permission to send in the meantime.
- **Owned by one user.** Each scheduled message belongs to the user who scheduled it. Client-side, users can only see and change their own scheduled messages. Server-side, you can manage the scheduled messages of any user.
- **Events go to the owner only.** `scheduled_message.*` events are delivered only to the connections of the user who scheduled the message.

## Enabling scheduled messages

Scheduled messages are an app-level setting that is off by default. To enable them for your app, [contact support](https://getstream.io/contact/support/).

While the feature is off, scheduling a message or editing a scheduled message fails with error code `114` (`scheduled_messages_disabled`). Listing, deleting and sending scheduled messages now keep working, so users can still manage the messages they already scheduled.

## Permissions

Scheduling a message requires the `ScheduleMessage` permission in addition to the permissions needed to send the same message right away (`CreateMessage`, `CreateReply` for thread replies, attachment, mention and pin permissions, and so on). By default, `ScheduleMessage` is granted to the same roles as `CreateMessage`. See the [permissions reference](https://getstream.io/chat/docs/javascript/permissions-reference/) for the default grants.

To show or hide a "schedule" option in your UI, check the `schedule-message` capability in the channel's `own_capabilities`. The capability is only returned when scheduled messages are enabled for the app.

A user without the `ScheduleMessage` permission gets the usual permission error: HTTP 403 with error code `17`.

## Limits

| Limit                                                   | Default  | Channel `config` field                     |
| ------------------------------------------------------- | -------- | ------------------------------------------ |
| Pending scheduled messages per user in a single channel | 10       | `scheduled_messages_max_per_channel`       |
| Pending scheduled messages per user across the app      | 100      | `scheduled_messages_max_per_user`          |
| Earliest send time                                      | 1 minute | `scheduled_messages_min_lead_time_seconds` |
| Latest send time                                        | 120 days | `scheduled_messages_max_lead_time_seconds` |

- A pending scheduled message is one with status `scheduled` or `sending`. Failed and deleted scheduled messages don't count toward the limits.
- The earliest send time is fixed at 1 minute and cannot be changed. The other values are defaults that Stream can change for your app.
- A future send time closer than 1 minute is moved to 1 minute from now, rounded up to the minute. It is never rejected.
- A send time later than the latest send time is rejected with error code `116`.

The app's limits are returned in every channel's `config`, in the fields listed above, so your UI can respect them before the API refuses a request, for example to bound a date picker or disable the schedule option when the user has reached the limit. The fields are only present when scheduled messages are enabled for the app. The values are the same for every channel of the app: they are app settings, not channel type settings.

## Scheduling a message

To schedule a message, send it with `scheduled_at` set to the time it should be sent. Times are in UTC.

```js label="JavaScript"
const response = await channel.sendMessage(
  { text: "Happy birthday!", priority: "high" },
  { scheduled_at: new Date("2026-12-24T09:00:00Z") },
);

// The message is not in the channel yet
const scheduledMessage = response.scheduled_message;
console.log(scheduledMessage.id, scheduledMessage.scheduled_at);
```

The response contains `scheduled_message`, and nothing is added to the channel. The response's `message` field is only a preview of what will be sent: it is not in the channel, and its `id` is the ID of the scheduled message, not the ID the sent message will have. Don't add it to the channel's message list or render it as sent.

Custom data is set on a scheduled message the same way as on a regular message, and the sent message carries it like any other message.

To schedule a thread reply, set `parent_id` on the message, as you would when [replying in a thread](https://getstream.io/chat/docs/javascript/threads/).

### Send time

Messages are sent on whole minutes. A future `scheduled_at` is moved to the first whole minute (UTC, seconds set to zero) that is not earlier than the requested time and at least 1 minute from now. A message is never sent earlier than requested.

| Requested `scheduled_at` (now is 10:20:00) | Stored `scheduled_at` |
| ------------------------------------------ | --------------------- |
| 10:30:00                                   | 10:30:00              |
| 10:30:20                                   | 10:31:00              |
| 10:20:30                                   | 10:21:00              |
| 10:20:00 or earlier                        | Sent right away       |

`scheduled_message.scheduled_at` in the response holds the stored time. Show that time to the user, and send it back unchanged when you update the message.

A `scheduled_at` that is now or in the past sends the message right away, as if `scheduled_at` was not set: the response is a normal send message response with the sent `message` and no `scheduled_message`. This lets a client that queued a schedule request while offline replay it after reconnecting, even when the time has passed. Sending right away needs only the permissions of a normal send, not `ScheduleMessage`. It still needs scheduled messages to be enabled for the app: while the feature is off, a request with `scheduled_at` fails with error code `114`, whatever the time. A request with `scheduled_at` cannot set `pending: true`, even when the time has passed.

### Retrying a schedule request

If you set `message.id` when scheduling, it becomes the ID of the scheduled message. Sending the same request again with the same ID, from the same user and to the same channel, returns the existing scheduled message instead of scheduling a second one, as long as it is still `scheduled`, `sending` or `failed`. This makes it safe to retry a schedule request after a timeout or replay it after the device was offline. Using an ID that already belongs to another user's or another channel's scheduled message fails with error code `119` (`scheduled_message_id_taken`).

A retry can also find that the scheduled message is no longer there:

- If it was **already sent**, the request fails with error code `123` (`scheduled_message_already_sent`), so the message is not posted twice. Treat this error as success: the message under that ID was sent.
- If the user **canceled** it, for example on another device, a retry whose `scheduled_at` is now or in the past fails with error code `124` (`scheduled_message_canceled`), so a replay does not send a message the user canceled. A retry with a future `scheduled_at` schedules the message again under the same ID.

Stream keeps a canceled scheduled message for 7 days. A replay that arrives later, or after the sent message was hard deleted, is treated as a new message.

### What is checked when

When a message is scheduled, Stream runs the checks that need no side effects: the feature setting, the send time, the user and the channel, permissions, content rules (text length, attachments, mentions, quoted message, poll, shared location) and the limits. Requests that fail these checks are rejected right away.

At the scheduled time, the message goes through the full send path, which also runs moderation, the [before message send webhook](https://getstream.io/chat/docs/node/before-message-send-webhook/), URL enrichment and translation. Slow mode does not apply to scheduled messages.

Because every check runs again at the scheduled time, a message that was valid when it was scheduled fails if it is no longer valid when it is sent. For example, if the message quotes another message and that message is deleted before the send, the scheduled message fails with the `invalid_message` reason, just as a normal send that quotes a deleted message is rejected.

If the channel is configured to mark new messages as [pending](https://getstream.io/chat/docs/javascript/pending-messages/) but pending messages are not enabled for your app, scheduling fails with HTTP 403, as a normal send would. Otherwise scheduling works, but the send fails with the `pending_not_supported` reason unless the request sets `pending: false` explicitly (server-side only). A schedule request that sets `pending: true` is rejected.

## The scheduled message object

| Field            | Type   | Description                                                                                                           |
| ---------------- | ------ | --------------------------------------------------------------------------------------------------------------------- |
| `id`             | string | ID of the scheduled message. Not the ID of the message once it is sent                                                |
| `cid`            | string | The channel the message will be sent to                                                                               |
| `user`           | object | The user who scheduled the message                                                                                    |
| `message`        | object | The message that will be sent: `text`, `attachments`, `parent_id`, `mentioned_users` (user IDs), custom data and more |
| `scheduled_at`   | date   | When the message will be sent                                                                                         |
| `status`         | string | See [Statuses](#statuses)                                                                                             |
| `failure_reason` | string | Why the send failed. Only set when `status` is `failed`. See [Failure reasons](#failure-reasons)                      |
| `channel`        | object | The channel's `cid`, `type`, `id`, `name` and `image`. Only set in query results                                      |
| `created_at`     | date   | When the message was scheduled                                                                                        |
| `updated_at`     | date   | When the scheduled message was last changed                                                                           |

In the scheduled message's `message`, `mentioned_users` holds the user IDs from the request, not user objects. Custom data behaves exactly as on regular messages: set it on the message the same way when scheduling or updating, and it comes back in the same shape your SDK uses for regular messages (top-level fields in the JavaScript SDK, under `custom` in the server-side SDKs).

### Statuses

| Status      | Meaning                                                                                         | Allowed actions          |
| ----------- | ----------------------------------------------------------------------------------------------- | ------------------------ |
| `scheduled` | Waiting for its send time                                                                       | Update, send now, delete |
| `sending`   | Being sent, or waiting to retry after a temporary send error                                    | None                     |
| `failed`    | The send failed. `failure_reason` says why                                                      | Delete                   |
| `canceled`  | Deleted by the user. Only returned by the delete call and the `scheduled_message.deleted` event | None                     |
| `sent`      | Sent. Only reported by the `scheduled_message.sent` event                                       | None                     |

Once a scheduled message is sent, it no longer exists as a scheduled message: it is a normal message in the channel. Failed scheduled messages are kept for 7 days so the user can see what went wrong, and are then removed.

### Failure reasons

| Reason                  | Meaning                                                                                                                                      |
| ----------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- |
| `banned`                | The user is banned from the channel or the app                                                                                               |
| `no_permission`         | The user no longer has permission to send this message                                                                                       |
| `channel_frozen`        | The channel is frozen. Only for messages scheduled client-side: like a live send, a message scheduled server-side is not blocked by a freeze |
| `channel_deleted`       | The channel was deleted                                                                                                                      |
| `user_deleted`          | The user was deleted or deactivated                                                                                                          |
| `moderation_blocked`    | Moderation blocked the message                                                                                                               |
| `hook_rejected`         | The before message send webhook rejected the message                                                                                         |
| `expired`               | The message could not be sent in time. See below                                                                                             |
| `invalid_message`       | The message is no longer valid, for example because the app's message settings changed or a quoted message was deleted                       |
| `pending_not_supported` | The channel marks new messages as pending, and the request did not set `pending: false`                                                      |
| `internal_error`        | The send failed 3 times because of a temporary error                                                                                         |

A message expires when it could not be sent within 1 hour of its scheduled time, for example during an outage. When many messages are due at the same minute, the ones that were queued on time are still sent after that hour, but never more than 24 hours late.

`failure_reason` only ever holds one of the values above, so you can switch on it to show your own text.

## Listing scheduled messages

Query scheduled messages to list a user's pending (`scheduled` and `sending`) and `failed` scheduled messages. Pending messages come first, then failed ones, each ordered by `scheduled_at`. Client-side, the query returns the current user's scheduled messages. Server-side, `user_id` is required.

You can filter by channel (`cid`) and by `status` (one or more of `scheduled`, `sending` and `failed`). The only supported sort is `scheduled_at` ascending, which is also the default. The page size defaults to 20 and can be at most 100.

```js label="JavaScript"
const { scheduled_messages, next } = await client.queryScheduledMessages({
  filter: { cid: channel.cid },
  limit: 20,
});

// Next page
const nextPage = await client.queryScheduledMessages({
  filter: { cid: channel.cid },
  limit: 20,
  next,
});
```

A user can have up to 100 pending scheduled messages plus their failed ones, so a single page may not hold all of them. Follow `next` until it is empty, and remove duplicates by `id` across pages, because a message can move between pages when its time changes.

### Including scheduled messages in channel state

You can load the user's scheduled messages together with the channels themselves. Set `include_scheduled_messages` when watching or querying a channel, or when querying channels, and each channel's state contains `scheduled_messages`: the user's `scheduled`, `sending` and `failed` messages in that channel, or an empty list when there are none.

```js label="JavaScript"
// One channel
const channel = client.channel("messaging", "general");
const state = await channel.watch({ include_scheduled_messages: true });

if (state.scheduled_messages) {
  // The user's scheduled messages in this channel
} else {
  // Not available, use client.queryScheduledMessages instead
}

// A page of channels
const channels = await client.queryChannels(
  { members: { $in: [client.userID] } },
  { last_message_at: -1 },
  { include_scheduled_messages: true },
);
```

`scheduled_messages` is always the complete list, never a partial one. When the complete list can't be returned, the field is left out instead, and you should use the [query](#listing-scheduled-messages) for that channel. A missing field means "not loaded", not "none". This happens when:

- the feature is off for the app;
- the list would hold more than 100 scheduled messages. Pending messages are limited to 10 per channel, so this only happens when the user also has many failed messages. When querying channels, the 100 applies to all channels of the page together, and the field is then left out on every channel of the page;
- the scheduled messages could not be loaded.

Which user's scheduled messages are returned:

- **Client-side:** the current user's.
- **Server-side, watching or querying one channel:** the user set as `created_by` or `created_by_id` in the request's `data`. Without one, the field is left out.
- **Server-side, querying channels:** the option is not supported. Query scheduled messages with `user_id` and a `cid` filter instead.

## Updating a scheduled message

You can change the content of a scheduled message, its send time, or both. A new `message` replaces the scheduled message as a whole, so send every field you want to keep. Only messages with status `scheduled` can be updated.

```js label="JavaScript"
const { scheduled_message } = await client.updateScheduledMessage(
  scheduledMessageId,
  {
    message: { text: "Happy birthday, John!" },
    scheduled_at: new Date("2026-12-24T08:00:00Z"),
  },
);
```

An update runs the same content and permission checks as scheduling, but not the limits.

A new `scheduled_at` is rounded up to the minute and checked against the latest send time, as when scheduling. Unlike scheduling, an update never sends the message: a `scheduled_at` that is now or in the past fails with error code `115` (`scheduled_at_too_soon`). To send a scheduled message right away, use [send now](#sending-a-scheduled-message-now).

When you only change the text, you can send `scheduled_at` again or leave it out. A `scheduled_at` equal to the stored one (`scheduled_message.scheduled_at` from the last response) is treated as unchanged: it is not checked again, even if the send time is less than a minute away. An update without `message` whose `scheduled_at` is left out or equal to the stored one changes nothing: it returns the scheduled message without a `scheduled_message.updated` event. An update that includes `message` is always saved and sends `scheduled_message.updated`, even when the message is unchanged.

## Sending a scheduled message now

To send a scheduled message right away instead of waiting for its time, use send now. The send happens in the background: the response returns the scheduled message, and the `scheduled_message.sent` or `scheduled_message.failed` event reports the outcome. Only messages with status `scheduled` can be sent now.

```js label="JavaScript"
const { scheduled_message } =
  await client.sendScheduledMessageNow(scheduledMessageId);
```

## Deleting a scheduled message

Deleting a `scheduled` message cancels it: it will not be sent, and the response returns it with status `canceled`. Deleting a `failed` message removes it, and the response returns it as it was, with status `failed`. After a delete, the scheduled message no longer appears anywhere.

```js label="JavaScript"
const { scheduled_message } =
  await client.deleteScheduledMessage(scheduledMessageId);
```

Deleting the user or the channel also affects their scheduled messages:

- **Hard or pruning user delete:** the user's scheduled messages are removed.
- **Soft user delete or deactivation:** the scheduled messages are kept, and fail with the `user_deleted` reason at their send time.
- **Channel delete:** scheduled messages that were not sent yet fail with the `channel_deleted` reason, and their owners get `scheduled_message.failed`. After a soft delete they are kept for 7 days like other failed scheduled messages; a hard delete removes them. Messages that were already `sending` are removed without an event.

### Messages that are being sent

A message with status `sending` cannot be updated, sent now or deleted: these calls fail with HTTP 409 and error code `121` (`scheduled_message_sending`). This includes a message that is waiting to be retried after a temporary send error. Don't retry the call; wait for the `scheduled_message.sent` or `scheduled_message.failed` event instead.

If two changes to the same scheduled message happen at the same time, one of them fails with HTTP 409 and error code `122` (`scheduled_message_conflict`). Load the scheduled message again and retry.

## Keeping scheduled messages in sync on the client

The JavaScript SDK keeps the current user's scheduled messages in `client.scheduledMessages`. It loads them with one query and then keeps them up to date from the `scheduled_message.*` events: created and updated messages are added, failed ones are marked as failed, and sent or deleted ones are removed. It also takes the `scheduled_messages` of channels watched or queried with `include_scheduled_messages`, and the `scheduled_message` of schedule responses. After a connection recovery, it loads them again.

```js label="JavaScript"
// Once the user is connected
client.scheduledMessages.registerSubscriptions();
await client.scheduledMessages.load();

// Ordered by scheduled_at
const forChannel = client.scheduledMessages.getForChannel(channel.cid); // excludes thread replies
const forThread = client.scheduledMessages.getForThread(parentMessageId);
const count = client.scheduledMessages.getCountForChannel(channel.cid);

// React to changes
const unsubscribe = client.scheduledMessages.state.subscribeWithSelector(
  (state) => ({ scheduledMessages: state.scheduledMessages }),
  ({ scheduledMessages }) => {
    // scheduledMessages is a Map of scheduled message ID to scheduled message
  },
);

// These call the API and update the store
await client.scheduledMessages.updateScheduledMessage(id, { scheduled_at });
await client.scheduledMessages.sendScheduledMessageNow(id);
await client.scheduledMessages.deleteScheduledMessage(id);
```

`load()` fetches up to 100 scheduled messages. When `state.next` is set, call `loadMore()` to fetch the next page. When the feature is off for the app, `state.isDisabled` is `true`.

## Events

`scheduled_message.*` events are sent only to the connections of the user who scheduled the message, never to other channel members. Only `scheduled_message.sent` and `scheduled_message.failed` are also sent to [webhooks](https://getstream.io/chat/docs/node/webhook-events/).

| Event                       | Trigger                                                  | Webhook |
| --------------------------- | -------------------------------------------------------- | ------- |
| `scheduled_message.created` | A message was scheduled                                  | No      |
| `scheduled_message.updated` | A scheduled message was updated or sent now              | No      |
| `scheduled_message.deleted` | A scheduled message was canceled or a failed one removed | No      |
| `scheduled_message.sent`    | A scheduled message was sent                             | Yes     |
| `scheduled_message.failed`  | A scheduled message failed to send                       | Yes     |

Every event carries `cid`, `user` and `scheduled_message`. `scheduled_message.sent` reports the status as `sent` and also carries `message_id`, the ID of the message that was sent to the channel. The sent message itself is delivered like any other message, with `message.new` to the channel members.

Messages that expire, or fail because their channel was deleted, also send `scheduled_message.failed`. If the user who scheduled the message was deleted, the `scheduled_message.sent` and `scheduled_message.failed` webhooks are still sent, with the deleted user in `user`.

```js label="JavaScript"
client.on("scheduled_message.sent", (event) => {
  console.log(`${event.scheduled_message.id} was sent as ${event.message_id}`);
});

client.on("scheduled_message.failed", (event) => {
  console.log(event.scheduled_message.failure_reason);
});
```

## Errors

Scheduled message errors set `exception_fields.reason` to the name in the table below, so you can handle them without hardcoding the numeric code. Errors caused by a limit also carry the limit in `exception_fields`, as a string, so you can write your own error text.

| Code | Reason                           | HTTP status | Meaning                                                                                     | Extra `exception_fields` |
| ---- | -------------------------------- | ----------- | ------------------------------------------------------------------------------------------- | ------------------------ |
| 114  | `scheduled_messages_disabled`    | 400         | Scheduled messages are not enabled for the app                                              |                          |
| 115  | `scheduled_at_too_soon`          | 400         | An update set `scheduled_at` to now or a past time. Only returned when updating             | `min_lead_time_seconds`  |
| 116  | `scheduled_at_too_far`           | 400         | `scheduled_at` is later than the app's latest send time (120 days by default)               | `max_lead_time_seconds`  |
| 117  | `scheduled_limit_channel`        | 400         | The user has too many pending scheduled messages in this channel                            | `limit`                  |
| 118  | `scheduled_limit_user`           | 400         | The user has too many pending scheduled messages                                            | `limit`                  |
| 119  | `scheduled_message_id_taken`     | 400         | The scheduled message ID belongs to another user's or channel's message                     |                          |
| 120  | `scheduled_message_not_editable` | 400         | The scheduled message failed to send and can only be deleted                                |                          |
| 121  | `scheduled_message_sending`      | 409         | The scheduled message is being sent and cannot change                                       |                          |
| 122  | `scheduled_message_conflict`     | 409         | Another change to the scheduled message happened first. Reload and retry                    |                          |
| 123  | `scheduled_message_already_sent` | 400         | A message with this ID was already sent. When retrying a schedule request, treat as success |                          |
| 124  | `scheduled_message_canceled`     | 400         | The scheduled message with this ID was canceled, so a replay with a past time is not sent   |                          |

For example:

```json
{
  "code": 117,
  "message": "SendMessage failed with error: \"you can schedule at most 10 messages in this channel\"",
  "exception_fields": { "reason": "scheduled_limit_channel", "limit": "10" }
}
```

Updating, sending now or deleting a scheduled message that does not exist, was already sent or canceled, or belongs to another user (client-side) returns HTTP 404.

## REST endpoints

| Action                       | Endpoint                                                 |
| ---------------------------- | -------------------------------------------------------- |
| Schedule a message           | `POST /channels/{type}/{id}/message` with `scheduled_at` |
| Query scheduled messages     | `POST /scheduled_messages/query`                         |
| Update a scheduled message   | `PATCH /scheduled_messages/{id}`                         |
| Send a scheduled message now | `POST /scheduled_messages/{id}/send_now`                 |
| Delete a scheduled message   | `DELETE /scheduled_messages/{id}`                        |

---

For the most recent version of this documentation, visit [https://getstream.io/chat/docs/javascript/scheduled-messages/](https://getstream.io/chat/docs/javascript/scheduled-messages/).