Notifications API
Inbox, preferences, digest times, push tokens
Every endpoint below requires an authenticated session and is scoped to the calling user + institution. See Notifications System for the engine behind these endpoints.
GET /api/notifications
Paginated list of in-app notifications for the current user. Only IN_APP channel rows with status SENT or PENDING are returned — email/push delivery rows and SKIPPED/FAILED rows don't appear.
Query
| Param | Default | Notes |
|---|---|---|
page | 1 | 1-indexed |
limit | 25 | Max 100 |
unreadOnly | false | true to filter |
category | — | e.g. Live classes — match against the definition's category |
Response
{
"data": {
"notifications": [
{
"id": "...",
"title": "Quiz graded",
"message": "You scored 8/10 on Cell Biology Quiz",
"type": "ASSESS_GRADED",
"key": "ASSESS_GRADED",
"category": "Grades & feedback",
"actionUrl": "/submissions/sub_123",
"isRead": false,
"readAt": null,
"createdAt": "2026-04-15T08:14:00Z",
"metadata": { "submissionId": "sub_123", "score": 8 }
}
],
"total": 42,
"unreadCount": 7
},
"error": null
}
PATCH /api/notifications/read
Mark notifications as read. Two body shapes accepted:
# Specific ids
curl -X PATCH /api/notifications/read \
-H "Content-Type: application/json" \
-d '{"ids": ["n1", "n2", "n3"]}'
# Mark all unread as read
curl -X PATCH /api/notifications/read \
-d '{"all": true}'
Always scoped to the caller — clients can't mark someone else's notification by sending its id.
Response: { data: { updated: 7 } }.
The legacy PATCH /api/notifications endpoint still accepts { markAllRead: true } and { notificationId: "..." } for backward-compat with the older TopNav. New code should use /api/notifications/read.
GET /api/notifications/unread-count
Lightweight bell-badge poll. The header polls every 30 seconds.
{ "data": { "count": 7 }, "error": null }
GET /api/notifications/preferences
Returns the merged preference set — definition defaults overlaid by the user's stored rows. The response always contains a row for every key the user's role can see, so the UI never has to handle a "missing row" case.
Response
{
"data": {
"role": "STUDENT",
"categories": [
{
"category": "Courses & enrollment",
"definitions": [ { "key", "label", "description", "defaultChannels", "canDisable", "canChangeFrequency", "availableTo" } ]
}
],
"preferences": {
"ENROLL_WELCOME": {
"emailEnabled": true,
"pushEnabled": true,
"inAppEnabled": true,
"frequency": "IMMEDIATE"
}
},
"digestTimes": {
"dailyTime": "19:00",
"weeklyDay": "SUNDAY",
"weeklyTime": "09:00",
"timezone": "Asia/Kolkata"
},
"hasPushToken": true
}
}
role is one of STUDENT | TEACHER | ADMIN | PARENT — derived from the JWT.
PATCH /api/notifications/preferences
Upsert one preference row. Body:
{
"key": "REENGAGE_NUDGE",
"emailEnabled": true,
"pushEnabled": false,
"inAppEnabled": true,
"frequency": "DAILY_DIGEST"
}
All fields except key are optional — only sent fields are updated.
Validation:
keymust be a top-level preference key (not a child key likeREENGAGE_7_DAY_INACTIVITY)- If the definition has
canDisable: false, any attempt to set a channel tofalseorfrequency: "OFF"returns 403 - If the definition has
canChangeFrequency: false, onlyIMMEDIATEis accepted - Visibility check: students can't toggle teacher-only keys
Response: the updated row.
DELETE /api/notifications/preferences
Reset to defaults — wipes every NotificationPreference row for the caller. Requires confirmation:
{ "confirm": true }
Response: { data: { reset: true } }.
PATCH /api/notifications/preferences/digest-times
Update digest delivery time + timezone. All fields optional:
{
"dailyTime": "20:00",
"weeklyDay": "MONDAY",
"weeklyTime": "08:00",
"timezoneOverride": "America/New_York"
}
Times are validated HH:MM. weeklyDay is one of SUNDAY..SATURDAY. Writes to NotificationUserState.
Response: the updated values.
POST /api/notifications/push-token
Register a web push token. Wraps the same MobileDevice upsert used by the mobile FCM token endpoint.
{
"token": "<FCM token>",
"platform": "web" // or "ios", "android"
}
Server derives a stable deviceId from sha256(userId:token) so repeat registrations from the same browser don't pile up rows. Any other device row holding the same token is cleared (FCM token migration).
Response: { data: { id, registered: true } }.
The mobile app should continue using /api/v1/devices/fcm-token (bearer-authed, includes device metadata). This route is for web push and parity with the spec.
Related
- Notifications System — engine internals
- Worker Deployment — what processes these
- Notifications Admin Setup — post-deploy checklist