Scheduled sends with timezone
/api/v1/emails and /api/v1/sms accept three mutually exclusive ways to schedule a send:
| Shape | Example |
|---|---|
| No scheduling fields | (sends immediately) |
| ISO UTC | "scheduled_at": "2026-05-01T13:00:00.000Z" |
| Local wall-clock + IANA zone | "scheduled_local": "2026-05-01 09:00", "scheduled_at_tz": "America/New_York" |
When scheduled_local is supplied, scheduled_at_tz is required. resolveScheduledAt (src/lib/api/schedule.ts) converts the wall-clock to the correct UTC instant by measuring the offset the target timezone has at that instant using Intl.DateTimeFormat.
Cross-DST example: 2026-03-08 02:30 America/New_York falls inside the spring-forward gap. The helper resolves it to the first existing instant (handled consistently by Intl), so the send doesn't silently drop.
Behavior
- Scheduled rows land in
messageswithstatus="scheduled"andscheduled_atset to the UTC instant. - The
send-scheduledcron runs every minute and dispatches rows whosescheduled_at <= now(). - You can cancel with
DELETE /api/v1/{emails,sms}/:idwhile the row is stillscheduled.
Precedence
scheduled_atwins if both it andscheduled_localare supplied.- Missing
scheduled_at_tzwithscheduled_localreturns a 422 validation error. scheduled_localmust be a full date and time,YYYY-MM-DD HH:MMorYYYY-MM-DD HH:MM:SS(aTseparator also works). A bare time such as"09:00", or a date or time that does not exist (2027-02-30, month13,24:00, minute75), returns422 VALIDATION_ERRORwith the messageinvalid scheduled_local. It is never rolled forward into the next day or month.