DocsAPI Reference

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

ParamDefaultNotes
page11-indexed
limit25Max 100
unreadOnlyfalsetrue to filter
categorye.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 } }.

Note

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:

  • key must be a top-level preference key (not a child key like REENGAGE_7_DAY_INACTIVITY)
  • If the definition has canDisable: false, any attempt to set a channel to false or frequency: "OFF" returns 403
  • If the definition has canChangeFrequency: false, only IMMEDIATE is 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 } }.

Tip

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.