RelayGrid logoRelayGrid

Scheduled Deliveries

Accept a send now, deliver it later — and know exactly what happens in between.

Requires Pro or the Scheduled Notifications add-on. Scheduling is included with the Pro plan and the $9/mo Scheduled Notifications add-on. Without either, a POST carrying scheduled_at returns 402 with a feature_key naming the limit. See pricing to enable it.
1

Schedule a send

Add scheduled_at to any send — a single message or a broadcast — as an ISO 8601 timestamp with an explicit UTC offset (for example 2026-09-01T09:00:00-07:00 or 2026-09-01T09:00:00Z). A naive timestamp with no offset, a malformed string, or a time in the past all return 422. Both SDKs enforce the same shape: the Ruby gem rejects a bare Date, and the Node library serializes a Date with an offset while passing a raw string through for the server to validate.

Shell
curl -X POST "https://relaygrid.dev/api/v1/messages" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "message": {
      "message_template_name": "renewal_reminder",
      "user": { "external_user_id": "user_12345" },
      "scheduled_at": "2026-09-10T09:00:00-07:00"
    }
  }'
Response201 Created
{
  "message": {
    "id": 87,
    "scheduled_at": "2026-09-10T16:00:00.000Z",
    "deliveries": [
      { "id": 201, "channel_type": "email", "status": "scheduled" }
    ]
  }
}

The deliveries exist immediately with status scheduled — a 201 means the schedule was accepted, not that the message went out.

2

The two-stage lifecycle

Scheduling is two stages, not one. Creating the send puts every delivery in scheduled and emits delivery.scheduled. When the time comes, the DispatchDueScheduledMessagesJob sweep claims the due messages, moves their deliveries to queued, emits delivery.queued, and the normal send pipeline takes over from there.

sweep when duesendcancel:DELETE /messages/:id/schedulequota spent at send time:delivery fails insteadscheduleddelivery.scheduledqueueddelivery.queuedsenthanded to providercanceledbefore the sweep runsfailedfriendly_error_message set
A scheduled send moves through scheduledqueuedsent. Cancelling is only possible from scheduled; an over-quota account fails the delivery when the sweep dispatches it.

Dispatch-time reconciliation. The sweep re-checks the world as it is at dispatch, not as it was at schedule time. A recipient deleted in the meantime, a template left with no live channels, or a channel removed from the template cancels the delivery with a reason — recipient_deleted, no_live_channels, or channel_deleted_or_detached. It works the other way too: a channel added to the template after you scheduled gets a delivery at dispatch time.

Quota is enforced at send time, not schedule time. Your plan's monthly allowance is re-checked when the sweep dispatches the message. Over quota, the delivery lands in failed with a friendly_error_message — so a schedule accepted today can still fail next month. Watch delivery.failed webhooks or poll rather than trusting the schedule response alone.
Delivery list showing a delivery in scheduled status
A scheduled send shows up in the recipient's delivery list with status scheduled until the sweep dispatches it.
3

Cancel before the sweep

DELETE /api/v1/messages/:id/schedule withdraws a message that is still pending dispatch. Once the sweep has run there is no window to cancel — the endpoint returns 422 for a message that already dispatched or was already canceled, and 404 for an unknown message.

Shell
curl -X DELETE "https://relaygrid.dev/api/v1/messages/87/schedule" \
  -H "Authorization: Bearer YOUR_API_KEY"
Ruby
result = RelayGrid.client.notify(
  user:         { id: 'user-123' },
  template:     'renewal_reminder',
  scheduled_at: Time.now + 3 * 24 * 60 * 60
)

# Cancel before it dispatches
RelayGrid.client.messages.cancel_schedule(result.message_id)
Node.js
const result = await relaygrid.notify({
  user: { id: "user-123" },
  template: "renewal_reminder",
  scheduledAt: "2026-09-10T09:00:00-07:00",
});

// Cancel before it dispatches
await relaygrid.messages.cancelSchedule(result.messageId);

Statuses a scheduled send can reach. scheduled, queued, sent, delivered, failed, and canceled (with a reason — your own cancel reads requested).

Next Steps

Messages reference

The full scheduled_at rules, cancel responses, and every message parameter.

Messages API

Broadcast to a segment later

A scheduled broadcast defers every fanned-out message individually.

Segments & Broadcasts