Skip to main content

Support Module

The decentralized, state‑driven ticketing engine – threaded conversations, polymorphic entity linking, and DPDP‑compliant privacy redaction.

Tickets Threaded Polymorphic Privacy


📖 Table of Contents


Base Route : /api/v1/support

Customer endpoints require a valid access token with USER role. Admin endpoints require ADMIN role.


Endpoints Overview

MethodEndpointAccessDescription
Customer
POST/ticketsPrivate (Bearer)Create a new support ticket (with optional linked entity and images)
POST/tickets/:ticketId/replyPrivate (Bearer)Customer replies to an existing ticket
GET/tickets/mePrivate (Bearer)Fetch authenticated user's ticket history (paginated)
Admin
GET/admin/ticketsAdminView all tickets across the platform (arbitration queue)
POST/admin/tickets/:ticketId/replyAdminAdmin replies to a customer ticket (auto‑shifts state)
PATCH/admin/tickets/:ticketId/stateAdminUpdate ticket status, priority, or assign to an admin

Rate limits: standardLimiter (100/15min) on all endpoints.


Ticket State Machine

Tickets follow a strict state machine to keep the arbitration queue clean.

Auto‑state shifts based on sender role:

SenderPrevious StatusNew Status
Admin (any)OPEN, IN_PROGRESS, RESOLVEDWAITING_ON_CUSTOMER
CustomerWAITING_ON_CUSTOMERIN_PROGRESS
CustomerOPENstays OPEN
CustomerRESOLVEDstays RESOLVED (no auto‑reopen)

Once a ticket is CLOSED, any reply attempt returns 403 Forbidden – user must create a new ticket.


Customer Endpoints

POST /tickets

Create a new support ticket. Optionally link to an order, product, or return (polymorphic). Images can be attached (max 3, each ≤10MB).

Headers: Authorization: Bearer <access_token>
Content-Type: multipart/form-data

Form fields (text):

FieldTypeRequiredDescription
subjectstringYesTicket subject (5‑150 chars)
categorystringYesORDER_ISSUE, PAYMENT_ISSUE, RETURN_ISSUE, PRODUCT_INQUIRY, TECHNICAL_ISSUE, GENERAL
messagestringYesInitial issue description (10‑3000 chars)
linkedEntity[entityType]stringNoORDER, PRODUCT, or RETURN
linkedEntity[entityId]stringNoValid MongoDB ObjectId of the linked entity

File field:

  • images – array of images (max 3, each ≤10MB).

Example request (cURL style):

subject=Missing item in delivery
category=ORDER_ISSUE
message=I received only 1 bangle set instead of 2.
linkedEntity[entityType]=ORDER
linkedEntity[entityId]=64a7b...
images=@photo1.jpg

Response (201 Created):

{
"success": true,
"statusCode": 201,
"message": "Support ticket created successfully. Our team will respond shortly.",
"data": {
"_id": "...",
"ticketId": "TCK-9A4B2C3D",
"subject": "Missing item in delivery",
"category": "ORDER_ISSUE",
"status": "OPEN",
"messages": [
{
"senderRole": "USER",
"message": "I received only 1 bangle set instead of 2.",
"attachments": ["https://res.cloudinary.com/..."],
"createdAt": "2026-05-22T10:30:00.000Z"
}
]
},
"timestamp": "2026-05-22T10:30:00.000Z"
}

IDOR protection: If a linked entity is provided, the server verifies that the user owns the linked order or return before creating the ticket.

POST /tickets/:ticketId/reply

Add a customer reply to an existing ticket. Attachments allowed.

Headers: Authorization: Bearer <access_token>
Content-Type: multipart/form-data

URL Parameter:

  • ticketId – the human‑readable ticket ID (e.g., TCK-9A4B2C3D), not the MongoDB _id.

Form fields:

FieldTypeRequiredDescription
messagestringYesReply text (2‑3000 chars)

File field:

  • images – max 3 images.

Response (200 OK): Returns the updated ticket with the new message appended.

The senderRole is hardcoded to USER in the controller – customers cannot spoof admin replies.

GET /tickets/me

Fetch the authenticated user's ticket history (paginated).

Headers: Authorization: Bearer <access_token>

Query parameters:

ParamTypeDefaultDescription
pageinteger1Page number
limitinteger10Items per page (max 50)
statusstringFilter by ticket status

Response (200 OK):

{
"success": true,
"statusCode": 200,
"message": "Support tickets retrieved successfully.",
"data": {
"tickets": [
{
"_id": "...",
"ticketId": "TCK-9A4B2C3D",
"subject": "Missing item in delivery",
"status": "WAITING_ON_CUSTOMER",
"createdAt": "2026-05-20T10:00:00.000Z"
}
],
"meta": { "total": 3, "limit": 10, "page": 1 }
},
"timestamp": "2026-05-22T10:35:00.000Z"
}

Admin Endpoints

GET /admin/tickets

View all tickets across the platform with filtering.

Headers: Authorization: Bearer <admin_access_token>

Query parameters:

ParamTypeDefaultDescription
pageinteger1Page number
limitinteger20Items per page (max 100)
statusstringFilter by ticket status
prioritystringLOW, MEDIUM, HIGH, CRITICAL
categorystringFilter by ticket category

Response (200 OK): Paginated list of tickets with user details (populated) and message preview.


POST /admin/tickets/:ticketId/reply

Admin replies to a customer ticket. Auto‑shifts the ticket status to WAITING_ON_CUSTOMER (unless already closed).

Headers: Authorization: Bearer <admin_access_token>
Content-Type: multipart/form-data

URL Parameter:

  • ticketId – human‑readable ticket ID.

Form fields: Same as customer reply (message + optional images).

Response (200 OK): Returns updated ticket with the new admin message.

After an admin reply, an email notification is sent to the customer (fire‑and‑forget via BullMQ).


PATCH /admin/tickets/:ticketId/state

Update ticket status, priority, or assign to another admin. At least one field must be provided.

Headers: Authorization: Bearer <admin_access_token>
Content-Type: application/json

URL Parameter:

  • ticketId – human‑readable ticket ID.

Request body (partial):

{
"status": "CLOSED",
"priority": "HIGH",
"assignedAdmin": "64a7b..."
}

Response (200 OK): Returns the updated ticket.

Assigning an admin is optional – tickets can be unassigned (assignedAdmin: null).


Validation Rules (Zod)

EndpointFieldRules
POST /ticketssubjectstring, 5‑150 chars, required
categoryenum TicketCategory, required
messagestring, 10‑3000 chars, required
linkedEntity.entityTypeenum LinkedEntityType (ORDER, PRODUCT, RETURN), optional
linkedEntity.entityIdvalid ObjectId, required if entityType present
POST /:ticketId/replymessagestring, 2‑3000 chars, required
PATCH /admin/tickets/:ticketId/statestatusenum TicketStatus, optional
priorityenum TicketPriority, optional
assignedAdminvalid ObjectId, optional
(at least one field required)refine

All schemas use .strict() – extra fields are rejected.


Error Responses

StatusCodeExample messageWhen
400BAD_REQUEST"At least one field must be provided for an update"Empty state update
401UNAUTHORIZED"Authentication required"Missing/invalid token
403FORBIDDEN"This ticket has been closed. Please open a new ticket."Reply to closed ticket
403FORBIDDEN"You do not have permission to perform this action"Non‑admin on admin route
404NOT_FOUND"The specified order could not be found or does not belong to you"Linked entity IDOR check fails
404NOT_FOUND"Support ticket not found"Invalid ticket ID
429TOO_MANY_REQUESTS"Too many requests, please try again later."Rate limit exceeded

Runbook

For manual testing with Thunder Client / Postman, see ../../testing/support-runbook.md. The runbook covers:

  • Customer creating a ticket with linked order
  • Customer replying to ticket
  • Admin replying (state machine shift to WAITING_ON_CUSTOMER)
  • Admin updating ticket state (close, escalate priority)
  • Edge cases: closed ticket reply, IDOR on linked entity


Customer first, privacy compliant – the Reshma‑Core support engine.