Project ROUNDTABLE Docs
API reference

Notifications API

In-app notification lists, preferences, reads, and rerouting

Notifications are database rows displayed by the bell UI. They are not external email. Every read/list operation scopes by the current user's ID, while rerouting is a government operation that recalculates submitted records.

GET /api/notifications

Auth: Any signed-in user. Success: 200.

Query fieldTypeRequired?Constraints/default
pageintegerNoMinimum 1; default 1.
pageSizeintegerNo1–50; default 20.
unreadenumNo"true" or "false".
curl 'http://localhost:3000/api/notifications?page=1&pageSize=20&unread=true' \
  -H 'cookie: next-auth.session-token=SESSION'

Example:

{"data":{"items":[{"id":"clx-notification","kind":"SUBMISSION_MATCH","title":"New submission routed: Autonomous UUV navigation","body":"Pelagic Dynamics submitted...","linkUrl":"/gov/organizations/clx-pelagic?submission=clx-submission","read":false,"createdAt":"2026-02-01T12:00:01.000Z"}],"total":1,"page":1,"pageSize":20}}

Errors: 401, 400 invalid query, 500. Pagination is applied after the current user's filter.

GET /api/notifications/unread-count

Auth: Any signed-in user. Success: 200.

curl http://localhost:3000/api/notifications/unread-count \
  -H 'cookie: next-auth.session-token=SESSION'

Response: { "data": { "count": 3 } }. Errors: 401 or 500.

POST /api/notifications/read

Auth: Any signed-in user. The body is a union, so send exactly one form.

BodyTypeMeaning
idstring 1–64Mark this user's notification read.
allliteral trueMark all of this user's notifications read.
curl -X POST http://localhost:3000/api/notifications/read \
  -H 'content-type: application/json' -H 'cookie: next-auth.session-token=SESSION' \
  -d '{"id":"clx-notification"}'

Or { "all": true }. Success: { "data": { "updated": 1 } }; errors 401, malformed JSON 400, schema 400, and 500. An ID belonging to another user does not update another user's row.

GET /api/notifications/settings

Auth: Any signed-in user. Success: 200.

curl http://localhost:3000/api/notifications/settings \
  -H 'cookie: next-auth.session-token=SESSION'

Response shape:

{"data":{"settings":[{"kind":"SUBMISSION_MATCH","enabled":true,"techTags":""},{"kind":"ANNOUNCEMENT","enabled":true,"techTags":""},{"kind":"CALL_OPENED","enabled":true,"techTags":""},{"kind":"ENGAGEMENT_UPDATE","enabled":true,"techTags":""},{"kind":"SYSTEM","enabled":true,"techTags":""}]}}

Missing database rows are returned as enabled defaults. Errors: 401 or 500.

PUT /api/notifications/settings

Auth: Any signed-in user.

FieldTypeRequired?Constraints
settingsarrayYes1 through all notification kinds.
settings[].kindenumYesEvery NotificationKind value.
settings[].enabledbooleanYesExplicit true/false.

The implementation also accepts the optional tag filter where the settings UI sends it; tags are normalized lowercase and persisted in NotificationSetting.techTags.

curl -X PUT http://localhost:3000/api/notifications/settings \
  -H 'content-type: application/json' -H 'cookie: next-auth.session-token=SESSION' \
  -d '{"settings":[{"kind":"SUBMISSION_MATCH","enabled":true,"techTags":"uuv,autonomy"},{"kind":"ANNOUNCEMENT","enabled":false},{"kind":"CALL_OPENED","enabled":true},{"kind":"ENGAGEMENT_UPDATE","enabled":true},{"kind":"SYSTEM","enabled":true}]}'

Success: { "data": { "ok": true } }. Errors: 401, 400 for a malformed body or unknown kinds, and 500. Duplicate kinds are accepted; the last occurrence wins because settings are upserted in array order. A disabled kind suppresses future rows; it does not delete old notifications.

POST /api/notifications/reroute

Auth: admin-tier government session (GOV_ADMIN or GOV_SYSTEM_ADMIN). Success: 200.

curl -X POST http://localhost:3000/api/notifications/reroute \
  -H 'cookie: next-auth.session-token=ADMIN_SESSION'

The route finds SUBMITTED submissions and calls routeSubmission for each. Response: { "data": { "routed": 4, "totalMatches": 7 } }. Errors: 401, 403, 500. Reroute preserves match replacement behavior and notification de-duplication.

Notification kinds

KindTypical producerTypical recipient
SUBMISSION_MATCHrouteSubmissionGovernment POC or matching profile user.
ANNOUNCEMENTGovernment announcement publisherIndustry users or configured recipients.
CALL_OPENEDGovernment call publisherIndustry users.
ENGAGEMENT_UPDATEGovernment engagement workflowConfigured users when produced.
SYSTEMAdministrative/system pathUser-specific system recipient.

Read semantics

Marking a notification read is not deletion. The row, title, body, link, kind, and timestamp remain available in the paginated list. Mark-all updates rows belonging to the current user only. The unread count is a count query, so a stale browser can refresh it without downloading all notification bodies.

Preference semantics

A missing setting row behaves as enabled. An existing disabled row suppresses the matching future event. A nonempty tag filter is applied only when the event carries tags; it is lowercased and compared by exact comma-separated token after normalization. Changing a preference does not retroactively delete notifications or mark them read.

Safe client behavior

Treat notification IDs as opaque strings. A client should send only the ID or mark-all instruction supported by the route and should not attempt to update another user's row. The server derives the user ID from the session, so adding a userId property to a request cannot broaden the operation.

After a mutation, refresh the list and unread count rather than assuming every local badge update succeeded. A network retry is safe for an idempotent read; for a write, follow the route's documented body and inspect the response before retrying.

Example preference update

await fetch('/api/notifications/settings', {
  method: 'PUT',
  headers: { 'content-type': 'application/json' },
  body: JSON.stringify({
    settings: [
      { kind: 'SUBMISSION_MATCH', enabled: true, techTags: 'ai, autonomy' },
      { kind: 'CALL_OPENED', enabled: false, techTags: '' }
    ]
  })
});

The array must contain at least one setting and cannot exceed the number of NotificationKind enum values. Tag filters reject control and injection-relevant characters before normalization. A successful update writes notification_settings_updated to the audit log.