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
readstringโ€”accepts '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 OK โ€” PaginatedApiResponseOf<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