---
title: Scheduling
description: Defer a send up to 30 days, move it, or cancel it — and what stays mutable until the moment it goes out.
sidebar:
  icon: clock
---

Pass `scheduled_at` on a send or a reply and the message is held until then. The API returns it immediately with `status: "scheduled"`, and it stays yours to move or cancel right up until it leaves.

```ts TypeScript
const email = await aiinbx.emails.send({
  from: "Ada <ada@example.com>",
  to: "grace@example.com",
  subject: "Monday reminder",
  text: "The review is at 10.",
  scheduled_at: "2026-09-14T09:00:00Z",
})

console.log(email.status) // "scheduled"
```

```python Python
email = client.emails.send(
    {
        "from_": "Ada <ada@example.com>",
        "to": ["grace@example.com"],
        "subject": "Monday reminder",
        "text": "The review is at 10.",
        "scheduled_at": "2026-09-14T09:00:00Z",
    }
)
```

```bash curl
curl https://api.aiinbx.com/api/v2/emails \
  -H "Authorization: Bearer $AI_INBX_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "from": "Ada <ada@example.com>",
    "to": "grace@example.com",
    "subject": "Monday reminder",
    "text": "The review is at 10.",
    "scheduled_at": "2026-09-14T09:00:00Z"
  }'
```

`scheduled_at` is an RFC 3339 timestamp **with an offset** (`2026-09-14T09:00:00Z` or `2026-09-14T11:00:00+02:00`), at most 30 days out. A naive timestamp is rejected with [`invalid_request`](/reference/errors#invalid_request) — the offset is required precisely because "9am" without one means six different instants.

:::note
Scheduling works identically on [`threads.reply`](/guides/threads#replying). A scheduled reply still resolves its recipients and headers from the thread at the moment it's composed, so a participant added in the meantime is included.
:::

## Rescheduling

`PATCH /emails/{id}` moves a scheduled message. The same 30-day bound applies from the moment of the call.

```ts TypeScript
const moved = await aiinbx.emails.reschedule("eml_...", {
  scheduled_at: "2026-09-15T09:00:00Z",
})
```

```python Python
moved = client.emails.reschedule("eml_...", scheduled_at="2026-09-15T09:00:00Z")
```

```bash curl
curl -X PATCH https://api.aiinbx.com/api/v2/emails/eml_... \
  -H "Authorization: Bearer $AI_INBX_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "scheduled_at": "2026-09-15T09:00:00Z" }'
```

Only a message still in `scheduled` status can move. Once it has begun sending, the call fails with `409` [`not_scheduled`](/reference/errors#not_scheduled).

## Canceling

```ts TypeScript
const canceled = await aiinbx.emails.cancel("eml_...")
console.log(canceled.status) // "canceled"
```

```python Python
canceled = client.emails.cancel("eml_...")
```

```bash curl
curl -X POST https://api.aiinbx.com/api/v2/emails/eml_.../cancel \
  -H "Authorization: Bearer $AI_INBX_API_KEY"
```

Cancellation is terminal — a canceled message can't be rescheduled back into flight. Send a new one instead.

Both rescheduling and cancellation need a key with the `sending` or `full` [scope](/authentication#scopes).

## Finding what's scheduled

`status: "scheduled"` on the email list is the queue of everything still pending:

```ts TypeScript
for await (const email of aiinbx.emails.list({ status: "scheduled" })) {
  console.log(email.scheduled_at, email.subject, email.to)
}
```

```python Python
for email in client.emails.iter(status="scheduled"):
    print(email["scheduled_at"], email["subject"], email["to"])
```

```bash curl
curl "https://api.aiinbx.com/api/v2/emails?status=scheduled&limit=100" \
  -H "Authorization: Bearer $AI_INBX_API_KEY"
```

## Composed at send time, not at schedule time

A scheduled message is stored as an intent and composed at the instant it goes out. Two consequences worth designing around:

- **Suppressions are applied then, not now.** A recipient who unsubscribes between scheduling and sending is dropped, and shows up in the delivery record rather than in the response you already received.
- **Pacing rules are evaluated then, too.** A message scheduled for 3am under an [hours rule](/guides/pacing#sending-hours) that forbids 3am is held until the window opens; `scheduled_at` sets the earliest time it may go, not a guarantee of the exact instant.

If you need a message to ignore the pacing rules entirely, set `pacing.skip` on it:

```json
{
  "scheduled_at": "2026-09-14T09:00:00Z",
  "pacing": { "skip": true }
}
```

## Scheduling versus pacing

They solve different problems and compose fine:

| | [Scheduling](/guides/scheduling) | [Pacing](/guides/pacing) |
| --- | --- | --- |
| Set on | One message | Workspace-wide rules |
| Answers | "Not before this instant" | "Not faster than this, and not outside these hours" |
| Changed by | `reschedule` / `cancel` | Rule edits, or releasing from the queue |

Use `scheduled_at` for "send this Monday morning". Use pacing for "never more than 200 an hour from this domain".
