Later, only if needed, and not at night.
Scheduled sends you can cancel by your own key, follow-ups that go out only when the inbox item is still unread, and quiet hours.
1
Schedule and cancel
send_at (an ISO time) or delay_seconds, at most the type’s TTL ahead (422 SEND_AT_OUT_OF_RANGE otherwise). Preferences and devices are checked again at release time. A cancellation_key lets you cancel without having stored the notification id; it cancels everything still pending, scheduled or retrying under that key.
// Remind tomorrow at 08:00 UTC unless the user acts first.
await notify.notifications.send(
{
type: "invite.reminder",
recipients: ["user_123"],
data: { team: "Acme" },
send_at: "2026-11-12T08:00:00Z", // or delay_seconds: 3600; at most the type's TTL ahead
cancellation_key: "invite:inv_77",
},
{ idempotencyKey: "invite-reminder:inv_77" },
);
// The user accepted the invite: cancel without having stored our id.
await notify.notifications.cancelByKey({ cancellation_key: "invite:inv_77" });2
Inbox first, email if unread
The pattern most apps want: the inbox at once, push if the user has a device, and an email only if they still have not read the item 15 minutes later. Reading or archiving the item first ends the email step as read_in_inbox. A fallback step without after_unread_seconds runs only when the previous step had nothing to deliver or failed.
await notify.notificationTypes.replaceByKey("comment.created", {
name: "New comment",
category_key: "comments",
ttl_seconds: 86400,
routing: {
always: ["inbox"],
fallback: [
{ channel: "push" }, // straight away, when the user has a device
{ channel: "email", after_unread_seconds: 900 }, // only if the item is still unread 15 minutes later
],
},
throttle: { max: 5, per_seconds: 3600 }, // at most 5 an hour per recipient
templates: [
{ channel: "inbox", locale: "en", body: { title: "{{author}} commented", body: "{{excerpt}}" } },
{ channel: "push", locale: "en", body: { title: "{{author}} commented", body: "{{excerpt}}" } },
{ channel: "email", locale: "en", body: { mail_template_name: "new-comment" } },
],
});3
Quiet hours
A recipient’s own window wins, then the workspace default. A device’s own time zone wins for push. Critical types ignore quiet hours. Users set their own in <NotifyPreferences/>.
// Recipients without their own window get this one (an end before the
// start spans midnight). Push and web push wait; the inbox never does.
await notify.settings.update({
default_timezone: "Europe/Lisbon",
default_quiet_hours: { start: "22:00", end: "08:00" },
});
await notify.recipients.update("user_123", { timezone: "America/New_York", quiet_hours: { start: "23:00", end: "07:00" } });4
Not in the preview yet
Batching and digests (“3 new comments”), topics you can subscribe recipients to, and tenant default preferences are planned. Until then a per-type throttle caps how often one person gets the same type.