BIO.RE
Notifications

List Notifications

Paginated in-app notification feed for the calling user. Optional read-state filter. Returns the full row including event key, title, body, and free-form data payload.

GET /api/v1/notifications — 🔑 Bearer · Rate limit: 60 req / minute

Paginated list of the calling user's in-app notifications, ordered by createdAt DESC. Optional ?read=true|false filter. Each item includes the structured data payload (e.g. messageId, deepLink) so the UI can render contextual actions without an extra round-trip.

Notifications are dispatched server-side per event (e.g. new_message_creator, payout_processed, message_replied_fan, email_subscription_confirm). The data payload shape varies per eventKey — frontend should branch on eventKey (not on data field presence) when deciding what to render.

No service layer. This endpoint is implemented directly in the controller via Prisma — no separate service method. Performance is bound by the indexed (userId, createdAt DESC) query.

Request

Query parameters

ParamTypeDefaultValidationNotes
readstringaccepts 'true' or 'false' literal strings (anything else falls back to "no filter")Filter to read or unread only
pagenumber1parseInt, server-clamped to >= 11-based page index
limitnumber20parseInt, server-clamped to [1, 50]Items per page
HeaderRequiredNotes
Authorization: Bearer <accessToken>JWT from POST /auth/login

Response

200 OKPaginatedApiResponseOf<NotificationItemDto>

{
  "success": true,
  "data": {
    "items": [
      {
        "id": "n1a2b3c4-d5e6-7890-abcd-ef1234567890",
        "eventKey": "new_message_creator",
        "title": "New message from John",
        "body": "Hey, loved your latest post!",
        "data": {
          "messageId": "m1a2b3c4-d5e6-7890-abcd-ef1234567890",
          "senderName": "John",
          "dmType": "SINGLE_PAY",
          "deepLink": "/messages/s1a2b3c4-d5e6-7890-abcd-ef1234567890"
        },
        "read": false,
        "readAt": null,
        "createdAt": "2026-04-29T20:00:00.000Z"
      }
    ],
    "total": 42,
    "page": 1,
    "limit": 20,
    "totalPages": 3
  }
}

Item fields

FieldTypeNotes
idstring (UUID)Notification.id
eventKeystringEvent identifier (e.g. new_message_creator, message_replied_fan, payout_processed, message_rejected_refunded, message_expired, email_subscription_confirm). Branch on this for rendering.
titlestringServer-rendered title (localized server-side via the notification template)
bodystringServer-rendered body (localized server-side; may include placeholders pre-filled via variables)
dataobject | nullFree-form JSON payload — shape varies per eventKey. Common keys: messageId, deepLink, creatorName, amount. Do not assume any field is present; check before use.
readbooleanRead state
readAtstring (ISO 8601) | nullWhen marked read; null while unread
createdAtstring (ISO 8601)Notification creation timestamp

Top-level fields

FieldTypeNotes
itemsarrayUp to limit notifications, ordered by createdAt DESC
totalnumberTotal matching the read filter
page / limit / totalPagesnumberEchoed pagination metadata

Errors

HTTPcode / i18nKeyReason
401(guard)Missing / invalid bearer token
429(throttle)Rate limit exceeded (60 req/min)

Side effects

  1. Build where = { userId } + optional read: true|false filter.
  2. In parallel: notification.findMany({ where, skip, take, orderBy createdAt desc }) + notification.count({ where }).
  3. Map rows to plain objects (avoid Prisma runtime types leaking) with the 8-field client-facing slice.
  4. Return paginated envelope. No mutations.

Code samples

# All notifications
curl 'https://api.bio.re/api/v1/notifications?page=1&limit=20' \
  -H "Authorization: Bearer $ACCESS_TOKEN"

# Unread only
curl 'https://api.bio.re/api/v1/notifications?read=false' \
  -H "Authorization: Bearer $ACCESS_TOKEN"
type NotificationItem = {
  id: string;
  eventKey: string;
  title: string;
  body: string;
  data: Record<string, unknown> | null;
  read: boolean;
  readAt: string | null;
  createdAt: string;
};

type Paginated<T> = {
  items: T[];
  total: number;
  page: number;
  limit: number;
  totalPages: number;
};

async function listNotifications(
  accessToken: string,
  filter: { read?: boolean; page?: number; limit?: number } = {},
): Promise<Paginated<NotificationItem>> {
  const url = new URL('https://api.bio.re/api/v1/notifications');
  if (filter.read !== undefined) url.searchParams.set('read', String(filter.read));
  if (filter.page) url.searchParams.set('page', String(filter.page));
  if (filter.limit) url.searchParams.set('limit', String(filter.limit));
  const res = await fetch(url, {
    headers: { Authorization: `Bearer ${accessToken}` },
  });
  const json = await res.json();
  if (!res.ok || !json.success) {
    throw Object.assign(new Error(json?.error?.message ?? 'Notifications fetch failed'), {
      code: json?.error?.code,
    });
  }
  return json.data;
}
import { useQuery, keepPreviousData } from '@tanstack/react-query';

export const notificationKeys = {
  list: (filter: { read?: boolean; page?: number; limit?: number }) =>
    ['notifications', 'list', filter] as const,
};

export function useNotifications(filter: {
  read?: boolean;
  page?: number;
  limit?: number;
} = {}) {
  return useQuery({
    queryKey: notificationKeys.list(filter),
    queryFn: async () => {
      const url = new URL('/api/v1/notifications', window.location.origin);
      if (filter.read !== undefined) url.searchParams.set('read', String(filter.read));
      if (filter.page) url.searchParams.set('page', String(filter.page));
      if (filter.limit) url.searchParams.set('limit', String(filter.limit));
      const res = await fetch(url);
      const json = await res.json();
      if (!res.ok || !json.success) {
        throw Object.assign(new Error(json?.error?.message ?? 'Notifications fetch failed'), {
          code: json?.error?.code,
          i18nKey: json?.error?.i18nKey,
        });
      }
      return json.data as Paginated<NotificationItem>;
    },
    placeholderData: keepPreviousData,
    staleTime: 10_000, // refresh frequently — server pushes events constantly
  });
}

Try it

GET
/api/v1/notifications
AuthorizationBearer <token>

In: header

Query Parameters

read?string
page?string
limit?string

Response Body

application/json

application/json

curl -X GET "https://loading/api/v1/notifications"
{
  "success": true,
  "data": {
    "items": [
      {
        "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
        "eventKey": "string",
        "title": "string",
        "body": "string",
        "data": {},
        "read": true,
        "readAt": "2019-08-24T14:15:22Z",
        "createdAt": "2019-08-24T14:15:22Z"
      }
    ],
    "page": 1,
    "limit": 20,
    "total": 150,
    "totalPages": 8
  }
}
{
  "success": false,
  "error": {
    "code": "AUTH_UNAUTHORIZED",
    "message": "Invalid credentials",
    "i18nKey": "auth.login.invalid_credentials",
    "i18nVars": {
      "field": "email"
    },
    "details": [
      {
        "message": "email must be an email"
      }
    ],
    "correlationId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
  }
}

Source

SourcePathLines
Controller (inline impl)apps/api-core/src/modules/notification/user-notification.controller.ts41–77 (list — direct Prisma, no service)
DTO (response item)apps/api-core/src/modules/notification/dto/notification-client-response.dto.ts8–32 (NotificationItemDto)
Prisma modelpackages/prisma/prisma/schema.prismaNotification (composite index (userId, createdAt))

On this page