Peer Reviews API
Reviewer queue, draft, submit, flag, instructor moderation
The peer review surface (Sprint δ Track 1). Reviewer-facing endpoints are anonymous — the original author's name is never in the payload. Instructor moderation is not anonymous — the whole point of moderation is seeing both sides.
Engine: src/lib/peer-review.ts. See Architecture — Peer Reviews for the assignment heuristic.
GET /api/me/peer-reviews
The calling student's review queue. Defaults to PENDING; pass ?status=PENDING,SUBMITTED,FLAGGED to see history.
Response
{
"data": {
"reviews": [
{
"id": "pr_123",
"status": "PENDING",
"score": null,
"assignedAt": "2026-04-13T08:00:00Z",
"submittedAt": null,
"assignment": {
"id", "title", "maxScore", "dueDate", "courseId", "courseTitle"
},
"submission": {
"id", "submittedAt",
"textContentPreview": "First 240 chars…",
"fileCount": 2
}
}
],
"total": 4,
"pendingCount": 2
}
}
Anonymity: the response deliberately omits the original studentId and any author-identifying field. The reviewer sees the work, never the author.
GET /api/peer-reviews/[id]
Full review detail for the assigned reviewer. Returns the assignment instructions, the submission's content + files, the rubric (if any), and the reviewer's existing draft.
Access control: only the assigned reviewer (peerReview.reviewerId === user.id) can read this. Instructors get /api/assignments/[id]/peer-reviews instead.
Response
{
"data": {
"peerReview": {
"id", "status", "score", "rubricScores", "feedback", "submittedAt", "createdAt"
},
"assignment": { "id", "title", "instructions", "maxScore", "courseId", "courseTitle" },
"rubric": {
"id", "title", "totalPoints",
"criteria": [ { "id", "title", "description", "maxPoints", "order" } ]
},
"submission": {
"id", "submittedAt", "textContent", "files", "isLate"
}
}
}
submission.studentId, gradedBy*, and feedback are intentionally absent.
PATCH /api/peer-reviews/[id]
Save a draft. Body:
{
"rubricScores": { "<criterionId>": 4, "<criterionId>": 3 },
"feedback": "Nice intro, more depth on..."
}
Both fields optional. Cannot transition status — that's reserved for /submit and /flag. Returns 409 if already SUBMITTED or FLAGGED.
POST /api/peer-reviews/[id]/submit
Final submit. Validates per the assignment shape:
With a rubric
Every criterion must have a score within [0, criterion.maxPoints]. The aggregate score is the sum.
{
"rubricScores": { "crit_1": 4, "crit_2": 3, "crit_3": 5 },
"feedback": "Strong analysis, consider..."
}
Without a rubric
Body must include a top-level score between 0 and assignment.maxScore.
{ "score": 17, "feedback": "..." }
Side effects
- Review row →
SUBMITTED,submittedAt = now recomputeSubmissionPeerScore(submissionId)runs — updatessubmission.peerScoreAverageand incrementspeerReviewsCompleted- If this was the last assigned reviewer AND the instructor hasn't manually graded:
submission.scorebecomes the peer averageASSESS_PEER_GRADEDfires once to the original student (force-immediate)
Response
{
"data": {
"status": "SUBMITTED",
"score": 17,
"aggregate": {
"peerScoreAverage": 16.4,
"reviewsSubmitted": 3,
"reviewsAssigned": 3,
"finalisedNow": true
}
}
}
POST /api/peer-reviews/[id]/flag
Reviewer flags a submission as inappropriate / off-topic / academic dishonesty / etc. Body:
{ "reason": "Submission contains plagiarised content from..." }
reason required, 3–500 chars.
Side effects
- Review row →
FLAGGEDwith the reason - Recomputes the aggregate — FLAGGED reviews count toward "completed" but are excluded from the score average
- Fires
TEACHER_NEW_SUBMISSIONto the course owner withflagged: truein metadata - May trigger
ASSESS_PEER_GRADEDto the student if this was the last outstanding review
Response: { data: { status: "FLAGGED" } }.
GET /api/assignments/[id]/peer-reviews
Instructor moderation view. Requires canManageCourses(user).
Returns every peer review for the assignment, grouped by submission, with reviewer + author display names resolved via the OptiCRM Contact lookup.
Response
{
"data": {
"assignment": { "id", "title", "maxScore", "peerReviewCount", "isPeerAssessed", "rubric": { ... } },
"rubric": { "id", "criteria": [...] },
"groups": [
{
"submissionId": "sub_1",
"student": { "id": "ct_...", "name": "Aarav Sharma" },
"instructorScore": null,
"peerScoreAverage": 16.4,
"peerReviewsCompleted": 3,
"peerReviewCount": 3,
"instructorOverridden": false,
"submittedAt": "...",
"reviews": [
{
"id": "pr_a",
"reviewer": { "id": "ct_...", "name": "Diya R." },
"status": "SUBMITTED",
"score": 17,
"rubricScores": { "crit_1": 4, "crit_2": 3, "crit_3": 5 },
"feedback": "...",
"flagReason": null,
"submittedAt": "...",
"createdAt": "..."
}
]
}
],
"total": 9
}
}
The instructor can override the score via the existing POST /api/assignments/[id]/grade endpoint — that wins over the peer average. instructorOverridden reflects whether the override has been applied.
Related
- Architecture — Peer Reviews — assignment heuristic
- Notifications System —
PEER_REVIEW_ASSIGNED,ASSESS_PEER_GRADED