Schedule and cancel
`scheduledAt`, `emails.reschedule` and `emails.cancel`.
Sending later
from datetime import datetime, timezone from openemail import openemailfrom openemail.types import EmailSend message: EmailSend = {'from': sender, 'to': recipient, 'subject': subject, 'text': text} openemail.emails.send({**message, 'scheduledAt': 'PT1H'})openemail.emails.send({**message, 'scheduledAt': datetime(2027, 1, 1, 9, 0, tzinfo=timezone.utc)})openemail.emails.send({**message, 'scheduledAt': '2027-01-01T09:00:00.000Z'})A datetime, an ISO-8601 instant, or a duration like PT1H / P2D. Up to a year out, never in the past.
A datetime is sent in UTC, and a naive one is read as this machine’s local time first, the way astimezone reads it, so pass an aware one when the zone matters. A timedelta is not accepted: add it to datetime.now(timezone.utc), or send a duration such as PT1H.
Moving and stopping
from datetime import datetime, timedelta, timezone from openemail import openemailfrom openemail.types import EmailSend message: EmailSend = {'from': sender, 'to': recipient, 'subject': subject, 'text': text} queued = openemail.emails.send({**message, 'scheduledAt': 'PT1H'}) openemail.emails.reschedule(queued['id'], datetime.now(timezone.utc) + timedelta(days=1))openemail.emails.cancel(queued['id'])Only queued and scheduled messages can be stopped; anything further on is a conflict_error, because some of it is already in somebody's mailbox. Cancelling an already-cancelled message succeeds and changes nothing.
An undo window instead
from openemail import openemailfrom openemail.types import EmailSend message: EmailSend = {'from': sender, 'to': recipient, 'subject': subject, 'text': text} openemail.emails.send({**message, 'cancellableForSeconds': 30})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
scheduledAtdatetime | str- When to send, on `emails.send`: a `datetime`, an ISO-8601 instant, or a duration like `PT1H` or `P2D`, which the client renders to a string for the wire. 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 integer 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.
idstrrequired- The `msg_…` id, and the first argument to both `emails.cancel` and `emails.reschedule`. Both need `emails:send` rather than a scope of their own, and both resolve 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.
scheduled_atdatetime | strrequired- The new time, as the second argument to `emails.reschedule`, parsed by the same rules and against the same one-year window, and the only thing the underlying `PATCH /emails/{id}` will change. 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.
Response: EmailResource
objectLiteral['email']- Always `email`. Both calls answer with the whole message rather than an acknowledgement, so nothing has to be re-fetched to see what changed; `emails.send` returns this same shape plus `replayed`.
idstr- The `msg_…` handle. Stable for the life of the message and the id every other call on it takes.
statusEmailStatus- `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 is a `conflict_error` with code `email_not_cancellable`, because some of it is already in somebody's mailbox.
scheduledAtstr | None- The ISO 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.
cancellableUntilstr | None- 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.
sentAtstr | None- When the message actually left. Null while it waits, and null for ever on a cancelled one.
messageIdstr | None- The RFC 5322 Message-ID, null until the MIME exists, so always null on a message these two 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.
threadIdstr | None- 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.
transportEmailTransport | str | None- 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 the union stays open so a transport this SDK does not yet name is not a breaking change.
attemptsint- How many times dispatch has claimed this row. It is incremented by the claim rather than by a successful send, and is 0 for anything still waiting.
lastErrorstr | None- The last failure recorded against the message, null while nothing has failed. A deferred send whose job could not be enqueued 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.
fromstr- 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 `emails.list`'s `from_` filter compares on equality.