BIO.RE
Support

Reopen Ticket

Move a RESOLVED or CLOSED ticket back to OPEN. Clears resolvedAt + closedAt. Owner-only. Fails if the ticket is in any other status.

POST /api/v1/tickets/:ticketId/reopen — 🔑 Bearer

Transitions a ticket from RESOLVED or CLOSED back to OPEN. Both resolvedAt and closedAt are nulled in the same update. Use this when the user's underlying issue wasn't actually solved and they need an agent to look again.

Only RESOLVED and CLOSED can be reopened. The transition map says RESOLVED → CLOSED, OPEN and CLOSED → OPEN. Trying to reopen an already-active ticket (OPEN, ASSIGNED, IN_PROGRESS, WAITING_USER, WAITING_INTERNAL) returns 400 support.ticket.invalid_transition with the current status in the error payload. The transition validator runs before the ownership check.

Error precedence: not-found > invalid-transition > not-owner. The validator runs findUnique first (404 if missing), then checks the transition map (400 if not in RESOLVED/CLOSED), and only then does the ownership check (403). So if you try to reopen someone else's already-OPEN ticket, you get 400, not 403 — the existence + state of the ticket are revealed before authorization. Worth knowing if you build any abuse rules around this.

No notification fires from this endpoint. Unlike create and reply, the reopen path doesn't emit any ticket_* event — the admin/agent will see the ticket back at the top of the OPEN queue on their next refresh, but no push/email is sent.

Request

Path parameters

ParamTypeRequiredValidationNotes
ticketIdstring (UUID)ParseUUIDPipeYour ticket — must currently be in RESOLVED or CLOSED.

Headers

HeaderRequiredNotes
Authorization: Bearer <accessToken>Global JwtAuthGuard.

No body.

Response

200 OKSuccessOnlyResponseDto

{ "success": true }

After this returns, fetching GET /tickets/:ticketId shows:

  • status: 'OPEN'
  • resolvedAt: null
  • closedAt: null
  • existing messages[] unchanged (no new system message is appended)

The ticket is now back in the agent queue with no assignment carry-over (the previous assignedTo value is left as-is — only status, resolvedAt, closedAt are touched).

Errors

HTTPCode / i18nKeyReason
400(ParseUUIDPipe):ticketId is not a UUID.
400support.ticket.invalid_transitionTicket is in a status that doesn't allow OPEN (anything except RESOLVED / CLOSED). Payload: { currentStatus, targetStatus: 'OPEN' }.
401(guard)Missing / invalid bearer token.
403support.ticket.not_ownerThe ticket exists, is in RESOLVED/CLOSED, but belongs to another user. (See note above on error precedence.)
404support.ticket.not_foundTicket does not exist.

Side effects

  1. Decode bearer; ParseUUIDPipe validates :ticketId.
  2. Transition validator first: prisma.ticket.findUnique({ where: { id } }). Missing → throw 404. Then check status is in ['RESOLVED', 'CLOSED']; if not → throw 400 invalid_transition.
  3. Owner check second: if (ticket.userId !== userId) throw 403 not_owner.
  4. prisma.ticket.update({ where: { id }, data: { status: 'OPEN', resolvedAt: null, closedAt: null } }) — no transaction (single row write).
  5. No notification, no log mutation, no message append. The audit trail of "user reopened" lives only in the status/timestamp diff.
  6. Return { success: true }.

Code samples

curl -X POST https://api.bio.re/api/v1/tickets/a1b2c3d4-e5f6-7890-abcd-ef1234567890/reopen \
  -H "Authorization: Bearer $ACCESS_TOKEN"
async function reopenTicket(
  accessToken: string,
  ticketId: string,
): Promise<void> {
  const res = await fetch(
    `https://api.bio.re/api/v1/tickets/${ticketId}/reopen`,
    {
      method: 'POST',
      headers: { Authorization: `Bearer ${accessToken}` },
    },
  );
  const json = await res.json();
  if (!res.ok || !json.success) {
    throw Object.assign(new Error(json?.error?.message ?? 'Reopen failed'), {
      code: json?.error?.code,
      // For invalid_transition, the API includes currentStatus in the payload:
      currentStatus: json?.error?.payload?.currentStatus,
    });
  }
}
import { useMutation, useQueryClient } from '@tanstack/react-query';

export function useReopenTicket(ticketId: string) {
  const qc = useQueryClient();
  return useMutation({
    mutationFn: async () => {
      const res = await fetch(`/api/v1/tickets/${ticketId}/reopen`, {
        method: 'POST',
      });
      const json = await res.json();
      if (!res.ok || !json.success) {
        throw Object.assign(new Error(json?.error?.message ?? 'Reopen failed'), {
          code: json?.error?.code,
        });
      }
    },
    onSuccess: () => {
      // status flipped to OPEN, resolvedAt/closedAt cleared
      qc.invalidateQueries({ queryKey: ['support', 'ticket', ticketId] });
      qc.invalidateQueries({ queryKey: ['support', 'tickets'] });
    },
  });
}

Try it

POST
/api/v1/tickets/{ticketId}/reopen
AuthorizationBearer <token>

In: header

Path Parameters

ticketId*string

Response Body

application/json

application/json

application/json

application/json

curl -X POST "https://loading/api/v1/tickets/string/reopen"
{
  "success": true
}
{
  "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.ts80–90 (reopen)
Serviceapps/api-core/src/modules/support/support.service.ts130–134 (reopenTicket — transition validator + owner check + null timestamps)
Transition validatorapps/api-core/src/modules/support/support.service.ts36–48 (validateTicketTransition), 12–20 (VALID_TICKET_TRANSITIONS — only RESOLVED → OPEN and CLOSED → OPEN allowed)
Prisma modelpackages/prisma/prisma/schema.prismaTicket lines 1245–1266, TicketStatus enum 809–817

On this page