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
| Param | Type | Default | Validation | Notes |
|---|---|---|---|---|
read | string | โ | accepts 'true' or 'false' literal strings (anything else falls back to "no filter") | Filter to read or unread only |
page | number | 1 | parseInt, server-clamped to >= 1 | 1-based page index |
limit | number | 20 | parseInt, server-clamped to [1, 50] | Items per page |
| Header | Required | Notes |
|---|---|---|
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
| Field | Type | Notes |
|---|---|---|
id | string (UUID) | Notification.id |
eventKey | string | Event identifier (e.g. new_message_creator, message_replied_fan, payout_processed, message_rejected_refunded, message_expired, email_subscription_confirm). Branch on this for rendering. |
title | string | Server-rendered title (localized server-side via the notification template) |
body | string | Server-rendered body (localized server-side; may include placeholders pre-filled via variables) |
data | object | null | Free-form JSON payload โ shape varies per eventKey. Common keys: messageId, deepLink, creatorName, amount. Do not assume any field is present; check before use. |
read | boolean | Read state |
readAt | string (ISO 8601) | null | When marked read; null while unread |
createdAt | string (ISO 8601) | Notification creation timestamp |
Top-level fields
| Field | Type | Notes |
|---|---|---|
items | array | Up to limit notifications, ordered by createdAt DESC |
total | number | Total matching the read filter |
page / limit / totalPages | number | Echoed pagination metadata |
Errors
| HTTP | code / i18nKey | Reason |
|---|---|---|
401 | (guard) | Missing / invalid bearer token |
429 | (throttle) | Rate limit exceeded (60 req/min) |
Side effects
- Build
where = { userId }+ optionalread: true|falsefilter. - In parallel:
notification.findMany({ where, skip, take, orderBy createdAt desc })+notification.count({ where }). - Map rows to plain objects (avoid Prisma runtime types leaking) with the 8-field client-facing slice.
- 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
Authorization
bearer In: header
Query Parameters
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
| Source | Path | Lines |
|---|---|---|
| Controller (inline impl) | apps/api-core/src/modules/notification/user-notification.controller.ts | 41โ77 (list โ direct Prisma, no service) |
| DTO (response item) | apps/api-core/src/modules/notification/dto/notification-client-response.dto.ts | 8โ32 (NotificationItemDto) |
| Prisma model | packages/prisma/prisma/schema.prisma | Notification (composite index (userId, createdAt)) |
Rate Message
Fan rates a completed conversation 1โ5 stars. Atomic create + recompute creator's avgRating / ratingCount in one transaction. Single-shot โ duplicate attempts are 409 (P2002).
Unread Notification Count
Lightweight badge counter โ total unread notifications for the calling user. Pairs with the inbox list endpoint for navigation badges.