# Send it later, and change your mind until then

Add scheduled\_at to a send and the email waits, up to 30 days. Move it or cancel it by id until its scheduled time, and it goes out under the suppressions, thread and tracking of that moment.

[Get started](/signup)
[Documentation](https://docs.aiinbx.com/guides/scheduling)

Example: a lesson reminder is sent with scheduled\_at 2026-10-07T18:00:00+02:00 and comes back with status scheduled. Three days later the lesson moves, so PATCH /emails/eml\_7hq2 sets scheduled\_at to 2026-10-08T18:00:00+02:00. Reading the email back shows its events: scheduled, scheduled again for the move, then queued at 16:00 UTC on 8 October when it was composed, sent and delivered.

Timezones

## Whose nine o'clock

scheduled\_at is an instant, so it carries its offset. Nine in Berlin, New York and Tokyo are three different values, and a time with no zone is refused instead of guessed.

[Timezones](https://docs.aiinbx.com/reference/conventions)

Rescheduling

## Move it without resending it

PATCH the email with a new scheduled\_at. It keeps its id, so whatever your app stored still points at it. Once it has begun sending, the call answers 409.

[Rescheduling](https://docs.aiinbx.com/guides/scheduling#rescheduling)

Cancelling

## Cancelled means it stays cancelled

Cancel is terminal. The email keeps its idempotency key, so a retry of the original request returns the cancelled email instead of sending it, and it gives back its place in your allowance.

[Cancelling](https://docs.aiinbx.com/guides/scheduling#canceling)

At send time

## Written for the minute it leaves

A scheduled email is composed again when its time comes, and what goes out is that version. Suppressions, the thread it answers, the domain's tracking and your sending hours are read then, not when you called.

[At send time](https://docs.aiinbx.com/guides/scheduling#recomposed-at-release)

| Depends on | 26 September, when it was scheduled | Thursday 18:00, when it leaves |
| --- | --- | --- |
| Recipients | Mia, with her father on cc | His address hard-bounced on Wednesday, so he is dropped and Mia still gets it |
| Thread | Sent into the booking's thread, after the confirmation | Mia answered on Wednesday, and the reminder replies to her message |
| Tracking | The domain tracked no opens | Opens were switched on since, so this one is tracked, unless the send said otherwise |
| Hours | No window in the way | A sending-hours rule closed at that minute holds it until the window opens |

## The rules, in one place

-   How far ahead: Up to 30 days from the moment of the call. A reschedule counts its 30 days from when you make it.
-   The format: An RFC 3339 timestamp with an offset or a Z. Without one it is rejected with invalid\_request.
-   A time already past: Sends straight away, and comes back queued rather than scheduled.
-   What can change: The time, and nothing else. For a new subject, body or recipient, cancel and schedule a fresh one.
-   Where it works: emails.send, threads.reply and threads.forward all take scheduled\_at, and a scheduled reply is threaded when it leaves.
-   Who can move it: A key with the sending or full scope. A read key can list the queue but not touch it.

## A time on one email, or hours for all of them

scheduled\_at belongs to one message: this reminder, not before Thursday at six. Sending hours are a rule over the mail that leaves, like weekdays nine to five in a timezone you pick, and they hold any mail they cover that arrives outside them.

The two stack. A scheduled email is the earliest a message may go, and a closed window can still make it wait. For a per-message instant, use scheduled\_at; for a standing policy about when your product is allowed to email people, write an hours rule.

[Sending hours](/features/sending-hours)

In your code

## A reminder that follows the booking

Store the email id next to the thing it is about. When the booking moves, move the email; when it is called off, cancel it. There is no job runner, queue table or cron of your own.

Step 1, emails.send: the time goes in with its offset.

```
const email = await aiinbx.emails.send({
  from: "Yourapp <lessons@mail.yourapp.com>",
  to: "mia.okafor@gmail.com",
  subject: "Your lesson tomorrow at 16:00",
  text: "See you at 16:00 — reply to move it.",
  scheduled_at: "2026-10-07T18:00:00+02:00",
})

await lessons.update(lessonId, { reminderId: email.id })
```

Step 2, emails.reschedule: the lesson moved.

```
await aiinbx.emails.reschedule(lesson.reminderId, {
  scheduled_at: "2026-10-08T18:00:00+02:00",
})
```

Step 3, emails.cancel: the lesson was called off.

```
const canceled = await aiinbx.emails.cancel(lesson.reminderId)
canceled.status // "canceled"
```

Step 4, emails.list: everything still waiting.

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

## Questions

### How far in advance can I schedule an email?

Up to 30 days from the moment of the call; a later scheduled\_at is rejected with invalid\_request. Rescheduling applies the same 30 days from when you move it. For anything further out, keep the date in your own app and schedule the send once it is inside the window.

### Can I cancel a scheduled email?

Yes, with POST /emails/{id}/cancel while its status is still scheduled. The email becomes canceled and is never sent, and it no longer counts against your allowance. Once it has begun sending, the call returns 409 not\_scheduled.

### Can I change the content of a scheduled email?

No, only its time: PATCH /emails/{id} takes a new scheduled\_at and nothing else. To change the subject, body or recipients, cancel it and schedule a new one. What does change on its own is decided at send time: suppressed recipients are dropped and a reply answers the latest message on its thread.

### How do I send an email at 9am in the recipient's timezone?

Work out 9am in their zone and pass it with its offset, for example 2026-10-05T09:00:00-04:00 for New York in October, as one send per recipient. A timestamp without an offset is refused. If every email to a region should wait for its working hours, a sending-hours rule does that without a time on each send.

### What happens if scheduled\_at is in the past?

The email is sent straight away, exactly as if the field had been left out, and the response says queued rather than scheduled.

### Do I need a cron job to send emails later?

No. The API holds the email and releases it at its time, so your code makes one call and keeps the id it gets back. Nothing of yours has to be running at the moment it goes.

### Which webhooks fire for a scheduled email?

The same as for any send, once it leaves: email.sent, then email.delivered, email.bounced or the others. Scheduling, moving and cancelling are answered in the response to your own call and recorded in the email's events timeline, without a webhook of their own.

Keep reading

## More on building with email

-   [Send email APISend email from your domain or your users' own Gmail and Outlook with one API call. Opt-outs and pacing are checked first, and replies land on the thread.ThreadingMailboxesPacingSuppressions](/solutions/send-email-api)
-   [Email open trackingTrack email opens and clicks per domain or per send. Apple privacy prefetches and security scanner clicks arrive flagged as bots, each with its reason.Threading](/solutions/email-open-tracking)
-   [SPF DKIM DMARC checkCheck a customer's SPF, DKIM and DMARC records over an API. Live lookups at their own nameservers name the record that is wrong and the change that fixes it.Wildcard domains](/solutions/spf-dkim-dmarc-check)

-   [ThreadingOne conversation, however it comes back.](/features/threading)
-   [Sending hoursNever email at 3am, wherever they are.](/features/sending-hours)
-   [SuppressionsUnsubscribes honoured on every send.](/features/suppressions)

[All solutions](/solutions)

## Give your product an inbox. The hard parts are already handled.

[Get started](/signup)
