DocsAPI Reference

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

  1. Review row → SUBMITTED, submittedAt = now
  2. recomputeSubmissionPeerScore(submissionId) runs — updates submission.peerScoreAverage and increments peerReviewsCompleted
  3. If this was the last assigned reviewer AND the instructor hasn't manually graded:
    • submission.score becomes the peer average
    • ASSESS_PEER_GRADED fires 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

  1. Review row → FLAGGED with the reason
  2. Recomputes the aggregate — FLAGGED reviews count toward "completed" but are excluded from the score average
  3. Fires TEACHER_NEW_SUBMISSION to the course owner with flagged: true in metadata
  4. May trigger ASSESS_PEER_GRADED to 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.