Skip to main content

Notifications Inbox

The notifications inbox delivers activity alerts to members within a hub. All endpoints are scoped to a single hub and require a valid contact JWT (aud=contact). Resource types:

Endpoints


Authentication

  • Read endpoints (GET) — accept a Bearer token or the refresh cookie (get_current_contact_for_read).
  • Write endpoints (POST, PUT) — require a Bearer token only (get_current_contact).
  • Hub membership is enforced on every request. A non-member (or banned member) receives 404 hub_not_found or 403 hub_membership_required.

List notifications

Returns notifications for the authenticated contact in this hub, newest-first. Query parameters: Response (200):
Pagination: pass page[after]=<next_cursor> to fetch the next page. The cursor is the raw notification UUID (not base64-encoded). When meta.page.has_more is false, you are on the last page. Nullable attributes: actor, actors, count, target, preview, deep_link, highlight, and icon are always present in the response body (serialized as null when absent — never omitted). Clients must handle null. Filtering a tab’s content: filter[category] and filter[kind] narrow the list server-side so a tab’s content and its pagination are always complete — do not client-filter an already-fetched page. Both compose (AND) with each other, with filter[status]=unread, and with page[after]. Because the list and the /counts endpoint (below) share the same visibility rules, a filtered list’s total row count across all pages always matches that filter’s /counts bucket (e.g. filter[kind]=mention sums to kinds.mention.total). meta.unread_count is never affected by these filters — it always reflects the global bell-visible unread total, matching /unread-count; use /counts for a per-tab unread number. filter[kind]=direct-message always returns an empty page — direct messages are conversation-owned, not bell content.

Notification attributes

actor shape:
target shape:

Get unread count

Lightweight endpoint for badge display. Use this instead of fetching the full list just to get the count. Response (200):

Get per-category/kind counts

Server-aggregated breakdown for building inbox tabs (e.g. “Unread”, “Mentions”, “Moderation”) without fetching the full list. Uses the exact same visibility rules as the list and unread-count endpoints — direct-message notifications and dismissed notifications never contribute to any number here, so attributes.unread always matches the unread-count endpoint’s total for the same contact.
  • categories is a fixed set of four keys (transactional, content, community, moderation) — always present, even at zero.
  • kinds is sparse — only kinds with at least one matching notification appear (e.g. mention, comment, new-report).
  • Each bucket is { "total": <int>, "unread": <int> }, where total counts read + unread and unread counts unread only.
Response (200):
A typical inbox composes tabs from this response: Unread uses attributes.unread; Mentions uses attributes.kinds.mention.total; Moderation uses attributes.categories.moderation.total. Any future tab can read the same generic map without a backend change.

Get single notification

Fetch one notification by its id. Returns 404 notification_not_found if the notification does not belong to the authenticated contact. Response (200): same shape as a single item in the list data array, wrapped in { "data": { ... } }.

Mark one notification read

Idempotent. Calling it on an already-read notification succeeds silently. Response: 204 No Content (no body). Errors:
  • 404 notification_not_found — notification does not belong to this contact.
  • 403 hub_membership_required — contact is banned from this hub.

Mark all notifications read

Bulk marks every unread notification for this contact + hub as read. Returns the timestamp and the count of rows updated. Response (200):
marked_count will be 0 if there were no unread notifications — this is not an error.

Record inbox last-opened time

Upserts last_opened_at = now() for this (hub_id, contact_id) pair. Use this whenever the member opens the inbox UI so the backend can determine which notifications arrived since the last visit (e.g. for a “new since you last looked” divider in the UI). Response (200):

Typical inbox polling pattern


Error codes


Web Push (browser notifications)

Web Push enables the browser to receive notifications even when the member does not have the hub tab open. The backend uses the VAPID protocol (MIO-1145). Resource types:

Endpoints

Three-step setup

Step 1 — Get the VAPID public key
Response (200):
Pass attributes.public_key to the browser’s PushManager.subscribe() call as the applicationServerKey. Step 2 — Subscribe in the browser
Step 3 — POST the subscription to the backend
Request body:
Response: 201 Created with the created subscription resource.

Removing a subscription

Response: 204 No Content. A 404 is expected and safe to ignore if the subscription was already deleted.

Notification preferences

notification_prefs.push is now a valid key on the notification-preferences PATCH. Valid preference keys: Set push: false to suppress web push delivery without deleting the subscription.

Error codes

Rate limit: 60 subscribe requests per hour per contact per hub. Exceeding the limit returns 429.

Real-time badge sync via SSE

When a new notification arrives, the server emits a community.notification.created event on the SSE stream. Listen for this event to update the unread badge without polling:
See the Realtime SSE guide for connection setup and the full event-type reference.