← Back to blog

Scheduled Email Sending: Deliver at the Right Moment in Any Time Zone

Rovela's scheduled_at field queues email for delivery at a future time, with support for rescheduling and cancellation. This article walks through the full flow and shows how to compute send times in the recipient's time zone.

Updated October 13, 2026
Scheduled Email Sending: Deliver at the Right Moment in Any Time Zone

Plenty of email should not go out the moment your code runs: a reminder three days before a trial ends, a weekly report that lands at 9 a.m. in the user's time zone, a notice an hour before an event. The usual answer is a homegrown cron job that scans a table and calls the send API on time. It works, but it is one more table, one more cron and one more retry path to maintain.

Rovela's send endpoint has scheduling built in. Add scheduled_at and the email enters the scheduled state and is delivered when the time comes. Until then you can move it or cancel it.

Schedule an email

scheduled_at is an ISO 8601 timestamp. Include an offset, or just use UTC:

curl https://api.rovela.dev/emails \
  -H "Authorization: Bearer $ROVELA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "from": "Acme <hello@yourdomain.com>",
    "to": "user@example.com",
    "subject": "3 days left in your trial",
    "html": "<p>Your trial ends in 3 days. Upgrade now to keep your data.</p>",
    "scheduled_at": "2026-10-20T01:00:00Z"
  }'

You get an email ID back exactly as with an immediate send. GET /emails/:id reports the status as scheduled. With a webhook configured you receive email.scheduled, then email.sent and email.delivered once it goes out.

Every entry in POST /emails/batch can carry its own scheduled_at, so one request can queue a set of emails for different times.

Reschedule

The user moved their booking, or the event slipped. Update the time with PATCH /emails/:id:

curl -X PATCH https://api.rovela.dev/emails/$EMAIL_ID \
  -H "Authorization: Bearer $ROVELA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "scheduled_at": "2026-10-21T01:00:00Z" }'

Only an email still in scheduled can be moved. One that has already gone out returns "email not found or is not scheduled".

Cancel

The user upgraded before the reminder fired, so the reminder should not go:

curl -X POST https://api.rovela.dev/emails/$EMAIL_ID/cancel \
  -H "Authorization: Bearer $ROVELA_API_KEY"

Success returns {"object":"email","id":"...","cancelled":true}. Both scheduled emails and queued ones that have not been sent yet can be cancelled; one already handed to the receiving server cannot.

In practice: 9 a.m. in the user's time zone

Your server thinks in UTC; your user thinks in local time. Get the conversion right when computing the send time:

// "Tomorrow 09:00" in the user's time zone, as a UTC ISO string
function nextNineAm(timeZone) {
  const now = new Date()
  const local = new Date(now.toLocaleString('en-US', { timeZone }))
  const offset = local.getTime() - now.getTime()
  const target = new Date(local)
  target.setDate(target.getDate() + 1)
  target.setHours(9, 0, 0, 0)
  return new Date(target.getTime() - offset).toISOString()
}

await fetch('https://api.rovela.dev/emails', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.ROVELA_API_KEY}`,
    'Content-Type': 'application/json',
    'Idempotency-Key': `weekly-report-${user.id}-${weekId}`,
  },
  body: JSON.stringify({
    from: 'Acme <reports@yourdomain.com>',
    to: user.email,
    subject: 'Your weekly report',
    html: renderReport(user),
    scheduled_at: nextNineAm(user.timeZone),
  }),
})

Things to get right:

  • Store the email ID. You need it to cancel or reschedule, for example in a reminder_email_id column on the subscription.
  • Add an idempotency key. Your scheduling job can rerun too; a key like weekly-report-<user>-<week> keeps a rerun from queuing a second copy.
  • Content is fixed when you schedule. The body is rendered at request time, not at send time. A number like "3 days left" has to be computed for the send moment, or scheduled closer to it.

When not to schedule

Scheduling suits email whose time is known and whose content can be built ahead. If the content depends on state at the moment of sending, such as stock levels or a balance, have your own system look it up and send at that moment instead.

See the Send Email API reference for the request format and the batch and scheduled sending example for scheduling in bulk.