BIO.RE
Support

Create Support Ticket

Open a new ticket as the current user. Returns just the new ticketId. Throttled to 5 per minute per IP. Optional category drives default priority. Sends a ticket_created notification fire-and-forget.

POST /api/v1/tickets โ€” ๐Ÿ”‘ Bearer ยท Rate limit: 5 req / minute

Creates a new support ticket owned by the current user. The first message body is created in the same DB transaction, so the ticket starts with exactly one user message and is ready for an agent to pick up.

Single-shot atomic write. The ticket row and its first TicketMessage are created in one Prisma $transaction([...]). Either both rows land or neither does โ€” there's no half-created state where the ticket exists but has no body.

Priority resolution order: explicit priority in body > category's default priority (if categoryId is set) > MEDIUM. The category is fetched only when categoryId is provided; if it doesn't exist, the request fails with support.category.not_found (404) before any ticket is created.

Notification is fire-and-forget. After the ticket is committed, the service calls notificationService.send({ eventKey: 'ticket_created', ... }) and does not await the result โ€” failures are logged but never bubble up. A 201 means the ticket was created; it does NOT mean the user has been notified yet.

Request

Body โ€” CreateTicketDto

FieldTypeRequiredValidationNotes
subjectstringโœ“MinLength(3) + MaxLength(200)Short title โ€” surface in the inbox row.
contentstringโœ“MinLength(10) + MaxLength(5000)First message body. Plain text โ€” no HTML sanitization at this layer.
categoryIdstring (UUID)optionalIsStringPre-existing category from the admin tool. If supplied and not found โ†’ 404.
priorityenumoptionalIsEnum(Priority) โ€” LOW / MEDIUM / HIGH / URGENTOverrides the category default. Omit to inherit from the category, or fall through to MEDIUM.

Headers

HeaderRequiredNotes
Authorization: Bearer <accessToken>โœ“Global JwtAuthGuard.
Content-Type: application/jsonโœ“Standard.

Response

201 Created โ€” ApiResponseOf<CreateTicketResponseDto>

{
  "success": true,
  "data": {
    "ticketId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
  }
}
FieldTypeNotes
ticketIdstring (UUID)Server-generated randomUUID(). Use it as :ticketId for detail, reply, reopen.

The ticket starts at status: 'OPEN' with the resolved priority. The first message is created with authorType: 'USER' and isInternal: false.

Errors

HTTPCode / i18nKeyReason
400(DTO validation)subject/content length out of range, missing fields, unknown enum value for priority.
401(guard)Missing / invalid bearer token.
404support.category.not_foundcategoryId was supplied but the row doesn't exist. No ticket is created.
429THROTTLE_LIMIT_EXCEEDEDMore than 5 requests / minute from this IP. Response carries retryAfter (seconds).

Side effects

  1. Decode bearer (global JwtAuthGuard).
  2. ThrottleGuard increments throttle:<ip>:POST:/api/v1/tickets in Redis cache; throws 429 if count > 5 in 60s window. Falls back to in-process Map if Redis cache is down.
  3. If categoryId is set: prisma.ticketCategory.findUnique({ where: { id }, select: { id, priority } }). Missing โ†’ throw I18nNotFound('support.category.not_found') (no rows written).
  4. Resolve priority: body.priority ?? cat.priority ?? Priority.MEDIUM.
  5. Generate ticketId = randomUUID().
  6. Atomic write via prisma.$transaction([...]):
    • Create Ticket row: { id: ticketId, userId, subject, status: 'OPEN', priority, categoryId }.
    • Create TicketMessage row: { id: randomUUID(), ticketId, authorId: userId, authorType: 'USER', content, isInternal: false }.
  7. Fire-and-forget notification: notificationService.send({ eventKey: 'ticket_created', userId, variables: { ticketId } }) โ€” .catch(...) logs errors only.
  8. Log [support] Ticket created: <id> (priority: <p>) and return { ticketId }.

Code samples

curl -X POST https://api.bio.re/api/v1/tickets \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{
    "subject": "Payout delayed by 3 days",
    "content": "I requested a payout on 2026-04-20 but I have not received the funds yet.",
    "categoryId": "11111111-2222-3333-4444-555555555555"
  }'
type CreateTicketBody = {
  subject: string;     // 3-200 chars
  content: string;     // 10-5000 chars
  categoryId?: string;
  priority?: 'LOW' | 'MEDIUM' | 'HIGH' | 'URGENT';
};

async function createTicket(
  accessToken: string,
  body: CreateTicketBody,
): Promise<{ ticketId: string }> {
  const res = await fetch('https://api.bio.re/api/v1/tickets', {
    method: 'POST',
    headers: {
      Authorization: `Bearer ${accessToken}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify(body),
  });
  const json = await res.json();
  if (!res.ok || !json.success) {
    throw Object.assign(new Error(json?.error?.message ?? 'Create failed'), {
      code: json?.error?.code,
      retryAfter: json?.retryAfter,  // present on 429
    });
  }
  return json.data;
}
import { useMutation, useQueryClient } from '@tanstack/react-query';

export function useCreateTicket() {
  const qc = useQueryClient();
  return useMutation({
    mutationFn: async (body: CreateTicketBody) => {
      const res = await fetch('/api/v1/tickets', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(body),
      });
      const json = await res.json();
      if (!res.ok || !json.success) {
        throw Object.assign(new Error(json?.error?.message ?? 'Create failed'), {
          code: json?.error?.code,
        });
      }
      return json.data as { ticketId: string };
    },
    onSuccess: () => {
      // The new ticket is OPEN โ€” invalidate the ticket list so it re-fetches.
      qc.invalidateQueries({ queryKey: ['support', 'tickets'] });
    },
  });
}

Try it

POST
/api/v1/tickets
AuthorizationBearer <token>

In: header

Request Body

application/json

TypeScript Definitions

Use the request body type in TypeScript.

Response Body

application/json

application/json

application/json

application/json

curl -X POST "https://loading/api/v1/tickets" \  -H "Content-Type: application/json" \  -d '{    "subject": "Payment not received",    "content": "I sent a message 3 days ago but the creator has not responded and my payment is still held."  }'
{
  "success": true,
  "data": {
    "ticketId": "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"
  }
}
{
  "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.ts48โ€“57 (create, @Throttle(5, 60))
DTO (request)apps/api-core/src/modules/support/dto/index.ts5โ€“17 (CreateTicketDto)
DTO (response)apps/api-core/src/modules/support/dto/support-response.dto.ts217โ€“220 (CreateTicketResponseDto)
Serviceapps/api-core/src/modules/support/support.service.ts52โ€“74 (createTicket โ€” category lookup, priority resolution, $transaction, notification)
Throttleapps/api-core/src/common/guards/throttle.guard.ts64โ€“79 (Redis), 81โ€“101 (in-memory fallback)
Prisma modelspackages/prisma/prisma/schema.prismaTicket lines 1245โ€“1266, TicketMessage lines 1268โ€“1279, TicketCategory lines 1281โ€“1292, Priority enum 819โ€“824

On this page