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.