BIO.RE
Support

List My Tickets

Owner-scoped paginated list of the current user's support tickets. Filter by status. Always returns YOUR tickets — no admin lens, no cross-user reads possible.

GET /api/v1/tickets — 🔑 Bearer

Returns a page of tickets owned by the current user (the bearer-token subject). Always filtered by userId = req.user.id server-side — there is no way to ask for someone else's tickets through this endpoint, regardless of query params.

No throttle on this endpoint. The class registers @UseGuards(ThrottleGuard) but only the ticket-create handler carries @Throttle(5, 60). The list / detail / reply / reopen handlers have no @Throttle() decorator, and ThrottleGuard.canActivate() returns true when no decorator is found. List as freely as your client needs to.

status filter takes one value. It's an enum string — OPEN, ASSIGNED, IN_PROGRESS, WAITING_USER, WAITING_INTERNAL, RESOLVED, or CLOSED. There's no IN(...) or comma-separated form. To show "active" tickets (not RESOLVED/CLOSED), omit the filter and partition client-side, or fire two parallel calls.

Request

Query parameters

ParamTypeDefaultNotes
pageinteger (1-based)1Page number. Parsed via parseInt?page=abc becomes NaN, then defaults to 1.
limitinteger20Items per page. Service caps at 100 (Math.min(limit, 100)).
statusenumOne of: OPEN, ASSIGNED, IN_PROGRESS, WAITING_USER, WAITING_INTERNAL, RESOLVED, CLOSED. Single value only.

Headers

HeaderRequiredNotes
Authorization: Bearer <accessToken>Global JwtAuthGuard enforces.

No body.

Response

200 OKPaginatedApiResponseOf<TicketListItemDto>

{
  "success": true,
  "data": {
    "items": [
      {
        "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"
        }
      }
    ],
    "total": 42,
    "page": 1,
    "limit": 20,
    "totalPages": 3
  }
}

Item fields (items[])

FieldTypeNotes
idstring (UUID)Use as :ticketId for the detail/reply/reopen endpoints.
userIdstring (UUID)Always equals the authenticated user's id.
categoryIdstring (UUID) | nullOptional category at create time. Cascades to null if the category is deleted.
subjectstring3-200 chars (validated on create).
statusenumSee state machine below.
priorityenumLOW / MEDIUM / HIGH / URGENT. Defaults from category at create time, otherwise MEDIUM.
assignedTostring (UUID) | nullAgent currently assigned (admin-side action — user has no control).
resolvedAtstring (ISO 8601) | nullSet when ticket transitions to RESOLVED. Cleared on reopen.
closedAtstring (ISO 8601) | nullSet when ticket transitions to CLOSED. Cleared on reopen.
createdAt / updatedAtstring (ISO 8601)Standard timestamps.
categoryobject | nullEmbedded full TicketCategoryDto (when set). Always included; not categoryId-only.

Pagination envelope

FieldTypeNotes
totalnumberTotal tickets matching the filter (for the current user).
page / limitnumberEcho of the effective values (after defaulting + capping).
totalPagesnumberMath.ceil(total / limit).

Errors

HTTPReason
401Missing / invalid bearer token.

Status state machine

status is one of these states. Transitions are enforced server-side (see reopen):

StateSet byNext legal states
OPENTicket creationASSIGNED
ASSIGNEDAdmin assigns agentIN_PROGRESS, RESOLVED, CLOSED
IN_PROGRESSAdmin/agent reply, user reply on WAITING_USERWAITING_USER, WAITING_INTERNAL, RESOLVED, CLOSED
WAITING_USERAgent replyIN_PROGRESS (on user reply), RESOLVED, CLOSED
WAITING_INTERNALAdmin escalationIN_PROGRESS, RESOLVED, CLOSED
RESOLVEDAdmin resolvesCLOSED, OPEN (via reopen)
CLOSEDAdmin closesOPEN (via reopen)

Side effects

  1. Decode bearer token (global JwtAuthGuard); req.user.id is forwarded as @CurrentUser('id').
  2. Service builds where = { userId } plus optional status. The userId clause is unconditional — even if the controller forwarded an empty body or weird query, the WHERE always filters to the current user.
  3. Two parallel Prisma queries:
    • prisma.ticket.findMany({ where, orderBy: { createdAt: 'desc' }, skip, take, include: { category: true } })
    • prisma.ticket.count({ where })
  4. Compute totalPages = Math.ceil(total / limit). Return the envelope.

No mutations. No notifications, no logs, no Redis touches.

Code samples

# All my tickets, page 1, 20 per page
curl https://api.bio.re/api/v1/tickets \
  -H "Authorization: Bearer $ACCESS_TOKEN"

# Only OPEN tickets
curl 'https://api.bio.re/api/v1/tickets?status=OPEN' \
  -H "Authorization: Bearer $ACCESS_TOKEN"
type TicketStatus =
  | 'OPEN' | 'ASSIGNED' | 'IN_PROGRESS'
  | 'WAITING_USER' | 'WAITING_INTERNAL' | 'RESOLVED' | 'CLOSED';

type TicketListItem = {
  id: string;
  userId: string;
  categoryId: string | null;
  subject: string;
  status: TicketStatus;
  priority: 'LOW' | 'MEDIUM' | 'HIGH' | 'URGENT';
  assignedTo: string | null;
  resolvedAt: string | null;
  closedAt: string | null;
  createdAt: string;
  updatedAt: string;
  category: { id: string; name: string; priority: string } | null;
};

type TicketListResponse = {
  items: TicketListItem[];
  total: number;
  page: number;
  limit: number;
  totalPages: number;
};

async function listTickets(
  accessToken: string,
  opts?: { page?: number; limit?: number; status?: TicketStatus },
): Promise<TicketListResponse> {
  const url = new URL('https://api.bio.re/api/v1/tickets');
  if (opts?.page) url.searchParams.set('page', String(opts.page));
  if (opts?.limit) url.searchParams.set('limit', String(opts.limit));
  if (opts?.status) url.searchParams.set('status', opts.status);

  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 ?? 'List failed'), {
      code: json?.error?.code,
    });
  }
  return json.data;
}
import { useQuery, keepPreviousData } from '@tanstack/react-query';

export const supportKeys = {
  list: (opts: { page: number; status?: TicketStatus }) =>
    ['support', 'tickets', opts] as const,
  detail: (ticketId: string) => ['support', 'ticket', ticketId] as const,
};

export function useMyTickets(page = 1, status?: TicketStatus) {
  return useQuery({
    queryKey: supportKeys.list({ page, status }),
    queryFn: async () => {
      const url = new URL('/api/v1/tickets', location.origin);
      url.searchParams.set('page', String(page));
      if (status) url.searchParams.set('status', status);

      const res = await fetch(url);
      const json = await res.json();
      if (!res.ok || !json.success) {
        throw Object.assign(new Error(json?.error?.message ?? 'List failed'), {
          code: json?.error?.code,
        });
      }
      return json.data as TicketListResponse;
    },
    placeholderData: keepPreviousData,
    staleTime: 30_000,
  });
}

Try it

GET
/api/v1/tickets
AuthorizationBearer <token>

In: header

Query Parameters

page?number

Page number (1-based)

limit?number

Items per page (max 100)

status?string

Filter by ticket status

Value in"OPEN" | "ASSIGNED" | "IN_PROGRESS" | "WAITING_USER" | "WAITING_INTERNAL" | "RESOLVED" | "CLOSED"

Response Body

application/json

curl -X GET "https://loading/api/v1/tickets"
{
  "success": true,
  "data": {
    "items": [
      {
        "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"
        }
      }
    ],
    "page": 1,
    "limit": 20,
    "total": 150,
    "totalPages": 8
  }
}
Empty

Source

SourcePathLines
Controllerapps/api-core/src/modules/support/support.controller.ts20–46 (class), 27–46 (list)
Serviceapps/api-core/src/modules/support/support.service.ts216–230 (listTickets — owner filter, ordering, pagination)
DTO (response)apps/api-core/src/modules/support/dto/support-response.dto.ts41–77 (TicketListItemDto), 9–33 (TicketCategoryDto)
Throttle behaviorapps/api-core/src/common/guards/throttle.guard.ts43–48 (no-decorator → return true)
Global JWT guardapps/api-core/src/app.module.ts62 (APP_GUARD: JwtAuthGuard)
Prisma modelpackages/prisma/prisma/schema.prismaTicket lines 1245–1266, TicketStatus enum 809–817, Priority enum 819–824

On this page