---
title: "Schedule and cancel"
description: "`scheduledAt`, `emails->reschedule`, `emails->update` and `emails->cancel`."
url: "https://openemail.uk/docs/php/emails/schedule"
area: "PHP"
category: "Emails"
---

# Schedule and cancel

`scheduledAt`, `emails->reschedule`, `emails->update` and `emails->cancel`.

## Sending later

**schedule.php**

```
$message = [
    'from' => 'billing@acme.com',
    'to' => 'ada@example.com',
    'subject' => 'Your September invoice',
    'text' => 'Invoice attached.',
];

$client->emails->send([...$message, 'scheduledAt' => 'PT1H']);
$client->emails->send([...$message, 'scheduledAt' => new \DateTimeImmutable('2027-01-01 09:00', new \DateTimeZone('UTC'))]);
$client->emails->send([...$message, 'scheduledAt' => '2027-01-01T09:00:00.000Z']);
```

A `DateTimeInterface`, an ISO 8601 instant as a string, or a duration like `PT1H` or `P2D`. Up to a year out, never in the past. Spreading a message you built earlier into a new array with `scheduledAt` beside it adds the field and leaves the original alone.

> A `DateTimeInterface` is sent as an instant in UTC whatever zone it was made in, so 09:00 in `Europe/London` goes out as the instant it names. A date string with no time, such as `2027-01-01`, is read as midnight UTC on that day, so pass an instant when the hour matters.

## Moving and stopping

**reschedule.php**

```
$message = [
    'from' => 'billing@acme.com',
    'to' => 'ada@example.com',
    'subject' => 'Your September invoice',
    'text' => 'Invoice attached.',
];

$queued = $client->emails->send([...$message, 'scheduledAt' => 'PT1H']);

$client->emails->reschedule($queued['id'], new \DateTimeImmutable('+1 day'));
$client->emails->cancel($queued['id']);
```

Only `queued` and `scheduled` messages can be stopped. Anything further on throws a `ConflictException`, because some of it is already in somebody’s mailbox. Cancelling an already-cancelled message succeeds and changes nothing.

To find what is waiting to go out in a window, list with `status: ['scheduled', 'queued']` and `scheduledFrom:` and `scheduledTo:`, as the calendar of the app does.

## Changing it before it goes

`emails->update` changes a message that has not gone yet: when it goes with `scheduledAt`, what it says with `subject`, `html` and `text`, the address it goes out as with `from`, and who it goes to with `to`, `cc` and `bcc`. Send any of them together, and a field you leave out keeps its value. A recipient list replaces the stored one whole. It is what editing a scheduled message in the calendar of the app does.

**update.php**

```
$updated = $client->emails->update('msg_3f9a1c07d2b84e6a9c5b1f20', [
    'subject' => 'Your September invoice, corrected',
    'to' => ['ada@example.com', 'grace@example.com'],
    'scheduledAt' => new \DateTimeImmutable('2026-10-05 08:00', new \DateTimeZone('UTC')),
]);

echo $updated['status'], ' ', $updated['subject'], ' ', $updated['scheduledAt'], PHP_EOL;
```

> `from` is checked as it is on a send, so it has to be an address the key may send as. A message translated when it was accepted keeps its approved wording, so a new `subject`, `html` or `text` on it is a 409 `translation_locked`, and one that was encrypted before it was scheduled keeps its wording and its recipients. Cancel those and send again instead.

## An undo window instead

**undo_window.php**

```
$message = [
    'from' => 'billing@acme.com',
    'to' => 'ada@example.com',
    'subject' => 'Your September invoice',
    'text' => 'Invoice attached.',
];

$held = $client->emails->send([...$message, 'cancellableForSeconds' => 30]);

echo $held['status'], ' ', $held['cancellableUntil'], PHP_EOL;
```

> A scheduled message is already cancellable until it goes, so the two cannot be combined, and the server refuses it. Use this one for an undo-send window on an immediate message.

## Parameters: scheduling

- `scheduledAt` (DateTimeInterface or string): When to send, on `emails->send`: a `DateTimeInterface`, an ISO 8601 instant as a string, or a duration like `PT1H` or `P2D`. A `DateTimeInterface` is sent as a UTC instant and a string as it is, and a date string with no time means midnight UTC. At least one second in the future and at most 365 days out, with either bound a `validation_error` on `scheduledAt`, and natural language is not accepted, because parsing "next Tuesday" wrongly sends a message at a time that cannot be taken back.
- `cancellableForSeconds` (int): An undo-send window on an IMMEDIATE send: an int from 0 to 900, defaulting to 0. Any value above 0 is refused alongside `scheduledAt`, which is already cancellable until it goes, and a message held this way sits at `queued` rather than `scheduled`. It is the same deferral mechanism with a short delay.
- `$id` (string, required): The `msg_` id, and the first argument to `emails->cancel`, `emails->reschedule` and `emails->update`. They need `emails:send` rather than a scope of their own, and they look the id up within the key’s own workspace, so an id belonging to another one is a `not_found_error` exactly like an id that never existed.
- `$scheduledAt` (DateTimeInterface or string, required): The new time, as the second argument to `emails->reschedule`, read by the same rules and against the same one-year window. It is the only thing `reschedule` changes, and the client sends nothing else. A duration is relative to when the SERVER reads it, so a retried reschedule lands slightly later than the first would have: later, never earlier.
- `apiKey` (string): A named argument on any of the three calls. Acts with this key instead of the client’s.

## Response

`cancel`, `reschedule` and `update` each return the whole message as an array keyed by the API’s camelCase names.

- `object` (string): Always `email`. These calls answer with the whole message rather than an acknowledgement, so nothing has to be fetched again to see what changed. `emails->send` returns this same shape plus `replayed`.
- `id` (string): The `msg_` handle. Stable for the life of the message and the id every other call on it takes.
- `status` (string): `cancelled` after a cancel and `scheduled` after a reschedule, including for a message that was only `queued` behind an undo window, which a reschedule turns into a real schedule. Only `queued` and `scheduled` messages can be moved or stopped. Anything further on throws a `ConflictException` whose `errorCode` is `email_not_cancellable`, because some of it is already in somebody’s mailbox.
- `scheduledAt` (string or null): The ISO 8601 instant the message is due to be dispatched. Set for an undo window as well as for a `scheduledAt` send, since the two are one mechanism, and null on a plain immediate send.
- `cancellableUntil` (string or null): When cancelling stops working, which is the same instant as `scheduledAt` on both deferred paths. null on an immediate send, which has already gone by the time the call returns.
- `sentAt` (string or null): When the message actually left. null while it waits, and null for ever on a cancelled one.
- `messageId` (string or null): The RFC 5322 Message-ID, null until the MIME exists, so always null on a message these calls can act on. Not something to address the API with, and not what a later bounce comes back on either: the sending service rewrites the header on the way out.
- `threadId` (string or null): The thread this message belongs to, taken from the request and rewritten with whatever the transport reports once it sends. null when it is not a reply.
- `transport` (string or null): How the bytes left, and null until dispatch, so null on every message a cancel or a reschedule can return. A test-mode send records `test`, and a transport this package does not name yet may appear, so treat an unknown value as information rather than an error.
- `attempts` (int): How many times dispatch has claimed this row. It goes up with each claim rather than with a successful send, and is 0 for anything still waiting.
- `lastError` (string or null): The last failure recorded against the message, null while nothing has failed. A deferred send whose job could not be queued is written here as `Could not schedule: …` and moved to `failed`, which is the one way a scheduled message stops being cancellable without anybody asking.
- `from` (string): The address the message was authorised to go out as: the `from` that was sent, stored bare and lower-cased. Any display name is dropped here, because the `from:` filter of `emails->list` compares on equality.
