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 messages with status="scheduled" and scheduled_at set to the UTC instant.
  • The send-scheduled cron runs every minute and dispatches rows whose scheduled_at <= now().
  • You can cancel with DELETE /api/v1/{emails,sms}/:id while the row is still scheduled.

Precedence

  • scheduled_at wins if both it and scheduled_local are supplied.
  • Missing scheduled_at_tz with scheduled_local returns a 422 validation error.