schedule object to any send and the message is held instead of queued. The response is still 202, with status: "scheduled", the schedule you passed and scheduledFor, the first instant the message can fire.
The schedule object
at carries no offset: it is the time on a clock, and timezone decides whose clock that is.
A fixed time zone
With an IANA zone, the message has one moment. The Worker’s minute cron queues it whenscheduledFor arrives, and from there it behaves like any other send: fan-out, deliveries, retries, all of it described in Delivery and retries.
Each subscriber’s own time zone
Settimezone to "subscriber" and the message reaches each person as their own clock reaches at. So 18:00 means 18:00 in Berlin and 18:00 in Los Angeles, nine hours apart.
$timezone attribute the SDK stamps on the subscriber when it identifies them. Subscribers without one go out with defaultTimezone.
Every minute the cron releases the zones that have just reached at, fanning out to the subscribers in them. A topic or direct send filters on the attribute in Postgres; an expression send narrows its audience to the zones being released. The message reads processing while this happens and its counts grow, so one send spreads over the 26 hours between UTC+14 and UTC−12 before fan-out completes.
A zone is never sent twice. The message records the zones it has fanned out in
scheduledZones, a batch that dies is retried on the next tick, and deliveries are unique per subscription.When the moment has already passed
A moment that has already passed everywhere on earth is a400 schedule_in_past.
A "subscriber" schedule is different. If the moment has passed in some zones but not others, the subscribers in those zones are sent to right away and the rest are waited for. Nobody in Berlin is skipped just because you created the message after 18:00 there.
Invalid schedules
A400 invalid_schedule covers everything that cannot be resolved to a moment:
- A time that is not on the calendar, such as
2026-02-30T10:00orT24:00. - An unknown zone name.
- A
defaultTimezoneoutside a"subscriber"schedule, where it has no meaning.
Expiry on a scheduled message
ttlSeconds counts from the last moment the message can go out, not from when you created it. A message scheduled a week ahead with a one hour TTL is still valid for an hour after it fires, not expired before it ever left.
Canceling
POST /v1/messages/:id/cancel stops a message that has not been sent. It needs the messages:send scope.
- A message still
scheduledbecomescanceledwithcanceledAtset, and nothing is delivered. - A
"subscriber"schedule part way through fan-out keeps the deliveries it already made, stops releasing further zones, and finishes ascompletedwithcanceledAtset. - Anything else is a
400 message_not_cancelable.
message.created carries scheduledFor, and message.canceled carries the status the message left.
Next
Sending messages
Targeting, content fields, expiry and idempotency.
Delivery and retries
What happens once a scheduled message is released.
Subscribers
Where the
$timezone attribute comes from.Workflows
For anything recurring or triggered rather than a single moment.