Skip to the documentation
PHP

Schedule and cancel

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

Sending later

schedule.php
$message = [    'from' => '[email protected]',    'to' => '[email protected]',    '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' => '[email protected]',    'to' => '[email protected]',    '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' => ['[email protected]', '[email protected]'],    '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' => '[email protected]',    'to' => '[email protected]',    '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

scheduledAtDateTimeInterface 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.
cancellableForSecondsint
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.
$idstringrequired
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.
$scheduledAtDateTimeInterface or stringrequired
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.
apiKeystring
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.

objectstring
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`.
idstring
The `msg_` handle. Stable for the life of the message and the id every other call on it takes.
statusstring
`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.
scheduledAtstring 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.
cancellableUntilstring 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.
sentAtstring or null
When the message actually left. null while it waits, and null for ever on a cancelled one.
messageIdstring 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.
threadIdstring 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.
transportstring 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.
attemptsint
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.
lastErrorstring 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.
fromstring
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.