# Scheduled texts

> Send a text at a time you choose with send_at, change or cancel it until then, and see how quiet hours, pacing and expiry decide when it really goes.

Add `send_at` to a send and the text waits as a draft until that time. Until it’s due you can change its words, its time or its metadata, or cancel it. When the time comes, it joins the queue like any text sent at that moment.

Dr. Patel’s office books a cleaning for Maria on Thursday at 10:30 AM. The booking system schedules a reminder for Wednesday at 5 PM:

```bash
curl -X POST "https://usebluebot.com/api/v1/messages" \
  -H "Authorization: Bearer $BLUEBOT_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: appt-7712-reminder" \
  -d '{
    "to": "+15555550123",
    "text": "Hi Maria, a reminder that your cleaning with Dr. Patel is tomorrow at 10:30 AM. Reply C to confirm.",
    "send_at": "2026-11-19T17:00:00-05:00",
    "expires_at": "2026-11-20T10:30:00-05:00",
    "metadata": {
      "appointment_id": "7712"
    }
  }'
```

```ts
import BlueBot from '@usebluebot/sdk';

const client = new BlueBot({ apiKey: process.env.BLUEBOT_API_KEY });

const message = await client.messages.send({
  to: '+15555550123',
  text: 'Hi Maria, a reminder that your cleaning with Dr. Patel is tomorrow at 10:30 AM. Reply C to confirm.',
  sendAt: '2026-11-19T17:00:00-05:00',
  expiresAt: '2026-11-20T10:30:00-05:00',
  metadata: {
    appointment_id: '7712',
  },
}, { idempotencyKey: 'appt-7712-reminder' });
console.log(message);
```

```python
import os
from bluebot import BlueBot

client = BlueBot(api_key=os.environ["BLUEBOT_API_KEY"])

message = client.messages.send(
    to="+15555550123",
    text="Hi Maria, a reminder that your cleaning with Dr. Patel is tomorrow at 10:30 AM. Reply C to confirm.",
    send_at="2026-11-19T17:00:00-05:00",
    expires_at="2026-11-20T10:30:00-05:00",
    metadata={
        "appointment_id": "7712",
    },
    idempotency_key="appt-7712-reminder",
)
print(message)
```

The answer, `201`:

```json
{
  "id": "msg_1Hq2LxW9cVbN3mKpRtYs0a",
  "object": "message",
  "state": "scheduled",
  "to": "+15555550123",
  "hold": null,
  "send_at": "2026-11-19T22:00:00.000Z",
  "expires_at": "2026-11-20T15:30:00.000Z",
  "queued_at": null,
  "metadata": { "appointment_id": "7712" }
}
```

Write `send_at` as an ISO 8601 time with its zone: `2026-11-19T17:00:00-05:00` or `2026-11-19T22:00:00Z`. A time with no zone is refused, since BlueBot can’t guess whose 5 PM you meant. Answers always give times in UTC.

A `send_at` less than 5 seconds away, or already past, isn’t an error: the text is sent now and comes back `queued`. A booking made after the reminder time still gets its reminder.

Scheduling fires no webhook. The first event for a scheduled text is `message.queued`, when it becomes due.

## When it really goes

`send_at` is when the text joins the queue, not the second it leaves your phone:

1. Within about a minute of `send_at`, BlueBot moves the text from `scheduled` to `queued` and fires `message.queued`.
2. The rules decide whether it can go now. If not, it gets a `hold` that says why and until when, exactly as a text sent at that moment would.
3. The Mac takes it at its next check-in (every 10 seconds) in its turn, leaving your pace between texts.

So a reminder scheduled for 5:00 PM usually arrives a little after 5:00. Texts due at the same time leave in the order they became due, one pace apart: 200 reminders all scheduled for 9:00 AM leave over about an hour at the default pace of 20 seconds. Spread them out if the time matters, or lower the pace in your sending rules. [Sending rules](https://usebluebot.com/docs/concepts/sending-rules)

### Quiet hours

A scheduled text obeys quiet hours like any other. With the default quiet hours, 9:00 PM to 8:00 AM in your zone, a text scheduled for 7:30 AM is held with `quiet_hours` and goes at 8:00 AM. If you want texts to go at 7:30, move the end of quiet hours, or turn them off, under **Settings → Sending rules**. [Quiet hours](https://usebluebot.com/docs/concepts/quiet-hours)

The same goes for the other holds. If the Mac is asleep at 5 PM, the reminder waits for it to come back. If you’ve reached your new-people limit for the day, a scheduled text to somebody new waits until midnight. [Holds](https://usebluebot.com/docs/concepts/holds)

### Expiry

A held text doesn’t wait forever. Each text has an `expires_at`. A text still waiting at that time becomes `expired`, fires `message.expired` and never goes.

For a scheduled text, the default expiry counts from `send_at`: 24 hours after it, or whatever your **Expire texts after** setting says (1 to 168 hours). A text never expires while it’s still scheduled.

Give a reminder its own `expires_at` when a late text would be wrong. In the example above, the reminder expires at 10:30 AM on Thursday: if Dr. Patel’s Mac is off from Wednesday afternoon until Thursday noon, Maria gets no reminder for an appointment she has already missed. `expires_at` has to be after `send_at`, or the send is `400 invalid_request`.

## Change it before it goes

While the text is `scheduled`, `PATCH` it to change the words, the time, the expiry or the metadata. Maria moves her cleaning to Friday at 2 PM:

```bash
curl -X PATCH "https://usebluebot.com/api/v1/messages/msg_1Hq2LxW9cVbN3mKpRtYs0a" \
  -H "Authorization: Bearer $BLUEBOT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "text": "Hi Maria, a reminder that your cleaning with Dr. Patel is tomorrow at 2 PM. Reply C to confirm.",
    "send_at": "2026-11-20T17:00:00-05:00",
    "expires_at": "2026-11-21T14:00:00-05:00"
  }'
```

```ts
import BlueBot from '@usebluebot/sdk';

const client = new BlueBot({ apiKey: process.env.BLUEBOT_API_KEY });

const message = await client.messages.update('msg_1Hq2LxW9cVbN3mKpRtYs0a', {
  text: 'Hi Maria, a reminder that your cleaning with Dr. Patel is tomorrow at 2 PM. Reply C to confirm.',
  sendAt: '2026-11-20T17:00:00-05:00',
  expiresAt: '2026-11-21T14:00:00-05:00',
});
console.log(message);
```

```python
import os
from bluebot import BlueBot

client = BlueBot(api_key=os.environ["BLUEBOT_API_KEY"])

message = client.messages.update(
    "msg_1Hq2LxW9cVbN3mKpRtYs0a",
    text="Hi Maria, a reminder that your cleaning with Dr. Patel is tomorrow at 2 PM. Reply C to confirm.",
    send_at="2026-11-20T17:00:00-05:00",
    expires_at="2026-11-21T14:00:00-05:00",
)
print(message)
```

A new `send_at` without an `expires_at` moves the default expiry along with it. `metadata` replaces the whole object. A `send_at` that is now or past sends the text at once. Once a text is due it can’t be changed, only canceled. [Change, cancel or settle a text](https://usebluebot.com/docs/sending/change-or-cancel)

## Cancel it

```bash
curl -X POST "https://usebluebot.com/api/v1/messages/msg_1Hq2LxW9cVbN3mKpRtYs0a/cancel" \
  -H "Authorization: Bearer $BLUEBOT_API_KEY" \
  -H "Idempotency-Key: appt-7712-reminder-cancel"
```

```ts
import BlueBot from '@usebluebot/sdk';

const client = new BlueBot({ apiKey: process.env.BLUEBOT_API_KEY });

const message = await client.messages.cancel('msg_1Hq2LxW9cVbN3mKpRtYs0a', { idempotencyKey: 'appt-7712-reminder-cancel' });
console.log(message);
```

```python
import os
from bluebot import BlueBot

client = BlueBot(api_key=os.environ["BLUEBOT_API_KEY"])

message = client.messages.cancel(
    "msg_1Hq2LxW9cVbN3mKpRtYs0a",
    idempotency_key="appt-7712-reminder-cancel",
)
print(message)
```

A scheduled or queued text can be canceled. It comes back `"state": "canceled"` with `"cancel_reason": "requested"`, and `message.canceled` fires. Canceling a text that’s already canceled answers it again unchanged, so a retry is safe. Once the Mac has taken it, it can’t be called back.

If the person texts STOP before the text is due, BlueBot cancels it for you with `cancel_reason: "opted_out"`.

## Find your scheduled texts

```bash
curl "https://usebluebot.com/api/v1/messages?state=scheduled" \
  -H "Authorization: Bearer $BLUEBOT_API_KEY"
```

```ts
import BlueBot from '@usebluebot/sdk';

const client = new BlueBot({ apiKey: process.env.BLUEBOT_API_KEY });

const page = await client.messages.list({
  state: 'scheduled',
});
console.log(page.data);
```

```python
import os
from bluebot import BlueBot

client = BlueBot(api_key=os.environ["BLUEBOT_API_KEY"])

page = client.messages.list(state="scheduled")
print(page.data)
```

The list is newest first by when each text was made, not by `send_at`. Keep the message `id` in your own record when you schedule, next to the appointment or the order it’s for, so you can change or cancel the right text later. Putting your record’s id in `metadata` lets every event about the text find the record too.

| You got                               | It means                                                                  | Do next                                                    |
| ------------------------------------- | ------------------------------------------------------------------------- | ---------------------------------------------------------- |
| `201` with `"state": "scheduled"`     | It goes at `send_at`.                                                     | Keep the `id` to change or cancel it.                      |
| `201` with `"state": "queued"`        | `send_at` was less than 5 seconds away or in the past, so it’s going now. | Check that your clock and the zone in `send_at` are right. |
| `400 invalid_request` on `send_at`    | The time has no zone or isn’t a date.                                     | Write it like `2026-11-19T17:00:00-05:00`.                 |
| `400 invalid_request` on `expires_at` | It’s before `send_at`.                                                    | Give it a time after the text is due.                      |
| `409 conflict` on a `PATCH`           | The text is already due (`queued`) or gone.                               | Cancel it if it’s still `queued`, then send a new one.     |

## In the SDKs

The SDKs take the time as a string or a date:

```ts title="schedule.ts"
const message = await client.messages.send(
    {
        to: '+15555550123',
        text: 'Hi Maria, a reminder that your cleaning with Dr. Patel is tomorrow at 10:30 AM. Reply C to confirm.',
        sendAt: '2026-11-19T17:00:00-05:00',
        expiresAt: '2026-11-20T10:30:00-05:00',
        metadata: { appointment_id: '7712' },
    },
    { idempotencyKey: 'appt-7712-reminder' },
);
```

```python title="schedule.py"
from datetime import datetime
from zoneinfo import ZoneInfo

eastern = ZoneInfo("America/New_York")
message = client.messages.send(
    to="+15555550123",
    text="Hi Maria, a reminder that your cleaning with Dr. Patel is tomorrow at 10:30 AM. Reply C to confirm.",
    send_at=datetime(2026, 11, 19, 17, 0, tzinfo=eastern),
    expires_at=datetime(2026, 11, 20, 10, 30, tzinfo=eastern),
    metadata={"appointment_id": "7712"},
    idempotency_key="appt-7712-reminder",
)
```

A `Date` in TypeScript or an aware `datetime` in Python is sent with its zone. A naive Python `datetime`, with no `tzinfo`, is refused by the SDK before it reaches BlueBot.

The [appointment reminders guide](https://usebluebot.com/docs/guides/appointment-reminders) builds this into a whole booking flow.
