BIO.RE
Messages

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.

GET /api/v1/messages/:id — 🔑 Bearer · Rate limit: 60 req / minute · Kill-switched

Returns the full client-facing slice of a single Message row by id. Ownership-checked: you must be either senderId or receiverId; otherwise 403 not_authorized. Content is decrypted server-side on read (best-effort fallback to raw on decryption error).

This endpoint is implemented inline in the controller (no separate service method) — direct Prisma findUnique plus the ownership check plus safeDecrypt. Performance characteristic: a single indexed lookup, near-constant time.

Request

Path parameters

ParamTypeValidationNotes
idstring(no UUID pipe — raw string passthrough)The Message.id. Whatever you pass is fed to findUnique directly.
HeaderRequiredNotes
Authorization: Bearer <accessToken>JWT from POST /auth/login

Response

200 OKApiResponseOf<MessageDetailClientDto>

{
  "success": true,
  "data": {
    "id": "m1a2b3c4-d5e6-7890-abcd-ef1234567890",
    "content": "Hey, love your content!",
    "status": "REPLIED",
    "dmType": "SINGLE_PAY",
    "priceSnapshot": "5.00",
    "senderId": "f0e1d2c3-b4a5-6789-0123-456789abcdef",
    "receiverId": "c1a2b3c4-d5e6-7890-abcd-ef1234567890",
    "createdAt": "2026-04-29T20:00:00.000Z",
    "expiresAt": "2026-05-02T20:00:00.000Z",
    "repliedAt": "2026-04-30T18:00:00.000Z",
    "completedAt": null,
    "timeoutHours": 72
  }
}

Fields

FieldTypeNotes
idstring (UUID)Message.id
contentstringDecrypted body (best-effort; falls back to raw on decryption failure)
statusenumOne of PENDING / ESCROWED / DELIVERED / READ / REPLIED / COMPLETED / EXPIRED / REFUNDED / REJECTED / QUARANTINED
dmTypeenumFREE / SINGLE_PAY / PER_MESSAGE (snapshot at send time)
priceSnapshotstring | nullDecimal as string. The price the fan paid at send time. null for FREE.
senderId / receiverIdstring (UUID)Participants — you must be one of them
createdAtstring (ISO 8601)Send timestamp
expiresAtstring (ISO 8601) | nullReply window deadline (after this, system flips to EXPIRED + refunds the fan)
repliedAtstring (ISO 8601) | nullWhen the creator replied (null if no reply yet)
completedAtstring (ISO 8601) | nullWhen the message reached terminal COMPLETED state (escrow released to creator)
timeoutHoursnumber | nullThe reply deadline window in hours, captured at send time (used to compute expiresAt)

Errors

HTTPcode / i18nKeyReason
401(guard)Missing / invalid bearer token
403message.reply.error.not_authorizedYou're neither senderId nor receiverId
404message.reply.error.not_foundNo Message with this id
429(throttle)Rate limit exceeded (60 req/min)
503features.messaging_disabledAdmin kill switch MESSAGING is active

Side effects

  1. prisma.message.findUnique({ where: { id: messageId }, select: { id, content, status, dmType, priceSnapshot, senderId, receiverId, createdAt, expiresAt, repliedAt, completedAt, timeoutHours } }).
  2. Throw not_found if missing.
  3. Ownership check: senderId !== userId AND receiverId !== userId → throw not_authorized.
  4. Decrypt content via safeDecrypt (try / fallback to raw).
  5. Return the row with decrypted content. No mutations.

Code samples

curl https://api.bio.re/api/v1/messages/m1a2b3c4-d5e6-7890-abcd-ef1234567890 \
  -H "Authorization: Bearer $ACCESS_TOKEN"
type MessageDetail = {
  id: string;
  content: string;
  status: string;
  dmType: 'FREE' | 'SINGLE_PAY' | 'PER_MESSAGE';
  priceSnapshot: string | null;
  senderId: string;
  receiverId: string;
  createdAt: string;
  expiresAt: string | null;
  repliedAt: string | null;
  completedAt: string | null;
  timeoutHours: number | null;
};

async function getMessage(accessToken: string, messageId: string): Promise<MessageDetail> {
  const res = await fetch(`https://api.bio.re/api/v1/messages/${messageId}`, {
    headers: { Authorization: `Bearer ${accessToken}` },
  });
  const json = await res.json();
  if (!res.ok || !json.success) {
    throw Object.assign(new Error(json?.error?.message ?? 'Message fetch failed'), {
      code: json?.error?.code,
    });
  }
  return json.data;
}
import { useQuery } from '@tanstack/react-query';

export const messageKeys = {
  detail: (id: string) => ['messages', 'detail', id] as const,
};

export function useMessageDetail(messageId: string) {
  return useQuery({
    queryKey: messageKeys.detail(messageId),
    queryFn: async () => {
      const res = await fetch(`/api/v1/messages/${messageId}`);
      const json = await res.json();
      if (!res.ok || !json.success) {
        throw Object.assign(new Error(json?.error?.message ?? 'Message fetch failed'), {
          code: json?.error?.code,
          i18nKey: json?.error?.i18nKey,
        });
      }
      return json.data as MessageDetail;
    },
    enabled: Boolean(messageId),
    staleTime: 10_000,
  });
}

Try it

GET
/api/v1/messages/{id}
AuthorizationBearer <token>

In: header

Path Parameters

id*string

Response Body

application/json

application/json

application/json

application/json

curl -X GET "https://loading/api/v1/messages/string"
{
  "success": true,
  "data": {
    "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
    "content": "Hey, love your content!",
    "status": "PENDING",
    "dmType": "FREE",
    "priceSnapshot": "25.00",
    "senderId": "6b2f63ba-164c-48c9-87b1-690cee2b3da3",
    "receiverId": "2ec2e5a9-5968-4568-baf3-a525f7f8b9a6",
    "createdAt": "2019-08-24T14:15:22Z",
    "expiresAt": "2019-08-24T14:15:22Z",
    "repliedAt": "2019-08-24T14:15:22Z",
    "completedAt": "2019-08-24T14:15:22Z",
    "timeoutHours": 0
  }
}
{
  "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"
  }
}
{
  "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"
  }
}
{
  "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/message/message.controller.ts135–158 (getMessage — direct Prisma + ownership check + safeDecrypt)
DTO (response)apps/api-core/src/modules/message/dto/message-client-response.dto.ts165–201 (MessageDetailClientDto)
Encryptionapps/api-core/src/common/encryption.tsdecryptContent()
Prisma modelpackages/prisma/prisma/schema.prismaMessage (enum MessageStatus, enum DmType)

On this page