Push notifications

Scheduling

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" }
}
FieldRequiredNotes
nameyesUp to 120 characters.
enablednoDefault true: the rule is live unless you pass false.
contentModeno"fixed" (default) or "ai". See AI-written notifications.
title, bodyfixed copyUp to 200 and 4,000 characters.
appDescription, instructions, language, examplesAI copyThe brief.
frequencyyes"once", "daily" or "weekly".
dateonce"YYYY-MM-DD", in the rule's timezone.
dayOfWeekweekly0 (Sunday) to 6 (Saturday).
timeOfDayyes24-hour "HH:MM".
timezoneyesIANA zone such as America/New_York, up to 60 characters.
audienceKindno"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.
audienceRefgroup, subThe group key or the subscriber id, up to 256 characters.
environmentno"production" (default), "staging" or "development".
skipIfActiveWithinDaysno1–365 or null. Not applied to all, group or sub rules (skipIfActiveApplies is false).
pushDatanoA JSON object (or JSON string) delivered with every send, up to 16 KiB.
bigPictureURLnoAbsolute 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

  • timeOfDay is read in the rule's own timezone, 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.

statusMeaning
sentThe push went out. audienceCount is the devices targeted, title and body the copy sent, and sendId links it to the inbox and analytics.
heldQuiet hours held it. A later run records the send.
emptyNobody to send to. Nothing was sent.
blockedA gate refused it: an expired trial, a group audience on Premium, or a disabled environment. detail says which.
errorThe send failed, for example devices matched but no transport accepted it (missing credentials).
failedAn AI rule's copy couldn't be written. Nothing was sent.
missedA one-off whose time passed while the scheduler wasn't running. Nothing was sent.
sendingClaimed and in flight. outcomeUnknown: true when it never settled.

Errors

StatusCodeMeaning
400bad_scheduleA rule field is missing or invalid. The message names it.
400schedule_too_soonA one-off less than 2 minutes away.
400invalid_environment, environment_disabledNot one of the three environments, or switched off for this app.
400bad_app_id, bad_rule_id, bad_settingsA malformed id, or invalid quiet hours.
404app_not_found, rule_not_foundThe app isn't one this account can access, or the rule isn't on it.
409rule_completedThe 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.