Super People Docs
API Reference

Grievances

The anonymous grievance channel: raising, reading and answering grievances, and the super-admin queue.

The anonymous grievance channel: raising, reading and answering grievances, and the super-admin queue.

Generated from the OpenAPI spec by npm run docs:api -w api. Do not edit this page — change the decorators in apps/api/src/ and regenerate.

Conventions

  • Auth — a session on every route: the cookie on web, the bearer token on mobile. See Authentication.
  • Errors — every non-2xx response has the body described in Errors.
  • Types — a ? after a field name means it is optional; | null means it is present but may be null.
  • Guide — Grievance Channel walkthrough.

Endpoints

POST /api/v1/grievances — Raise a grievance

Open to anyone signed in. Filed as the signed-in caller — there is no way to name a different submitter, and sending submittedById is rejected with a 400. No response anywhere in this API ever says who raised it, to any role, ever. The grievance itself is visible to every employee the moment it is filed; only the identity is private. Attachments are not supported yet.

Body — CreateGrievanceDto

Returns 201 → GrievanceResponse

ErrorWhen
400The request was rejected. message is an array of field errors when validation failed.
401No valid session. Clear the stored token and sign in again.
403Signed in, but not allowed to do this.

GET /api/v1/grievances — The grievance board

Every grievance raised in the company, newest first by default, each one attributed to "Anonymous". Filter with category and status. There is no "only mine" filter, deliberately — see the note on the query DTO. One employee’s board is identical to another’s, apart from unread. total counts the whole filtered set, not the page.

QueryTypeRequiredNotes
limitnumbernoRows per page. Defaults to 20, capped at 50.
statusopen | in_review | resolved | closedno
categoryharassment | discrimination | workplace_safety | compensation | management | workload | policy | facilities | otherno
sortlatest | oldestno
cursorstringnoThe nextCursor from the previous page. Omit for the first.

Returns 200 → Page<GrievanceResponse> — see Paging

ErrorWhen
400The request was rejected. message is an array of field errors when validation failed.
401No valid session. Clear the stored token and sign in again.
403Signed in, but not allowed to do this.

GET /api/v1/grievances/unread-count — How many grievances you have not opened

For the sidebar badge. Counts grievances you did not raise and have not yet marked read. Per-person: another employee opening one does not change your count, and yours never counts your own.

Returns 200 → CountResponse

ErrorWhen
401No valid session. Clear the stored token and sign in again.
403Signed in, but not allowed to do this.

GET /api/v1/grievances/{grievanceId} — One grievance

Any grievance, raised by anybody. The submitter is not named, here or anywhere.

Path parameterTypeNotes
grievanceIdstring

Returns 200 → GrievanceResponse

ErrorWhen
401No valid session. Clear the stored token and sign in again.
403Signed in, but not allowed to do this.
404No such grievance.

POST /api/v1/grievances/{grievanceId}/read — Mark a grievance read

Call it when the thread opens. Read state is yours alone — it never marks the grievance read for anybody else — and calling it twice keeps the first time.

Path parameterTypeNotes
grievanceIdstring

Returns 204 → no body

ErrorWhen
401No valid session. Clear the stored token and sign in again.
403Signed in, but not allowed to do this.
404No such grievance.

GET /api/v1/grievances/{grievanceId}/replies — The thread on a grievance

Oldest first, so it reads as a conversation. Every message is attributed to "Anonymous" — the person who raised it, colleagues answering, and the grievance team alike. authorRole says which side wrote it without naming anybody.

Path parameterTypeNotes
grievanceIdstring
QueryTypeRequiredNotes
cursorstringnoThe nextCursor from the previous page. Omit for the first.
limitnumbernoRows per page. Defaults to 20, capped at 50.

Returns 200 → Page<GrievanceReplyResponse> — see Paging

ErrorWhen
401No valid session. Clear the stored token and sign in again.
403Signed in, but not allowed to do this.
404No such grievance.

POST /api/v1/grievances/{grievanceId}/replies — Reply to a grievance

Answer any grievance on the board — your own or a colleague’s. The reply is anonymous like everything else here; there is no per-reply choice, and sending one is a 400. Tag a colleague by putting an @[userId] token in body, the same format the feed uses: they are notified that they were mentioned in an anonymous grievance, and you stay anonymous to them. Adding a reply does not move the status or reorder the board. No attachments.

Path parameterTypeNotes
grievanceIdstring

Body — CreateGrievanceReplyDto

Returns 201 → GrievanceReplyResponse

ErrorWhen
400The request was rejected. message is an array of field errors when validation failed.
401No valid session. Clear the stored token and sign in again.
403Signed in, but not allowed to do this.
404No such grievance.

GET /api/v1/grievances/{grievanceId}/status-changes — How a grievance has been handled

Every status move with the reason given for it, newest first, starting from the entry that records it being raised. This is what answers “why is this still open” — show it wherever you show the status. Each entry is attributed to the grievance team, never to an individual handler.

Path parameterTypeNotes
grievanceIdstring

Returns 200 → GrievanceStatusChangeResponse[]

ErrorWhen
401No valid session. Clear the stored token and sign in again.
403Signed in, but not allowed to do this.
404No such grievance.

PATCH /api/v1/grievances/{grievanceId}/replies/{replyId} — Edit one of your own messages

Only the person who wrote it — employee or grievance team alike. The message is marked edited (editedAt), and anybody newly tagged is told. Anyone else gets a 403, which says nothing about who did write it.

Path parameterTypeNotes
grievanceIdstring
replyIdstring

Body — CreateGrievanceReplyDto

Returns 200 → GrievanceReplyResponse

ErrorWhen
400The request was rejected. message is an array of field errors when validation failed.
401No valid session. Clear the stored token and sign in again.
403Signed in, but not allowed to do this.
404No such message on that grievance.

DELETE /api/v1/grievances/{grievanceId}/replies/{replyId} — Delete one of your own messages

Only the person who wrote it. Removed from the thread for everybody, for good.

Path parameterTypeNotes
grievanceIdstring
replyIdstring

Returns 204 → no body

ErrorWhen
401No valid session. Clear the stored token and sign in again.
403Signed in, but not allowed to do this.
404No such message on that grievance.

GET /api/v1/admin/grievances — Every grievance

Grievance handlers (super admins) only — the same grievances every employee sees at /api/v1/grievances, in the same shape, with the filters somebody working through a queue wants. Nobody is named here either.

QueryTypeRequiredNotes
limitnumbernoRows per page. Defaults to 20, capped at 50.
statusopen | in_review | resolved | closedno
categoryharassment | discrimination | workplace_safety | compensation | management | workload | policy | facilities | otherno
sortlatest | oldestnoNewest or oldest first. Defaults to latest.
cursorstringnoThe nextCursor from the previous page. Omit for the first.

Returns 200 → Page<GrievanceResponse> — see Paging

ErrorWhen
400The request was rejected. message is an array of field errors when validation failed.
401No valid session. Clear the stored token and sign in again.
403Signed in, but not allowed to do this.

GET /api/v1/admin/grievances/{grievanceId} — One grievance

Same shape as a queue row. Anonymous grievances still have submitter: null.

Path parameterTypeNotes
grievanceIdstring

Returns 200 → GrievanceResponse

ErrorWhen
401No valid session. Clear the stored token and sign in again.
403Signed in, but not allowed to do this.
404No such grievance.

PATCH /api/v1/admin/grievances/{grievanceId}/status — Move a grievance to a new status, and say why

open → in_review → resolved / closed, in any order — a closed grievance can be reopened. resolvedAt is set on the first move to resolved or closed and cleared on a reopen. reason is required. It is shown to the person who raised the grievance, attributed to the grievance team rather than to you, and kept on the handling record — a status that changes with nothing said is the thing this channel is least able to afford.

Path parameterTypeNotes
grievanceIdstring

Body — UpdateGrievanceStatusDto

Returns 200 → GrievanceResponse

ErrorWhen
400The request was rejected. message is an array of field errors when validation failed.
401No valid session. Clear the stored token and sign in again.
403Signed in, but not allowed to do this.
404No such grievance.

GET /api/v1/admin/grievances/{grievanceId}/status-changes — How a grievance has been handled

Every status move with the reason given for it, newest first, starting from the entry that records it being raised. Carries changedBy so the team can see who made each call; the employee’s copy of this record does not.

Path parameterTypeNotes
grievanceIdstring

Returns 200 → AdminGrievanceStatusChangeResponse[]

ErrorWhen
401No valid session. Clear the stored token and sign in again.
403Signed in, but not allowed to do this.
404No such grievance.

GET /api/v1/admin/grievances/{grievanceId}/replies — The thread on a grievance

Oldest first. A handler’s reply carries author so the team can see which of them answered; an employee’s reply never does, whether they raised the grievance or are answering it.

Path parameterTypeNotes
grievanceIdstring
QueryTypeRequiredNotes
cursorstringnoThe nextCursor from the previous page. Omit for the first.
limitnumbernoRows per page. Defaults to 20, capped at 50.

Returns 200 → Page<AdminGrievanceReplyResponse> — see Paging

ErrorWhen
401No valid session. Clear the stored token and sign in again.
403Signed in, but not allowed to do this.
404No such grievance.

POST /api/v1/admin/grievances/{grievanceId}/replies — Answer a grievance

Adds a handler’s reply to the thread. The person who raised it sees it attributed to the grievance team, not to you by name. Replying does not move the status — do that explicitly, so the queue keeps saying what is true.

Path parameterTypeNotes
grievanceIdstring

Body — CreateGrievanceReplyDto

Returns 201 → AdminGrievanceReplyResponse

ErrorWhen
400The request was rejected. message is an array of field errors when validation failed.
401No valid session. Clear the stored token and sign in again.
403Signed in, but not allowed to do this.
404No such grievance.

Data models

CreateGrievanceDto

FieldTypeNotes
subjectstring≤ 150 chars
descriptionstring≤ 5000 chars
categoryharassment | discrimination | workplace_safety | compensation | management | workload | policy | facilities | other
anonymous?booleanKept for the clients that still send it, and for the column, but there is only one answer now: every grievance on the board is anonymous, and omitting this is the same as sending true. Sending false no longer puts a name anywhere — no route returns one.

GrievanceResponse

FieldTypeNotes
idstring
submitterNamestringWho to show it as. Always the literal "Anonymous", for every grievance and every viewer — the person who raised it included. Display it as given: do not substitute "You", and do not look the submitter up, because no route will tell you.
unreadbooleanWhether you have opened it yet. Drives the sidebar count; mark it read with POST /api/v1/grievances/{grievanceId}/read when the thread opens. Per-person: another employee reading it does not change yours.
subjectstring
descriptionstring
categoryharassment | discrimination | workplace_safety | compensation | management | workload | policy | facilities | other
anonymousbooleanWhether it was filed without a name attached.
statusopen | in_review | resolved | closed
resolvedAtstring ISO 8601 | nullWhen it was first resolved or closed. Null while open or in review, and after a reopen.
replyCountnumberHow many messages are on its thread. Read the thread itself from the /replies route; this is here so a list row can show the number without it.
statusNotestring | nullWhy it is at the status it is at — the most recent reason a handler gave. Show it with the status; a status on its own reads as nobody having looked. The whole record is on the /status-changes route.
statusNoteAtstring ISO 8601 | null
createdAtstring ISO 8601
updatedAtstring ISO 8601

CountResponse

FieldTypeNotes
countnumber

GrievanceReplyResponse

FieldTypeNotes
idstring
grievanceIdstring
authorNamestringWho to show it as. Always the literal "Anonymous" — the submitter’s replies, a colleague’s, and the grievance team’s alike. Nobody on a grievance thread is named to anybody.
authorRolesubmitter | handlerWhich side wrote it: the person who raised it, a colleague on the board, or the grievance team. It names a side, never a person — every message is drawn the same way and labelled "Anonymous".
bodystring
mentionsMentionResponse[]People tagged in body, resolved for display. The body carries each one as an @[id] token — render it with the same component the feed uses. These name the colleague who was tagged; the author stays anonymous.
createdAtstring ISO 8601
editedAtstring ISO 8601 | nullWhen its author last changed it; null if never. Show "edited".
isMinebooleanWhether you wrote it — offer edit and delete on these, and only these. Never says who wrote any other message.

MentionResponse

FieldTypeNotes
idstring
namestring
imagestring | null

CreateGrievanceReplyDto

FieldTypeNotes
bodystring≤ 5000 chars

GrievanceStatusChangeResponse

FieldTypeNotes
idstring
grievanceIdstring
fromStatusopen | in_review | resolved | closed | nullnull on the entry that records it being raised.
toStatusopen | in_review | resolved | closed
reasonstring
changedByNamestringAlways "Grievance team" here — individual handlers are not named to the person who raised the grievance, on the record any more than on the thread.
createdAtstring ISO 8601

UpdateGrievanceStatusDto

FieldTypeNotes
statusopen | in_review | resolved | closed
reasonstring≤ 1000 chars

AdminGrievanceStatusChangeResponse

FieldTypeNotes
idstring
grievanceIdstring
fromStatusopen | in_review | resolved | closed | nullnull on the entry that records it being raised.
toStatusopen | in_review | resolved | closed
reasonstring
changedByNamestringAlways "Grievance team" here — individual handlers are not named to the person who raised the grievance, on the record any more than on the thread.
createdAtstring ISO 8601
changedByGrievanceSubmitterResponse | nullThe handler who made the move. null on the entry that records it being raised, and when the handler’s account has been deleted.

GrievanceSubmitterResponse

FieldTypeNotes
idstring
namestring
emailstring
imagestring | null

AdminGrievanceReplyResponse

FieldTypeNotes
idstring
grievanceIdstring
authorNamestringWho to show it as. Always the literal "Anonymous" — the submitter’s replies, a colleague’s, and the grievance team’s alike. Nobody on a grievance thread is named to anybody.
authorRolesubmitter | handlerWhich side wrote it: the person who raised it, a colleague on the board, or the grievance team. It names a side, never a person — every message is drawn the same way and labelled "Anonymous".
bodystring
mentionsMentionResponse[]People tagged in body, resolved for display. The body carries each one as an @[id] token — render it with the same component the feed uses. These name the colleague who was tagged; the author stays anonymous.
createdAtstring ISO 8601
editedAtstring ISO 8601 | nullWhen its author last changed it; null if never. Show "edited".
isMinebooleanWhether you wrote it — offer edit and delete on these, and only these. Never says who wrote any other message.
authorGrievanceSubmitterResponse | nullThe handler who wrote it, so the team can see which of them answered. Always null on any employee’s reply — neither the submitter nor a colleague on the board is ever named. Also null when the handler’s account has been deleted.

On this page