Get Unread Count
Lightweight badge counter — total unread messages where you're the receiver. Counts messages in PENDING / ESCROWED / DELIVERED states across all sessions.
GET /api/v1/messages/unread-count — 🔑 Bearer · Rate limit: 60 req / minute · Kill-switched
Returns the total count of unread messages for the calling user — across all sessions. "Unread" = you're the receiverId AND status IN (PENDING, ESCROWED, DELIVERED). Use this to drive the navigation badge / app icon counter.
Read state isn't user-driven. There is no POST /messages/:id/read — status flips to READ server-side when the chat-service Socket.IO layer marks the message read in real-time (or via internal background job). This endpoint counts messages still in PENDING / ESCROWED / DELIVERED — anything beyond READ (REPLIED / COMPLETED / EXPIRED / REFUNDED / REJECTED / QUARANTINED) is excluded.
Request
No body, no params.
| Header | Required | Notes |
|---|---|---|
Authorization: Bearer <accessToken> | ✓ | JWT from POST /auth/login |
Response
200 OK — ApiResponseOf<UnreadCountResponseDto>
{
"success": true,
"data": {
"total": 5
}
}| Field | Type | Notes |
|---|---|---|
total | number | Total unread messages across all sessions where you're the receiver |
Errors
| HTTP | code / i18nKey | Reason |
|---|---|---|
401 | (guard) | Missing / invalid bearer token |
429 | (throttle) | Rate limit exceeded (60 req/min) |
503 | features.messaging_disabled | Admin kill switch MESSAGING is active |
Side effects
- Single
prisma.message.count({ where: { receiverId: userId, status: { in: [PENDING, ESCROWED, DELIVERED] } } }). - Return
{ total }. No mutations.
Polling guidance
- Refresh on app focus + after returning from a chat screen (Socket.IO event flips status → count drops).
- Don't tight-loop poll — the count flips via Socket.IO events delivered to the chat-service WebSocket. Subscribe there for real-time updates instead of polling.
- A 30s background refresh covers the case where the user backgrounded the WebSocket layer.
Code samples
curl https://api.bio.re/api/v1/messages/unread-count \
-H "Authorization: Bearer $ACCESS_TOKEN"async function getUnreadCount(accessToken: string): Promise<number> {
const res = await fetch('https://api.bio.re/api/v1/messages/unread-count', {
headers: { Authorization: `Bearer ${accessToken}` },
});
const json = await res.json();
if (!res.ok || !json.success) {
throw Object.assign(new Error(json?.error?.message ?? 'Unread count fetch failed'), {
code: json?.error?.code,
});
}
return json.data.total as number;
}import { useQuery } from '@tanstack/react-query';
export const messageKeys = {
unreadCount: () => ['messages', 'unread-count'] as const,
};
export function useUnreadCount() {
return useQuery({
queryKey: messageKeys.unreadCount(),
queryFn: async () => {
const res = await fetch('/api/v1/messages/unread-count');
const json = await res.json();
if (!res.ok || !json.success) {
throw Object.assign(new Error(json?.error?.message ?? 'Unread count fetch failed'), {
code: json?.error?.code,
i18nKey: json?.error?.i18nKey,
});
}
return json.data.total as number;
},
// Refresh on focus is the primary trigger; tight polling is wasteful (use Socket.IO instead)
staleTime: 30_000,
refetchOnWindowFocus: true,
});
}Try it
Authorization
bearer In: header
Response Body
application/json
application/json
curl -X GET "https://loading/api/v1/messages/unread-count"{
"success": true,
"data": {
"total": 5
}
}{
"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 | apps/api-core/src/modules/message/message.controller.ts | 125–131 (getUnreadCount) |
| DTO (response) | apps/api-core/src/modules/message/dto/message-client-response.dto.ts | 155–158 (UnreadCountResponseDto) |
| Service | apps/api-core/src/modules/message/message.service.ts | 915–923 (getUnreadCount) |
| Real-time updates | apps/chat-service/ | Socket.IO events flip read state on the server side |
| Prisma model | packages/prisma/prisma/schema.prisma | Message.status (enum MessageStatus), Message.receiverId |
List Conversations
Paginated chat session list with the other party's profile slice, last message preview (encrypted placeholder), unread count per session, and bidirectional block flags.
Get Message Detail
Read a single message by id with all client-facing fields. Ownership-checked — you must be the sender or receiver. Content decrypted on read.