API for the recognitions UI on custodia-md. Base path is the existing REST root, /md/api. Use the same authenticated session as the rest of that API. Comments, catalogs, settings, access, and moderators are in Recognitions API: comments, catalogs, settings, and access.
This API covers drafts, publish, visibility, comments, decorations, catalogs, access, moderators, and company settings. It does not award points or touch card spend.
Roles used below:
| Role | Who |
|---|---|
| Employee | Any signed-in user |
| Publisher | The recipient's current direct manager, a CommentModerator whose scope covers the recipient, or an admin |
| Admin | admin or hr-admin |
| Support | support or tech-admin, acting on the company in their token |
A direct manager is the leader of the recipient's default team (their order: 0 OrgTeam). When the recipient leads that team themselves, they have no manager. A team or subsidiary moderator can publish and open a recognition by id. Only a company-scope moderator can list drafts and the public feed without their own access row.
Calls the UI should make
| Screen | Call |
|---|---|
| Compose | POST /Recognitions |
| My list, a person's list, the public feed | GET /Recognitions |
| Open one recognition and its comments | GET /Recognitions/:id/thread |
| Publish | POST /Recognitions/:id/publish |
| Recipient hides or shows it | POST /Recognitions/:id/visibility |
| Author removes it | DELETE /Recognitions/:id |
| Comment | POST /Recognitions/:id/comments |
| Unsend | POST /RecognitionComments/:id/unsend |
| Moderation queue | GET /RecognitionComments/moderation-queue |
| Approve or reject | POST /RecognitionComments/:id/approve or .../reject |
| Badge and emotion picker | GET /ReactionOptions/available |
| GIF picker, when settings say catalog | GET /GifAssets |
| Occasion picker (admin compose) | GET /RecognitionOccasions |
| Comment hours, GIF mode, hold length | GET /RecognitionSettings |
| Admin: who has the feature | GET /RecognitionAccess, POST, PATCH /:id, DELETE /:id |
| Admin: moderators | GET /CommentModerators, POST, DELETE /:id |
| Admin: company settings | PUT /RecognitionSettings |
| Admin: company reactions and GIFs | POST and PATCH on /ReactionOptions and /GifAssets |
GET /Recognitions/:id, findOne, and count are also open. Comments and decorations have no list or create endpoint of their own. Create a comment through the recognition. Read comments through thread or the moderation queue.
Errors
Failures use the usual LoopBack body:
{ "error": { "statusCode": 403, "message": "Recognitions are not enabled for you", "code": "RECOGNITION-FORBIDDEN" } }
A recognition or comment the caller cannot see returns 404, including when the id exists. An empty find returns [].
| Code | HTTP | Meaning |
|---|---|---|
RECOGNITION-INVALID | 400 | Missing recipient, self recognition, message length or markup, bad visibility, unknown decoration |
RECOGNITION-FORBIDDEN | 403 | Feature off for the author or recipient, or the caller cannot publish, comment, delete, or set an occasion |
RECOGNITION-LOCKED | 403 | Visibility is locked, or a field was sent that only the server sets |
RECOGNITION-STATE | 409 | Visibility change before publish |
COMMENT-HOURS | 403 | Outside the company's comment window. The message includes the hours |
COMMENT-FORBIDDEN | 403 | Unsend or review by the wrong person. Comments cannot be deleted |
COMMENT-STATE | 409 | Unsend window closed, or approve/reject on a comment that is not pending |
COMMENT-LOCKED | 403 | Direct update of a comment field |
SETTINGS-FORBIDDEN / SETTINGS-INVALID | 403 / 400 | Settings change by a non-admin, or a bad settings value |
ACCESS-INVALID / ACCESS-DUPLICATE / ACCESS-LOCKED | 400 / 409 / 403 | Bad access scope, that scope already exists, or companyId was patched |
MODERATOR-INVALID / MODERATOR-LOCKED | 400 / 403 | Bad moderator scope, user not in the company, or companyId was patched |
REACTION-INVALID / REACTION-FORBIDDEN / REACTION-LOCKED | 400 / 403 | Bad reaction copy, non-admin write, or a change to a platform reaction |
GIF-INVALID / GIF-FORBIDDEN / GIF-LOCKED | 400 / 403 | Bad GIF copy, delete attempt, or a companyId change |
Text rules
message and comment body are plain text. The server rejects HTML and script: a tag such as <b> or </script>, a comment, or a javascript:, vbscript:, or data:text/html URL. A comparison such as a < b or thanks <3 is kept.
Limits: message 2000 characters, comment 1000, at most 10 decorations on a comment. The server trims the message and the comment body.
Recognitions
Create a draft
POST /Recognitions
{ "recipientUserId": "<user id>", "message": "Thanks for the launch" }
An admin may also send occasionId. Anyone else gets 403 RECOGNITION-FORBIDDEN.
The server sets companyId, authorUserId, status: "draft", visibility, visibilityLocked, and managerId. The client cannot set those. The author and the recipient must both have the feature on, the recipient must be an active user in the same company, and the author cannot recognize themselves.
managerId is the recipient's direct manager at create time. Publish writes it again from the manager at that moment.
Birthday and anniversary occasions are seeded with forcePrivate: true. Those drafts come back visibility: "private" and visibilityLocked: true no matter what the client sends.
There is no edit endpoint. The message is fixed at create. PATCH /Recognitions/:id is not exposed.
List
GET /Recognitions?filter={"where":{"status":"published"},"order":"createdOn DESC","limit":20,"skip":0}
filter is a LoopBack filter: where, order, limit, skip, fields. The server adds its own where-clause, so the caller only receives rows they can read. Deleted rows are omitted.
An employee list contains:
- recognitions they wrote
- recognitions they received
- published recognitions they published
- published recognitions where they are
managerId(the manager notified at publish, including private ones) - drafts and published recognitions, including private ones, for people who currently report directly to them
- every public published recognition, when the feature is on for them
- every draft and every public published recognition, when they are a company-scope moderator
Team and subsidiary moderators do not get those extra list rows. They open a recognition with thread when they have its id.
Admins see every non-deleted recognition in the company.
There is no "is this feature on for me?" endpoint. RecognitionAccess is admin-only. For an employee, a disabled feature means the public feed is absent and POST /Recognitions returns 403 RECOGNITION-FORBIDDEN with "Recognitions are not enabled for you". Their own rows and their direct reports' rows still list.
One recognition and its comments
GET /Recognitions/:id/thread
{
"recognition": { "id": "...", "status": "published", "visibility": "public", "message": "..." },
"comments": [
{
"id": "...", "body": "Nice work", "status": "approved", "holdUntil": null,
"decorations": [
{ "id": "...", "kind": "badge", "name": "Star", "symbol": "star", "imageUrl": null },
{ "id": "...", "kind": "gif", "name": "High five", "url": "https://..." }
]
}
]
}
The same read rules as the list apply, plus: a covering moderator can open a public recognition in their scope even when their own access row is off, and the current direct manager can open a draft or a private recognition for a direct report. A deleted recognition is 404.
comments is the newest view the caller may see, oldest first, capped at 200. Held and withdrawn comments are returned only to their author. Pending comments are returned to their author, an admin, or a covering moderator while the recognition is public. Rejected comments are returned to their author, an admin, or a covering moderator. Approved comments are returned to anyone who can read the recognition.
Opening the thread also releases any hold whose holdUntil has passed, so a comment can change from held to approved or pending in this response.
A catalog GIF decoration includes name, symbol, imageUrl, and url. An open GIF link includes url only. A badge or emotion includes name, symbol, and imageUrl.
Publish
POST /Recognitions/:id/publish
No body. The publisher is the recipient's current direct manager, a covering moderator, or an admin. Publish checks the feature again for both the author and the recipient, and that the recipient is still active.
The response is the recognition with status: "published", publishedById, publishedOn, and an updated managerId. Calling it again on an already published recognition returns the same row and retries any notification that failed.
Publish notifies the recipient and the direct manager. The UI does not send those notifications. A private recognition is notified as a private message.
Visibility
POST /Recognitions/:id/visibility
{ "visibility": "private" }
visibility is public or private. Only the recipient can call this, and only after publish. A locked recognition (birthday or anniversary) returns 403 RECOGNITION-LOCKED. The same value as the current one returns the row unchanged.
Making a recognition private hides it, and its pending comments, from the public feed and from moderators. The author, the recipient, the manager, and admins still see it.
Delete
DELETE /Recognitions/:id
The author or an admin. The response is { "count": 1 }. The row stays in the database with status: "deleted" and then 404s on thread. A second delete of an already deleted row is not available, because the row is already hidden. Comments are not deleted with it, and comments have no delete API.