Service Request Service API
The Service Request module is part of the Organisation Service (internal port 9107, gateway prefix /organisation). It exposes two distinct route groups:
/admin/service-requests/*— used by the Admin Portal (support staff, requiresisAdmin === true)./hub/service-requests/*— used by the Hub (organisation clients, scoped server-side to the caller’sdefaultOrganisationId).
All endpoints require a valid Bearer token (Authorization: Bearer <accessToken>).
Concepts
Types
| Type | Use |
|---|---|
INCIDENT |
An unplanned interruption / degradation of service. |
BUG |
A defect in the product to be reproduced and fixed. |
CHANGE_REQUEST |
A planned change requiring approval and a rollback plan. |
GENERAL |
Anything else (Q&A, feature ask, billing query…). |
Status lifecycle
NEW → OPEN → IN_PROGRESS ⇄ PENDING_CUSTOMER ⇄ ON_HOLD
IN_PROGRESS → RESOLVED → CLOSED
RESOLVED → REOPENED → IN_PROGRESS
* → CANCELLED (admin only, except NEW/OPEN which a client can also cancel)
CLOSED → REOPENED (within reopen window)
Invalid transitions return HTTP 409. Cancelling once a request is IN_PROGRESS requires admin.
Priority / Severity / Urgency / Impact
| Field | Values |
|---|---|
priority |
LOW, MEDIUM (default), HIGH, URGENT |
severity |
S1, S2, S3, S4 |
urgency |
LOW, MEDIUM, HIGH |
impact |
LOW, MEDIUM, HIGH |
Type-specific metadata (typeMeta)
typeMeta: {
bug?: {
environment?: 'DEV' | 'STAGING' | 'PROD';
affectedVersion?: string;
browser?: string;
os?: string;
stepsToReproduce?: string;
expectedBehaviour?: string;
actualBehaviour?: string;
affectedModule?: string;
};
incident?: {
affectedService?: string;
affectedUsersCount?: number;
incidentStartAt?: ISODate;
incidentEndAt?: ISODate;
rootCause?: string;
workaround?: string;
postMortemUrl?: string;
};
change?: {
changeType?: 'STANDARD' | 'NORMAL' | 'EMERGENCY';
proposedChange?: string;
justification?: string;
riskLevel?: 'LOW' | 'MEDIUM' | 'HIGH';
rollbackPlan?: string;
plannedStartAt?: ISODate;
plannedEndAt?: ISODate;
// Set by the approve-change endpoint:
approvalStatus?: 'PENDING' | 'APPROVED' | 'REJECTED';
approverId?: string;
approvedAt?: ISODate;
approvalNote?: string;
};
}
Ticket numbers
Auto-assigned in the form SR-YYYY-NNNNNN (zero-padded, monotonically increasing per year). Generated atomically; safe under concurrency.
Admin endpoints
Base prefix: /organisation/admin/service-requests (via gateway). All require an admin user.
POST /admin/service-requests
Create a request. Admin can target any organisation, leave organisationId = null for a standalone request, or set onBehalfOfUserId to record that the request was raised on behalf of another user.
{
"type": "BUG",
"title": "Pricing page 500s on Safari",
"description": "Repro steps inside",
"priority": "HIGH",
"severity": "S2",
"category": "Frontend",
"organisationId": "1f2c…", // optional, null for standalone
"onBehalfOfUserId": "9a3e…", // optional
"assigneeId": "auser…", // optional
"tags": ["safari", "p1"],
"typeMeta": {
"bug": { "environment": "PROD", "browser": "Safari 17", "stepsToReproduce": "…" }
}
}
Response 201 Created — full admin view of the request, including the assigned ticketNumber.
GET /admin/service-requests
List with filters (all optional, query-string):
| Param | Notes |
|---|---|
type |
Single INCIDENT / BUG / CHANGE_REQUEST / GENERAL |
status |
Comma-separated statuses |
priority |
Comma-separated priorities |
organisationId |
Filter by organisation |
assigneeId, requesterId, tag |
Exact-match filters |
q |
Case-insensitive substring on title/description/ticketNumber |
createdFrom, createdTo |
ISO 8601 |
page (default 1), pageSize (default 20, max 100) |
Pagination |
sort |
Comma-separated; prefix with - for descending. Default: -createdAt |
includeDeleted |
true to include soft-deleted rows |
Response: { items, page, pageSize, total, totalPages }.
GET /admin/service-requests/stats
Aggregated counts for dashboards. Optional organisationId, assigneeId filters.
{
"total": 124,
"slaBreached": 3,
"byStatus": { "NEW": 12, "IN_PROGRESS": 40, "RESOLVED": 60, "CLOSED": 12 },
"byType": { "BUG": 70, "INCIDENT": 30, "CHANGE_REQUEST": 14, "GENERAL": 10 },
"byPriority": { "LOW": 20, "MEDIUM": 60, "HIGH": 35, "URGENT": 9 }
}
GET /admin/service-requests/:id
Returns the full admin view (including internal comments, complete history).
PATCH /admin/service-requests/:id
Update any of: title, description, priority, severity, urgency, impact, category, subCategory, tags, dueAt, assigneeId, assigneeTeam, typeMeta. Each changed field is recorded as a history entry.
POST /admin/service-requests/:id/status
{ "status": "IN_PROGRESS", "note": "Engineer assigned, investigating" }
Returns 409 if the transition isn’t allowed for admins. RESOLVED sets resolvedAt; CLOSED sets closedAt; REOPENED increments reopenCount.
POST /admin/service-requests/:id/assign
{ "assigneeId": "uuid", "assigneeTeam": "Tier-2 Support", "note": "L2 escalation" }
Either field can be supplied. Triggers an assignee.changed email notification to the assignee (if their email is on file).
POST /admin/service-requests/:id/comments & GET /admin/service-requests/:id/comments
Add or list comments. Admins can post internal comments ("isInternal": true) which are hidden from the Hub view. The first non-internal comment sets firstResponseAt if it isn’t already set.
POST /admin/service-requests/:id/attachments (multipart)
Field name: file. Up to 10 MB. Allowed types: PNG/JPG/WEBP/GIF, PDF, plain text/CSV/JSON, ZIP, DOC(X)/XLS(X). Stored on Cloudinary under zeswa/service-requests/<id>/. Returns the updated request.
DELETE /admin/service-requests/:id/attachments/:attachmentId
Removes an attachment from the request and records the action in history.
GET /admin/service-requests/:id/history
Full audit trail (every status change, field edit, comment, attachment, assignment).
POST /admin/service-requests/:id/approve-change
Only valid when type === "CHANGE_REQUEST".
{ "decision": "APPROVED", "note": "Reviewed with infra; rollback ok" }
Sets typeMeta.change.approvalStatus, approverId, approvedAt, approvalNote.
DELETE /admin/service-requests/:id
Soft delete. Sets deletedAt; the request disappears from default lists. Recoverable via ?includeDeleted=true on the list endpoint and a manual DB restore (no API to undelete).
Hub endpoints
Base prefix: /organisation/hub/service-requests. All require a logged-in user with a defaultOrganisationId. The server always scopes reads/writes to that organisation; clients cannot read other organisations’ requests.
POST /hub/service-requests
{
"type": "BUG",
"title": "Cannot upload avatar",
"description": "When I select a PNG it fails with 500",
"priority": "HIGH",
"tags": ["avatar"],
"typeMeta": { "bug": { "environment": "PROD", "browser": "Chrome 120" } }
}
organisationId, requesterId, requesterType are set server-side.
GET /hub/service-requests
Same filter shape as admin minus organisationId (forced to caller’s org). Add mineOnly=true to restrict to the caller’s own requests. Internal comments are excluded.
GET /hub/service-requests/:id
Returns hub view. 404 if the request does not belong to the caller’s organisation (intentionally indistinguishable from “not found”).
PATCH /hub/service-requests/:id
Edit title / description / category / subCategory / tags / typeMeta while the request is in an editable state (NEW, OPEN, REOPENED, PENDING_CUSTOMER). Only the original requester may edit.
POST /hub/service-requests/:id/comments
Public comment only (isInternal is ignored; always false).
POST /hub/service-requests/:id/attachments (multipart)
Same rules as the admin attachment endpoint.
POST /hub/service-requests/:id/cancel
Sets status to CANCELLED. Allowed only while the request is NEW or OPEN. Only the requester may cancel.
POST /hub/service-requests/:id/confirm-resolution
Confirms a RESOLVED ticket and closes it; optional CSAT:
{ "rating": 5, "comment": "Quick fix, thanks!" }
POST /hub/service-requests/:id/reopen
Re-opens a RESOLVED or CLOSED ticket; increments reopenCount.
Notifications
The platform queues an email via the existing BullMQ email-queue on these events:
| Event | Recipient |
|---|---|
| Ticket created | Requester |
| Status changed | Requester |
| Assignee changed | New assignee |
Templates live under emails/service-request/.
Errors
| Status | When |
|---|---|
| 400 | Validation failure (Zod returns the first error message and the full errors array). |
| 401 | Missing / invalid token, or the authenticated user record is gone. |
| 403 | Admin endpoint hit by a non-admin; or non-requester editing a hub request. |
| 404 | Request not found (or not in your organisation, on Hub routes). |
| 409 | Disallowed status transition; approving a non-CHANGE_REQUEST; or wrong type/state. |