Schedule and cancel
`scheduledAt`, `emails.reschedule`, `emails.update` and `emails.cancel`.
Sending later
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: Time.utc(2027, 1, 1, 9))client.emails.send(message, scheduledAt: "2027-01-01T09:00:00.000Z")A Time or a DateTime, an ISO 8601 instant as a String, or a duration like PT1H or P2D. Up to a year out, never in the past. A keyword argument beside the Hash adds the field to a message you built earlier.
A Ruby Date is sent as a bare date such as 2027-01-01, which the API reads as midnight UTC on that day. Pass a Time, such as Time.utc(2027, 1, 1, 9), when the hour matters.
Moving and stopping
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], Time.now + 86_400)client.emails.cancel(queued[:id])Only queued and scheduled messages can be stopped. Anything further on raises an OpenEmail::ConflictError, 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 scheduled_from: and scheduled_to:, 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.
updated = client.emails.update( "msg_3f9a1c07d2b84e6a9c5b1f20", subject: "Your September invoice, corrected", to: ["[email protected]", "[email protected]"], scheduledAt: Time.utc(2026, 10, 5, 8)) puts updated[:status], updated[:subject], updated[:scheduledAt]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
message = {from: "[email protected]", to: "[email protected]", subject: "Your September invoice", text: "Invoice attached."} held = client.emails.send(message, cancellableForSeconds: 30) puts held[:status], held[:cancellableUntil]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
scheduledAtTime, DateTime or String- When to send, on `emails.send`: a Time or a DateTime, an ISO 8601 instant as a String, or a duration like `PT1H` or `P2D`. A Time or a DateTime is sent as a UTC instant, a String as it is, and a Ruby Date as a bare date that 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.
cancellableForSecondsInteger- 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.
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.
scheduled_atTime, DateTime 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.
api_keyString- Acts with this key instead of the client’s, on any of the three calls.
Response
cancel, reschedule and update each return the whole message as a Hash with Symbol keys.
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 is a `conflict_error` with code `email_not_cancellable`, because some of it is already in somebody’s mailbox.
scheduledAtString or nil- 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 nil on a plain immediate send.
cancellableUntilString or nil- When cancelling stops working, which is the same instant as `scheduledAt` on both deferred paths. nil on an immediate send, which has already gone by the time the call returns.
sentAtString or nil- When the message actually left. nil while it waits, and nil for ever on a cancelled one.
messageIdString or nil- The RFC 5322 Message-ID, nil until the MIME exists, so always nil 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 nil- The thread this message belongs to, taken from the request and rewritten with whatever the transport reports once it sends. nil when it is not a reply.
transportString or nil- How the bytes left, and nil until dispatch, so nil on every message a cancel or a reschedule can return. A test-mode send records `test`, and a transport this gem does not name yet may appear, so treat an unknown value as information rather than an error.
attemptsInteger- 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 nil- The last failure recorded against the message, nil 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.