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 field | Type | Required? | Constraints/default |
|---|---|---|---|
page | integer | No | Minimum 1; default 1. |
pageSize | integer | No | 1–50; default 20. |
unread | enum | No | "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.
| Body | Type | Meaning |
|---|---|---|
id | string 1–64 | Mark this user's notification read. |
all | literal true | Mark 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.
| Field | Type | Required? | Constraints |
|---|---|---|---|
settings | array | Yes | 1 through all notification kinds. |
settings[].kind | enum | Yes | Every NotificationKind value. |
settings[].enabled | boolean | Yes | Explicit 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
| Kind | Typical producer | Typical recipient |
|---|---|---|
SUBMISSION_MATCH | routeSubmission | Government POC or matching profile user. |
ANNOUNCEMENT | Government announcement publisher | Industry users or configured recipients. |
CALL_OPENED | Government call publisher | Industry users. |
ENGAGEMENT_UPDATE | Government engagement workflow | Configured users when produced. |
SYSTEM | Administrative/system path | User-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.