Scheduled Deliveries
Accept a send now, deliver it later — and know exactly what happens in between.
POST carrying scheduled_at returns 402 with a feature_key naming the limit. See pricing to enable it.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.
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"
}
}'{
"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.
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.
scheduled → queued → sent. 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.
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.
scheduled until the sweep dispatches it.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.
curl -X DELETE "https://relaygrid.dev/api/v1/messages/87/schedule" \
-H "Authorization: Bearer YOUR_API_KEY"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)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 APIBroadcast to a segment later
A scheduled broadcast defers every fanned-out message individually.
Segments & Broadcasts