Notifications
A Notification is one in-app message addressed to one user inside
one organisation — a flagged trace receipt, a team invitation,
a security event, or an admin broadcast. Notifications are
per-user scoped (a user only sees their own), TTL'd at 90 days, and
rendered in the dashboard's notification panel. Tested against API
version v1. JWT auth on every endpoint; broadcast requires the
admin or owner role.
The Notification object
| Field | Type | Required | Description |
|---|---|---|---|
_id / id | string | yes | MongoDB ObjectId |
userId | string | yes | The recipient (chains never mix across users) |
organizationId | string | yes | Tenancy scope |
type | enum | no | system (default), security, invite, broadcast, alert, info |
title | string | yes | Subject line; max 200 characters |
body | string | no | Detail; max 1,000 characters |
isRead | boolean | no | Default false; flips to true on read |
link | string | no | Dashboard URL the notification routes to on click |
sentBy | string | no | User ObjectId of the sender (populated on broadcasts) |
createdAt | string (ISO 8601) | yes | Default now; TTL: notifications older than 90 days are auto-purged |
updatedAt | string (ISO 8601) | no | Standard timestamp |
A MongoDB TTL index drops notifications 90 days after createdAt.
This is operational policy, not a bug; long-term audit retention
lives on the Operations Audit Log, not on
this resource.
Endpoints
GET /api/v1/notifications
DELETE /api/v1/notifications
PATCH /api/v1/notifications/read-all
PATCH /api/v1/notifications/:id/read
DELETE /api/v1/notifications/:id
POST /api/v1/notifications/broadcast
GET, DELETE-all, and the per-id endpoints are scoped to the
caller's own notifications. Broadcasts require the admin or
owner role.
List notifications
GET /api/v1/notifications
| Query parameter | Default | Description |
|---|---|---|
page | 1 | 1-indexed page number |
limit | 30 | Page size; capped server-side |
unreadOnly | — | true filters to isRead: false |
curl https://api.adjudon.com/api/v1/notifications \
-H "Authorization: Bearer $ADJUDON_API_KEY"
import os, requests
r = requests.get(
"https://api.adjudon.com/api/v1/notifications",
params={"unreadOnly": "true", "limit": 30},
headers={"Authorization": f"Bearer {os.environ['ADJUDON_API_KEY']}"},
)
data = r.json()
print(f"Unread: {data['unreadCount']}")
Response (200 OK):
{
"success": true,
"data": [/* Notification[] */],
"unreadCount": 4,
"pagination": { "page": 1, "limit": 30, "total": 47, "pages": 2 }
}
unreadCount is the total unread count for the user, independent of
the current page filter. Use this for the dashboard badge. Errors:
401, 500 INTERNAL_ERROR.
Mark one as read
PATCH /api/v1/notifications/:id/read
Returns the updated notification with isRead: true. The lookup
filters by (_id, userId); another user's notification returns
404, never their content. Errors: 401, 404 NOT_FOUND, 500.
Mark all as read
PATCH /api/v1/notifications/read-all
Bulk-flips every unread notification for the caller to isRead: true.
Returns the count of records updated.
curl -X PATCH https://api.adjudon.com/api/v1/notifications/read-all \
-H "Authorization: Bearer $ADJUDON_API_KEY"
Errors: 401, 500.
Delete one
DELETE /api/v1/notifications/:id
Hard-deletes a notification. Lookup is (_id, userId); a
cross-user attempt returns 404. The TTL index already collects
old notifications after 90 days, so explicit DELETE is for users
who want to clear the inbox sooner.
Errors: 401, 404, 500.
Clear all
DELETE /api/v1/notifications
Hard-deletes every notification for the caller in the active organisation. Use carefully; there is no undo. Returns the count of records deleted.
curl -X DELETE https://api.adjudon.com/api/v1/notifications \
-H "Authorization: Bearer $ADJUDON_API_KEY"
Errors: 401, 500.
Broadcast to the org
POST /api/v1/notifications/broadcast
| Body field | Required | Description |
|---|---|---|
title | yes | Subject line; max 200 characters |
body | yes | Detail; max 1,000 characters |
sendEmail | no | true also dispatches a transactional email to every recipient |
Creates one Notification per active OrgMember in the caller's
organisation. Each notification is stamped with type: 'broadcast'
and sentBy: <caller-userId>; the dashboard link defaults to
/dashboard/notifications. With sendEmail: true the same content
fans out to email via the transactional-email sub-processor (Resend,
EU). Admin/owner role only.
curl -X POST https://api.adjudon.com/api/v1/notifications/broadcast \
-H "Authorization: Bearer $ADJUDON_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"title": "Maintenance window 12 May 22:00 UTC",
"body": "Read-only mode on api.adjudon.com for 30 minutes; trace ingestion will fail open per SDK contract.",
"sendEmail": true
}'
Errors: 400 VALIDATION_ERROR (missing title or body), 401,
403 (role), 500.
Programmatic creation
The notification model is the same one Adjudon's other controllers
write to internally — a flagged trace creates a notification
of type alert; a team invitation creates one of type invite. The
public API does not expose targeted single-notification creation
(use broadcast for org-wide sends, or call the underlying
controllers via the dashboard's review / invitation paths).
Common gotchas
- 90-day auto-purge. The MongoDB TTL index drops every notification 90 days after creation. If you need an event log that survives, use the Operations Audit Log; notifications are an in-app inbox, not an audit trail.
- Per-user scope. Every endpoint filters by
userId = caller. Even anadmincannot fetch another user's notifications via this API; admin observability lives on the Operations Audit Log. - Idempotency on
broadcast. The Idempotency-Key middleware is wired only onPOST /traces; a retried broadcast call creates duplicate per-member notifications. Clients should retry defensively (catch the duplicates server-side viasentBy + createdAtproximity is not implemented today). - Email opt-out. The
sendEmail: truepath obeys per-user email-preference settings stored on theUserdocument. A user who has unsubscribed from broadcast emails still receives the in-app notification but no email.
See also
- Reviews API — flagged decisions
surface here as
type: 'alert'notifications when a reviewer is assigned - Webhooks Overview — the
external-system equivalent: subscribe to
alert.triggeredto fan out to Slack / PagerDuty - Audit Log API — the durable event log for compliance reviewers (90-day TTL on notifications does not apply)
- Sub-Processors — Resend (EU)
is the transactional-email path for
sendEmail: true - Error Codes — the full error taxonomy