Create one-off, daily and weekly rules that send fixed or AI-written pushes at a time in their own timezone.
Routes
GET /api/apps/:appId/schedules — rules + quiet hours
GET /api/apps/:appId/schedules/upcoming — the next sends
POST /api/apps/:appId/schedules — create a rule
PATCH /api/apps/:appId/schedules/:ruleId — update / pause / resume
DELETE /api/apps/:appId/schedules/:ruleId — delete a rule
GET /api/apps/:appId/schedules/:ruleId/runs — run history
GET /api/apps/:appId/schedules/settings — quiet hours
PUT /api/apps/:appId/schedules/settings — set quiet hours
POST /api/apps/:appId/schedules/ai-sample — preview AI copy
These are account routes. Use a dashboard session or an MCP access token (Authorization: Bearer nnmcp_…); an app token answers 403 with a plain-text message. Any member of the app's team may manage rules, and the dashboard's Push notifications → Scheduling tab manages the same ones.
Create a rule
{
"name": "Harvest bake sale",
"enabled": true,
"title": "Bake sale this Saturday",
"body": "Pies, bread and cookies in the church hall from 9:00. Everything goes to the food pantry.",
"frequency": "once",
"date": "2026-10-03",
"timeOfDay": "08:00",
"timezone": "America/New_York",
"audienceKind": "group",
"audienceRef": "church-newsletter",
"pushData": { "url": "/events/bake-sale" }
}
| Field | Required | Notes |
|---|---|---|
name | yes | Up to 120 characters. |
enabled | no | Default true: the rule is live unless you pass false. |
contentMode | no | "fixed" (default) or "ai". See AI-written notifications. |
title, body | fixed copy | Up to 200 and 4,000 characters. |
appDescription, instructions, language, examples | AI copy | The brief. |
frequency | yes | "once", "daily" or "weekly". |
date | once | "YYYY-MM-DD", in the rule's timezone. |
dayOfWeek | weekly | 0 (Sunday) to 6 (Saturday). |
timeOfDay | yes | 24-hour "HH:MM". |
timezone | yes | IANA zone such as America/New_York, up to 60 characters. |
audienceKind | no | "all" (default): every registered device in the rule's environment. "group": every device in a group, manual or always-on (lapsed, active, new). "sub": every device under one subscriber id. |
audienceRef | group, sub | The group key or the subscriber id, up to 256 characters. |
environment | no | "production" (default), "staging" or "development". |
skipIfActiveWithinDays | no | 1–365 or null. Not applied to all, group or sub rules (skipIfActiveApplies is false). |
pushData | no | A JSON object (or JSON string) delivered with every send, up to 16 KiB. |
bigPictureURL | no | Absolute https:// image URL, up to 2,000 characters. |
The 201 response is the saved rule (id, cadence, nextFireIso, lastRun and, for AI rules, ai) and a timing hint comparing the next send with the app's best send times. Each occurrence is an ordinary send, with an inbox entry per device and analytics.
PATCH takes only what changes. Every other field keeps its stored value, { "enabled": false } pauses a rule, and null clears pushData, bigPictureURL, skipIfActiveWithinDays or instructions. DELETE removes the rule and its run history; what it already sent stays in inboxes and analytics.
Timing
timeOfDayis read in the rule's owntimezone, daylight saving included.- A one-off must be at least 2 minutes away when you create it, move it or switch it back on (
400 schedule_too_soon). Once it has gone out, or was missed, it's complete and can't be changed (409 rule_completed). - A daily or weekly rule created, resumed or rescheduled after today's time first sends at its next occurrence (
nextFireIso). - The runner ticks every minute, and sends an occurrence up to 15 minutes late if the server was busy or restarting.
Quiet hours
One window per app. An occurrence inside it, read in the rule's timezone, is held and sent when the window ends. One-offs are held too; send-now pushes never are. Set it with PUT /api/apps/:appId/schedules/settings:
{ "quietHoursEnabled": true, "quietHoursStart": "22:00", "quietHoursEnd": "07:00" }
The default window is 22:00–07:00. It can cross midnight, and start and end must differ. A rule's heldByQuietHours and effectiveFireIso say when a held send will go out.
At most once
Every occurrence is sent at most once, even across a restart. The runner claims it in the database before it sends, so a second runner or a restart skips it, and each send carries the idempotency key rule:<ruleId>:<occurrence>, so no device gets a second inbox entry for it.
A run still sending after 30 minutes shows outcomeUnknown: true and is never resent. A blocked or failed send isn't retried: the rule continues at its next occurrence.
Run statuses
GET …/schedules/:ruleId/runs returns a rule's 20 most recent runs, newest first.
status | Meaning |
|---|---|
sent | The push went out. audienceCount is the devices targeted, title and body the copy sent, and sendId links it to the inbox and analytics. |
held | Quiet hours held it. A later run records the send. |
empty | Nobody to send to. Nothing was sent. |
blocked | A gate refused it: an expired trial, a group audience on Premium, or a disabled environment. detail says which. |
error | The send failed, for example devices matched but no transport accepted it (missing credentials). |
failed | An AI rule's copy couldn't be written. Nothing was sent. |
missed | A one-off whose time passed while the scheduler wasn't running. Nothing was sent. |
sending | Claimed and in flight. outcomeUnknown: true when it never settled. |
Errors
| Status | Code | Meaning |
|---|---|---|
400 | bad_schedule | A rule field is missing or invalid. The message names it. |
400 | schedule_too_soon | A one-off less than 2 minutes away. |
400 | invalid_environment, environment_disabled | Not one of the three environments, or switched off for this app. |
400 | bad_app_id, bad_rule_id, bad_settings | A malformed id, or invalid quiet hours. |
404 | app_not_found, rule_not_found | The app isn't one this account can access, or the rule isn't on it. |
409 | rule_completed | The one-off already went out or was missed. |
MCP tools
Every route is also an MCP tool: list_schedule_rules, create_schedule_rule, update_schedule_rule, delete_schedule_rule, set_quiet_hours, generate_ai_schedule_sample and more. Tools take snake_case arguments and times like "9:00 AM". create_schedule_rule requires enabled, because an enabled rule sends real pushes.