BIO.RE
Support

Get Ticket Detail

Single ticket including its full message history and category. Owner-scoped — querying someone else's ticket returns 404 (not 403) so existence is never leaked.

GET /api/v1/tickets/:ticketId — 🔑 Bearer

Returns the ticket plus its full ordered message history and the embedded category. Owned by current user only — see the security note below.

Foreign tickets respond 404, not 403. The service does if (!ticket || ticket.userId !== userId) throw I18nNotFound(...). This collapses two cases into one response so a caller can't probe for existence by trying random UUIDs. Don't try to distinguish "not found" from "not yours" client-side — they're indistinguishable on the wire by design.

Internal messages are NOT filtered out here. The service does include: { messages: { orderBy: { createdAt: 'asc' } } } — every TicketMessage row is returned, including those with isInternal: true. As of writing, the user-side controller doesn't strip them. Hide rows where isInternal === true client-side when rendering the user-facing thread, otherwise admin-only notes will leak into the user's UI.

No throttle on this read. Same as the list — only the ticket-create handler has @Throttle(). Polling is fine.

Request

Path parameters

ParamTypeRequiredValidationNotes
ticketIdstring (UUID)ParseUUIDPipe (rejects non-UUID with 400)The ticket id you got from create or list.

Headers

HeaderRequiredNotes
Authorization: Bearer <accessToken>Global JwtAuthGuard.

Response

200 OKApiResponseOf<TicketDetailDto>

{
  "success": true,
  "data": {
    "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
    "userId": "00000000-0000-4000-8000-000000000001",
    "categoryId": "11111111-2222-3333-4444-555555555555",
    "subject": "Payout delayed by 3 days",
    "status": "WAITING_USER",
    "priority": "HIGH",
    "assignedTo": "agent-uuid",
    "resolvedAt": null,
    "closedAt": null,
    "createdAt": "2026-04-20T09:00:00.000Z",
    "updatedAt": "2026-04-22T14:30:00.000Z",
    "category": {
      "id": "11111111-2222-3333-4444-555555555555",
      "name": "Payments",
      "description": "Payment-related issues",
      "priority": "HIGH",
      "active": true,
      "sortOrder": 1,
      "createdAt": "2026-01-01T00:00:00.000Z",
      "updatedAt": "2026-01-01T00:00:00.000Z"
    },
    "messages": [
      {
        "id": "msg-uuid-1",
        "ticketId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
        "authorId": "00000000-0000-4000-8000-000000000001",
        "authorType": "USER",
        "content": "I requested a payout on 2026-04-20 but I have not received the funds yet.",
        "isInternal": false,
        "createdAt": "2026-04-20T09:00:00.000Z"
      },
      {
        "id": "msg-uuid-2",
        "ticketId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
        "authorId": "agent-uuid",
        "authorType": "AGENT",
        "content": "Looking into this. Can you confirm the bank account on file?",
        "isInternal": false,
        "createdAt": "2026-04-22T14:30:00.000Z"
      }
    ]
  }
}

Top-level fields

Same as the list item plus:

FieldTypeNotes
messagesTicketMessageEmbedDto[]Full message history sorted by createdAt ASC (oldest first). Includes internal notes — filter client-side.

Message fields (messages[])

FieldTypeNotes
idstring (UUID)Message id. No public endpoint to fetch a single message — always read via the parent ticket.
ticketIdstring (UUID)Echo of the path parameter.
authorIdstring (UUID)The user (USER) or agent (AGENT) who wrote this message.
authorTypeenumUSER or AGENT.
contentstringPlain text — no HTML sanitization at this layer. Render with whiteSpace: pre-wrap for line breaks.
isInternalbooleanHide rows where this is true in the user-facing UI.
createdAtstring (ISO 8601)When the message was created.

Errors

HTTPCode / i18nKeyReason
400(ParseUUIDPipe):ticketId is not a UUID.
401(guard)Missing / invalid bearer token.
404support.ticket.not_foundTicket doesn't exist OR belongs to another user. Cases are intentionally indistinguishable.

Side effects

  1. Decode bearer; ParseUUIDPipe validates :ticketId.
  2. prisma.ticket.findUnique({ where: { id }, include: { messages: { orderBy: { createdAt: 'asc' } }, category: true } }).
  3. Owner check + missing collapsed: if (!ticket || ticket.userId !== userId) throw I18nNotFound.
  4. Return the ticket as-is. No DB writes, no notifications.

Code samples

curl https://api.bio.re/api/v1/tickets/a1b2c3d4-e5f6-7890-abcd-ef1234567890 \
  -H "Authorization: Bearer $ACCESS_TOKEN"
type TicketMessage = {
  id: string;
  ticketId: string;
  authorId: string;
  authorType: 'USER' | 'AGENT';
  content: string;
  isInternal: boolean;
  createdAt: string;
};

type TicketDetail = TicketListItem & { messages: TicketMessage[] };

async function getTicket(
  accessToken: string,
  ticketId: string,
): Promise<TicketDetail> {
  const res = await fetch(
    `https://api.bio.re/api/v1/tickets/${ticketId}`,
    { headers: { Authorization: `Bearer ${accessToken}` } },
  );
  const json = await res.json();
  if (!res.ok || !json.success) {
    throw Object.assign(new Error(json?.error?.message ?? 'Fetch failed'), {
      code: json?.error?.code,
    });
  }
  return json.data;
}

// In your render layer — strip internal notes before showing to the user:
const visible = (await getTicket(token, id)).messages.filter(m => !m.isInternal);
import { useQuery } from '@tanstack/react-query';

export function useTicketDetail(ticketId: string) {
  return useQuery({
    queryKey: ['support', 'ticket', ticketId],
    queryFn: async () => {
      const res = await fetch(`/api/v1/tickets/${ticketId}`);
      const json = await res.json();
      if (!res.ok || !json.success) {
        throw Object.assign(new Error(json?.error?.message ?? 'Fetch failed'), {
          code: json?.error?.code,
        });
      }
      return json.data as TicketDetail;
    },
    enabled: !!ticketId,
    // Refetch every 30s while the user is on this page; pair with a "send reply"
    // mutation that invalidates this query for instant updates after they post.
    refetchInterval: 30_000,
    staleTime: 0,
  });
}

Try it

GET
/api/v1/tickets/{ticketId}
AuthorizationBearer <token>

In: header

Path Parameters

ticketId*string

Response Body

application/json

application/json

application/json

curl -X GET "https://loading/api/v1/tickets/string"
{
  "success": true,
  "data": {
    "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
    "userId": "2c4a230c-5085-4924-a3e1-25fb4fc5965b",
    "categoryId": "337f5e5d-288b-40d5-be14-901cc3acacc0",
    "subject": "Payout delayed by 3 days",
    "status": "OPEN",
    "priority": "LOW",
    "assignedTo": "7869c1a9-a680-4086-b6f5-9e78a651f6f2",
    "resolvedAt": "2019-08-24T14:15:22Z",
    "closedAt": "2019-08-24T14:15:22Z",
    "createdAt": "2019-08-24T14:15:22Z",
    "updatedAt": "2019-08-24T14:15:22Z",
    "category": {
      "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
      "name": "Billing & Payments",
      "description": "string",
      "priority": "LOW",
      "active": true,
      "sortOrder": 1,
      "createdAt": "2019-08-24T14:15:22Z",
      "updatedAt": "2019-08-24T14:15:22Z"
    },
    "messages": [
      {
        "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
        "ticketId": "e3e3e8ea-b02a-4536-85a2-a9c90f4ee74f",
        "authorId": "ee6f7132-bd0a-4fcd-83b3-a8022377067b",
        "authorType": "USER",
        "content": "Details about: Payout delayed by 3 days",
        "isInternal": true,
        "createdAt": "2019-08-24T14:15:22Z"
      }
    ]
  }
}
{
  "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
Controllerapps/api-core/src/modules/support/support.controller.ts59–66 (get)
Serviceapps/api-core/src/modules/support/support.service.ts102–109 (getTicket — owner check collapses 404 + 403)
DTO (response)apps/api-core/src/modules/support/dto/support-response.dto.ts114–117 (TicketDetailDto), 85–106 (TicketMessageEmbedDto)
Prisma modelspackages/prisma/prisma/schema.prismaTicket lines 1245–1266, TicketMessage lines 1268–1279

On this page