Notifications API
Inbox and Delivery Controls
Read notifications, tune delivery, register devices, and shape inbox rules
These routes power the signed-in notification inbox, delivery preferences, push-device registry, filters and rules, mute scopes, and thread-level actions.
Authentication and Permissions
- All routes on this page require authentication.
- Everything is scoped to the authenticated user.
filtersandrulesare parallel route families for the same underlying concept in the current controller surface.- People-group invite notifications use the same inbox, thread, mute, and catalog surfaces as existing calendar notifications.
- Routes that create or mutate inbox rules (
POST/PATCH/DELETE /filters,POST/PATCH/DELETE /rules) andPUT /mode-packare additionally gated by@RequireFeature('notification_center')— whenENABLE_SUBSCRIPTIONS=true, the caller's plan must include thenotification_centerfeature or these return 403FEATURE_NOT_ENTITLED. Read-only routes (GET /filters,GET /rules,GET /mode-pack) are not gated. GET /:idis registered last in the controller specifically so it doesn't swallow the literal routes above it (catalog,scopes,preferences,digests,mode-pack,filters,rules, etc.) — this only matters if you're extending the controller, not for API consumers.
Endpoint Reference
Inbox and Delivery
| Method | Path | Purpose | Request or query | Auth | Source |
|---|---|---|---|---|---|
GET | /api/notifications | List notifications. | Query: unreadOnly,archived,threadId,afterCursor | JWT or user API key | notifications/notifications.controller.ts |
GET | /api/notifications/:id | Get one notification's full detail (priority class, score, routing decision, ack state, data/metadata). Registered last so it can't shadow the literal routes below it. | Path: id | JWT or user API key | notifications/notifications.controller.ts |
PATCH | /api/notifications/:id/read | Mark one notification read. | Path: id | JWT or user API key | notifications/notifications.controller.ts |
PATCH | /api/notifications/:id/unread | Mark one notification unread. | Path: id | JWT or user API key | notifications/notifications.controller.ts |
POST | /api/notifications/:id/act | Record an inline lifecycle action (confirm/snooze/delegate/claim/done/undo/ack) on one notification. | Path: id. Body: actionType,payload? | JWT or user API key | notifications/notifications.controller.ts |
POST | /api/notifications/read-all | Mark all notifications read. | None | JWT or user API key | notifications/notifications.controller.ts |
GET | /api/notifications/catalog | Read the notification catalog. | None | JWT or user API key | notifications/notifications.controller.ts |
GET | /api/notifications/scopes | Read available scopes for a type. | Query: type | JWT or user API key | notifications/notifications.controller.ts |
GET | /api/notifications/preferences | Read delivery preferences. | None | JWT or user API key | notifications/notifications.controller.ts |
PUT | /api/notifications/preferences | Replace delivery preferences. | Body: preferences | JWT or user API key | notifications/notifications.controller.ts |
GET | /api/notifications/digests | List past digest summaries (messages with eventType: 'digest.summary'). | None | JWT or user API key | notifications/notifications.controller.ts |
POST | /api/notifications/preview | Preview what a digest would currently contain, without sending or persisting anything. | None | JWT or user API key | notifications/notifications.controller.ts |
GET | /api/notifications/mode-pack | Read the caller's Mode Pack selection and feedback tuning. | None | JWT or user API key | notifications/notifications.controller.ts |
PUT | /api/notifications/mode-pack | Select (or clear) a Mode Pack; re-seeds that pack's default rules. | Body: modePack | JWT or user API key, notification_center feature | notifications/notifications.controller.ts |
POST | /api/notifications/feedback | One-tap "too much"/"too quiet" feedback for a priority class. | Body: class,direction | JWT or user API key | notifications/notifications.controller.ts |
POST | /api/notifications/devices | Register a push device. | Body: platform,token,userAgent | JWT or user API key | notifications/notifications.controller.ts |
DELETE | /api/notifications/devices/:deviceId | Delete a push device. | Path: deviceId | JWT or user API key | notifications/notifications.controller.ts |
GET | /api/notifications/push/vapid-public-key | Read the VAPID public key needed to create a browser push subscription. | None | JWT or user API key | notifications/notifications.controller.ts |
Filters and Rules
| Method | Path | Purpose | Request or query | Auth | Source |
|---|---|---|---|---|---|
GET | /api/notifications/filters | List filter rules. | None | JWT or user API key | notifications/notifications.controller.ts |
POST | /api/notifications/filters | Create or update one filter. | Body: inbox rule payload | JWT or user API key | notifications/notifications.controller.ts |
PATCH | /api/notifications/filters | Replace or reorder filters. | Body: rules | JWT or user API key | notifications/notifications.controller.ts |
DELETE | /api/notifications/filters/:id | Delete one filter. | Path: id | JWT or user API key | notifications/notifications.controller.ts |
GET | /api/notifications/rules | List rules. | None | JWT or user API key | notifications/notifications.controller.ts |
POST | /api/notifications/rules | Create or update one rule. | Body: inbox rule payload | JWT or user API key | notifications/notifications.controller.ts |
PATCH | /api/notifications/rules | Replace or reorder rules. | Body: rules | JWT or user API key | notifications/notifications.controller.ts |
DELETE | /api/notifications/rules/:id | Delete one rule. | Path: id | JWT or user API key | notifications/notifications.controller.ts |
Mutes and Threads
| Method | Path | Purpose | Request or query | Auth | Source |
|---|---|---|---|---|---|
GET | /api/notifications/mutes | List muted scopes. | None | JWT or user API key | notifications/notification-mutes.controller.ts |
POST | /api/notifications/mutes | Create or update a mute. | Body: scopeType,scopeId,isMuted | JWT or user API key | notifications/notification-mutes.controller.ts |
DELETE | /api/notifications/mutes/:scopeType/:scopeId | Remove one mute. | Path: scopeType,scopeId | JWT or user API key | notifications/notification-mutes.controller.ts |
GET | /api/notifications/threads | List notification threads. | None | JWT or user API key | notifications/notification-threads.controller.ts |
PATCH | /api/notifications/threads/:id/mute | Mute one thread. | Path: id | JWT or user API key | notifications/notification-threads.controller.ts |
PATCH | /api/notifications/threads/:id/unmute | Unmute one thread. | Path: id | JWT or user API key | notifications/notification-threads.controller.ts |
PATCH | /api/notifications/threads/:id/archive | Archive one thread. | Path: id | JWT or user API key | notifications/notification-threads.controller.ts |
PATCH | /api/notifications/threads/:id/unarchive | Unarchive one thread. | Path: id | JWT or user API key | notifications/notification-threads.controller.ts |
Request Shapes
List query
ListNotificationsQueryDto
unreadOnly: optional booleanarchived: optional booleanthreadId: optional numberafterCursor: optional string
Preferences
UpdateNotificationPreferencesDto.preferences[]
eventType: required stringchannels: required object mapdigest: optional stringfallbackOrder: optional string arrayquietHours: optional object ornull
Device registration
RegisterDeviceDto
platform: requiredweb|ios|androidtoken: required stringuserAgent: optional string
Inline actions (act)
ActNotificationDto (POST /api/notifications/:id/act)
actionType: required, one ofconfirm|snooze|delegate|claim|done|undo|ack(NotificationActionTypeenum)payload: optional object — forsnooze, either{ snoozeUntil: <ISO string or epoch ms> }or{ snoozeMinutes: <number> }; if neither is given, snooze defaults to 30 minutes from now
Calling act also implicitly marks the notification read if it wasn't already. done/confirm archive the notification. snooze archives it and schedules a resurface job (unarchives it again at the resolved snooze time). undo un-archives it. The created NotificationAction row (id, notificationId, userId, actionType, actionPayload, actedAt) is returned as-is.
Mode Pack and Feedback
PUT /api/notifications/mode-pack body:
modePack:"focus_adhd" | "work_store" | "family" | null—nullclears the selection
Selecting a pack (re)seeds a small set of global inbox rules owned by that pack (tagged internally via a packOwned column so switching packs never touches your own hand-authored rules) and removes whichever rules the previous pack had seeded. family currently seeds no rules (no child/view-only role exists yet in the platform), so selecting it only records the preference. Returns the saved UserNotificationSettings row (userId, modePack, feedbackTuning, createdAt, updatedAt).
GET /api/notifications/mode-pack returns the same shape, or null if the user has never set one.
POST /api/notifications/feedback body:
class: required, one ofcritical|time_critical|coordination|operational|fyi(NotificationClassenum)direction: required,"too_much" | "too_quiet"
too_much nudges that class one tier quieter for future notifications (toward hold_for_gap/batch_digest routing); too_quiet nudges it one tier louder (toward interrupt_now). critical is never adjustable this way — the triage engine always ignores feedback tuning for that class. Returns the updated UserNotificationSettings row with the new feedbackTuning map (e.g. { "operational": -1 }).
Digests and preview
GET /api/notifications/digests returns up to the 50 most recent messages with eventType: 'digest.summary', newest first — these are the same shape as any other item from GET /api/notifications.
POST /api/notifications/preview returns { count: number, items: Array<{ title: string; body: string }> } — a live snapshot of currently scheduled, digest-tagged email deliveries for the caller. It doesn't send anything or persist a message; it's read-only preview of what the next digest email would contain if it fired right now.
VAPID public key
GET /api/notifications/push/vapid-public-key returns { publicKey: string | null }. The public key is safe to expose to any authenticated user — only the paired private key (WEBPUSH_VAPID_PRIVATE_KEY, admin-configured, encrypted at rest) is sensitive. publicKey is null when no VAPID key pair has been configured yet, in which case the client should treat web push as unavailable rather than attempting to subscribe. The frontend's usePushSubscription hook (frontend/src/hooks/usePushSubscription.ts) converts this key to the Uint8Array shape PushManager.subscribe() expects and registers the resulting subscription via POST /api/notifications/devices with platform: "web".
Filters and rules
InboxRuleDto
id: optional numbername: required stringscopeType: requiredglobal|organisation|calendar|group|reservationscopeId: optionalisEnabled: required booleanconditions: required array of{ field, operator, value }actions: required array of{ type, payload }continueProcessing: optional booleanorder: optional number
UpdateInboxRulesDto.rules: required array of InboxRuleDto
Mutes
ScopeMuteDto
scopeType: requiredorganisation|calendar|group|reservation|resource|threadscopeId: required stringisMuted: required boolean
Invite event types
GET /api/notifications/catalog includes these people-group invite events:
| Event type | Purpose | Recommended channels | Scope |
|---|---|---|---|
calendar.group_invite.created | A user received an invite to join a calendar sharing people group. | inapp, email; fallback webpush | group |
calendar.group_invite.accepted | A recipient accepted an invite and joined the group. | inapp; fallback email | group |
calendar.group_invite.declined | A recipient declined an invite. | inapp; fallback email | group |
Created-invite notifications include group name, inviter identity, and a short invite-message snippet for the intended recipient. They must not expose invite lookup behavior to unrelated users.
Example Calls
List unread notifications
curl "$PRIMECAL_API/api/notifications?unreadOnly=true" \
-H "Authorization: Bearer $TOKEN"