---
title: "Scheduled sends"
description: "Schedule an email up to 90 days ahead with scheduled_at, then reschedule or cancel it before it dispatches."
---

> **For AI agents:** the complete documentation index is at [llms.txt](/docs/llms.txt). Append `.md` to any page URL for its markdown version.

Any send can carry a `scheduled_at` timestamp. The API accepts it immediately with a `202`, holds the message, and dispatches it at the scheduled time.

## Schedule a send

<Tabs>
<Tab title="curl">

```bash
curl -X POST https://api.mailfully.com/v1/emails \
  -H "Authorization: Bearer mf_live_xxxxxxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{
    "from": "orders@mail.acme.com",
    "to": "you@yourcompany.com",
    "subject": "Your order shipped",
    "text": "Order 4212 shipped.",
    "scheduled_at": "2026-07-10T09:00:00Z"
  }'
```

</Tab>
<Tab title="Node SDK">

```typescript
const { data, error } = await mailfully.emails.send({
  from: "orders@mail.acme.com",
  to: "you@yourcompany.com", // replace with an address you own
  subject: "Your order shipped",
  text: "Order 4212 shipped.",
  scheduledAt: "2026-07-10T09:00:00Z",
});
```

</Tab>
</Tabs>

The response is the standard acceptance:

```json
{ "id": "01JZFYK2NAB3C5D7E9F1G3H5J7" }
```

Rules for `scheduled_at`:

- **Format**: an ISO 8601 datetime string, e.g. `2026-07-10T09:00:00Z`.
- **Maximum horizon**: 90 days ahead. Beyond that, the send is rejected with `422 validation_error` (`param: "scheduled_at"`): `scheduled_at cannot be more than 90 days in the future.`
- **No minimum**: a `scheduled_at` in the past or at the current instant is accepted and dispatched immediately. It is never an error.

Templates render at accept, not at dispatch: a scheduled send referencing a template carries the content rendered when the `202` was returned, and later template edits don't touch it. See [Templates](/guides/templates).

## How scheduled sends appear

No separate `scheduled` status exists. A scheduled send is a `queued` message with a future `scheduled_at`, so it shows up in [`GET /v1/emails`](/api-reference/emails/list) under `status=queued`:

```json
{
  "id": "01JZFYK2NAB3C5D7E9F1G3H5J7",
  "to": ["you@yourcompany.com"],
  "from": "orders@mail.acme.com",
  "subject": "Your order shipped",
  "status": "queued",
  "last_event": "queued",
  "recipient_domain": "yourcompany.com",
  "tags": [],
  "created_at": "2026-07-08T14:02:11.000Z",
  "scheduled_at": "2026-07-10T09:00:00.000Z"
}
```

The detail response from [`GET /v1/emails/{id}`](/api-reference/emails/get) has no `status` field; read `last_event` instead, which stays `queued` until dispatch, alongside the `scheduled_at` timestamp.

## Reschedule

`PATCH /v1/emails/{id}` moves a scheduled send to a new time.

<Tabs>
<Tab title="curl">

```bash
curl -X PATCH https://api.mailfully.com/v1/emails/01JZFYK2NAB3C5D7E9F1G3H5J7 \
  -H "Authorization: Bearer mf_live_xxxxxxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{ "scheduled_at": "2026-07-12T09:00:00Z" }'
```

</Tab>
<Tab title="Node SDK">

```typescript
const { data, error } = await mailfully.emails.reschedule(
  "01JZFYK2NAB3C5D7E9F1G3H5J7",
  { scheduledAt: "2026-07-12T09:00:00Z" },
);
```

</Tab>
</Tabs>

A `200` returns the full email detail with the new `scheduled_at`.

Constraints:

- **Only sends scheduled more than 15 minutes ahead can be moved.** A send scheduled 15 minutes out or less (or sent immediately) is already committed to the delivery queue with a fixed delay; a farther-out send sits on a scheduler and can be moved. Attempting to reschedule a near-term send returns:

```json
{
  "error": {
    "type": "validation_error",
    "message": "This send cannot be rescheduled; only sends scheduled more than 15 minutes ahead can be moved.",
    "param": "scheduled_at"
  }
}
```

- **The new time must be strictly in the future** and at most 90 days out. This is stricter than the original send, where a past `scheduled_at` means "send now"; on reschedule a past or current time is a `422 validation_error` (`param: "scheduled_at"`).
- **Only queued sends move.** A canceled send returns `409 conflict` (`A canceled email cannot be rescheduled.`); any other non-queued status returns `409 conflict` (`Only a queued email can be rescheduled.`).

## Cancel

`POST /v1/emails/{id}/cancel` stops a send that hasn't dispatched yet. It works for any `queued` message, scheduled or not — unlike reschedule, it has no 15-minute restriction.

<Tabs>
<Tab title="curl">

```bash
curl -X POST https://api.mailfully.com/v1/emails/01JZFYK2NAB3C5D7E9F1G3H5J7/cancel \
  -H "Authorization: Bearer mf_live_xxxxxxxxxxxx"
```

</Tab>
<Tab title="Node SDK">

```typescript
const { data, error } = await mailfully.emails.cancel("01JZFYK2NAB3C5D7E9F1G3H5J7");
```

</Tab>
</Tabs>

A `200` returns the email detail, now canceled:

```json
{
  "object": "email",
  "id": "01JZFYK2NAB3C5D7E9F1G3H5J7",
  "to": ["you@yourcompany.com"],
  "from": "orders@mail.acme.com",
  "created_at": "2026-07-08T14:02:11.000Z",
  "subject": "Your order shipped",
  "html": null,
  "text": "Order 4212 shipped.",
  "cc": [],
  "bcc": [],
  "reply_to": [],
  "last_event": "canceled",
  "scheduled_at": "2026-07-10T09:00:00.000Z",
  "tags": []
}
```

Cancel semantics:

- **Idempotent.** Canceling an already-canceled send returns `200` again with the canceled email. Retrying is safe.
- **Too late once sending starts.** If the message has left `queued`, cancel returns `409 conflict`: `Only a queued email can be canceled.` If the delivery worker claims it during your request, the `409 conflict` message is `The email is already being sent and can no longer be canceled.`
- **Canceled sends are never billed.** A cancel that lands before dispatch means no delivery attempt and no metering.

## Related

- The `scheduled_at` field on [`POST /v1/emails`](/api-reference/emails/send)
- [`PATCH /v1/emails/{id}`](/api-reference/emails/reschedule) and [`POST /v1/emails/{id}/cancel`](/api-reference/emails/cancel)
- [How sending works](/concepts/how-sending-works) covers the full message lifecycle
