API Reference

AdaptiveTest v0.2 · All endpoints with sample calls

Auth roles:nonereviewermanageradmin· reviewer/manager also need grade assignment

0. Configuration

All endpoints are served from the Next.js app. Configure via .env.local:

env
# ── Authentication ────────────────────────────────────────────────
GOOGLE_CLIENT_ID=310180611458-xxxx.apps.googleusercontent.com
GOOGLE_CLIENT_SECRET=GOCSPX-xxxx
NEXTAUTH_SECRET=<random-32-byte-base64>
NEXTAUTH_URL=http://localhost:3002        # change for production

# First user to sign in gets admin role; or set explicitly:
ADMIN_EMAIL=admin@yourschool.com

# ── Data provider ──────────────────────────────────────────────────
DATA_PROVIDER=local                       # "local" | "api"

# Only needed when DATA_PROVIDER=api:
# EXTERNAL_API_BASE_URL=https://your-api.example.com/v1
# EXTERNAL_API_KEY=your-secret-key
# EXTERNAL_API_TIMEOUT_MS=10000

Internal API routes use session cookies (set automatically after Google OAuth login). The optional external data API uses an X-API-Key header — see Section 8.

Base URL (local dev): http://localhost:3002 — Replace with your production domain before deploying.

Student Test Lifecycle

Every student-facing action maps to an internal API route. When DATA_PROVIDER=api those routes transparently proxy to your external service — your frontend never changes.

Important: The entire grade question bank is loaded in one call when the student opens the test page. The adaptive engine then selects and sequences questions client-side. No per-question API calls are made during the test.

EventInternal API calledExternal (DATA_PROVIDER=api)
Student opens /test/5 (full bank, 1 call)GET /api/questions?grade=5GET /questions?grade=5
Student submits testPOST /api/sessionsPUT /sessions/{id}
Webhook fires (if configured)— (server fires automatically)POST <WEBHOOK_URL>
Student / admin views reportGET /api/sessions/{id}GET /sessions/{id}
Download PDF reportGET /api/report/{id}/pdfGET /sessions/{id} (to read session data)

Webhook payload (auto-fired by POST /api/sessions)

When the student submits a test and a webhookUrl is provided in the POST body, the server immediately fires a POST to that URL with the full session payload. This is how your LMS receives live test results without polling.

json
// POST <webhookUrl>   (fired server-side after session is saved)
// Headers:
//   Content-Type: application/json
//   X-Webhook-Secret: <WEBHOOK_SECRET env var, if set>

{
  "event": "test.completed",
  "sessionId": "a3f82c1d-9e4b-4f7a-b0c5-2d1e8f9a3c7b",
  "grade": 5,
  "timestamp": "2025-05-03T14:32:08.421Z",
  "startedAt": "2025-05-03T14:12:00.000Z",
  "completedAt": "2025-05-03T14:32:08.421Z",
  "reportUrl": "https://app.example.com/report/a3f82c1d-...",
  "annotations": { "studentId": "S-1042" },
  "result": {
    "score": 620,
    "correctAnswers": 15,
    "totalQuestions": 20,
    "accuracy": 75,
    "timeTaken": 1143,
    "topicBreakdown": [
      { "topic": "Fractions & Decimals", "correct": 7, "total": 8, "percentage": 88 }
    ],
    "difficultyBreakdown": [
      { "difficulty": 1, "correct": 4, "total": 4 },
      { "difficulty": 2, "correct": 5, "total": 6 },
      { "difficulty": 3, "correct": 3, "total": 5 },
      { "difficulty": 4, "correct": 3, "total": 5 }
    ],
    "answers": [
      { "questionId": "q_g5_001", "selected": "C", "correct": "C", "isCorrect": true, "timeTaken": 48 }
    ]
  }
}
Tip: The same payload shape is available via GET /api/sessions/{id}?payload=1 so you can inspect or replay it without re-running a test.

1. Test Questions

The entire grade question bank is fetched in a single API call the moment a student opens the test page — not one question at a time. For Grade 5 this is typically ~120 questions returned in one response.

After the full bank arrives, the adaptive engine (initTestState()) selects 10–40 questions client-side based on the chosen duration and difficulty distribution. No further API calls are made per question during the test.

API call count per test session:
  • GET /api/questions?grade=N when student opens the test page (full bank)
  • POST /api/sessions when student submits (saves results)
When DATA_PROVIDER=api, the first call proxies to your external GET /questions?grade=N.
GET/api/questions?grade=5
none

Returns the full question bank for a grade in one response. The adaptive engine then selects and sequences 10–40 questions client-side. No auth required — the /test/* pages are protected at the middleware level.

When DATA_PROVIDER=api this proxies to your external GET /questions?grade=5. Your service must return the full bank — the app never requests individual questions.

Request

javascript
const res = await fetch('http://localhost:3002/api/questions?grade=5');
const questions = await res.json();  // full Question[] for the grade (e.g. ~120 items)

// The adaptive engine then selects 10–40 of these client-side.
// You never need to call this per-question during the test.
console.log(`Loaded ${questions.length} questions for Grade 5`);

Response

json
// 200 OK — full Question[] for the grade (~120 items for Grade 5)
// Truncated sample showing 3 questions across different difficulties:
[
  {
    "id": "q_g5_001",
    "grade": 5,
    "topic": "Fractions & Decimals",
    "subtopic": "Adding Fractions",
    "difficulty": 1,
    "source": "Common Core",
    "type": "multiple-choice",
    "question": "What is 1/2 + 1/4?",
    "options": ["1/2", "3/4", "2/6", "1"],
    "answer": "B",
    "explanation": "Convert to a common denominator: 2/4 + 1/4 = 3/4.",
    "image": ""
  },
  {
    "id": "q_g5_047",
    "grade": 5,
    "topic": "Geometry",
    "subtopic": "Area of Composite Shapes",
    "difficulty": 3,
    "source": "Math Kangaroo",
    "type": "multiple-choice",
    "question": "A rectangle is 8 cm long and 5 cm wide. A square of side 2 cm is cut from one corner. What is the remaining area?",
    "options": ["36 cm²", "38 cm²", "40 cm²", "34 cm²"],
    "answer": "A",
    "explanation": "Rectangle area = 40 cm². Square area = 4 cm². Remaining = 40 − 4 = 36 cm².",
    "image": ""
  },
  {
    "id": "q_g5_103",
    "grade": 5,
    "topic": "Number Theory",
    "subtopic": "Prime Factorization",
    "difficulty": 5,
    "source": "AMC 8",
    "type": "multiple-choice",
    "question": "How many distinct prime factors does 360 have?",
    "options": ["2", "3", "4", "5"],
    "answer": "B",
    "explanation": "360 = 2³ × 3² × 5. Three distinct prime factors: 2, 3, and 5.",
    "image": ""
  }
  // ... ~117 more questions
]

// 400 — grade out of range
{ "error": "Invalid grade — must be 1–10" }

2. Sessions

Created automatically when a student finishes the adaptive test. The test page calls POST /api/sessions, receives a sessionId, and redirects to the report page. When DATA_PROVIDER=api, the saved session is also forwarded to your external service via PUT /sessions/{id} (see Section 9 — External Provider API). If a webhook URL was passed, it fires immediately after saving.

POST/api/sessions
none

Create a new test session. Optionally fires a webhook to an external URL after saving.

Request

javascript
const res = await fetch('http://localhost:3002/api/sessions', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    grade: 5,
    startedAt: '2025-05-03T14:12:00.000Z',
    baseUrl: 'http://localhost:3002',
    webhookUrl: 'https://your-server.com/webhook',  // optional
    annotations: { studentId: 'S-1042' },           // optional
    result: {
      score: 620,
      correctAnswers: 15,
      totalQuestions: 20,
      timeTaken: 1143,
      topicBreakdown: [
        { topic: 'Fractions', correct: 7, total: 8, percentage: 88 }
      ],
      difficultyBreakdown: [
        { difficulty: 1, correct: 4, total: 4 },
        { difficulty: 2, correct: 5, total: 6 }
      ],
      answers: [
        { questionId: 'q_g5_001', selected: 'C', correct: 'C', isCorrect: true }
      ]
    }
  })
});
const data = await res.json();
// data.sessionId  → use to build report URL
// data.reportUrl  → redirect student here

Response

json
// 200 OK
{
  "sessionId": "a3f82c1d-9e4b-4f7a-b0c5-2d1e8f9a3c7b",
  "reportUrl": "http://localhost:3002/report/a3f82c1d-9e4b-4f7a-b0c5-2d1e8f9a3c7b"
}

// 500 — body was invalid JSON
{ "error": "Failed to create session" }
GET/api/sessions
admin

List all sessions (summary only — no per-question detail). Requires admin login.

Request

javascript
const res = await fetch('http://localhost:3002/api/sessions', {
  credentials: 'include'   // sends the session cookie
});
const sessions = await res.json();

Response

json
// 200 OK — array of session summaries
[
  {
    "sessionId": "a3f82c1d-9e4b-4f7a-b0c5-2d1e8f9a3c7b",
    "grade": 5,
    "timestamp": "2025-05-03T14:32:08.421Z",
    "score": 620,
    "correctAnswers": 15,
    "totalQuestions": 20,
    "reportUrl": "http://localhost:3002/report/a3f82c1d-...",
    "webhookSent": true
  }
]

// 401 — not logged in as admin
{ "error": "Unauthorized" }
GET/api/sessions/{id}
none

Get a single session. Add ?payload=1 to get the webhook-shaped payload instead of raw session data.

Request

javascript
const id = 'a3f82c1d-9e4b-4f7a-b0c5-2d1e8f9a3c7b';

// Raw session
const raw = await fetch(`http://localhost:3002/api/sessions/${id}`).then(r => r.json());

// Webhook-shaped payload (same format POSTed to your webhook URL)
const payload = await fetch(`http://localhost:3002/api/sessions/${id}?payload=1`).then(r => r.json());

Response

json
// 200 OK — raw StoredSession
{
  "sessionId": "a3f82c1d-9e4b-4f7a-b0c5-2d1e8f9a3c7b",
  "grade": 5,
  "timestamp": "2025-05-03T14:32:08.421Z",
  "startedAt": "2025-05-03T14:12:00.000Z",
  "completedAt": "2025-05-03T14:32:08.421Z",
  "webhookSent": true,
  "reportUrl": "http://localhost:3002/report/a3f82c1d-...",
  "annotations": { "studentId": "S-1042" },
  "result": { "score": 620, "correctAnswers": 15, ... }
}

// 404
{ "error": "Session not found" }

3. Reports

The report page at /report/{id} calls GET /api/sessions/{id} to load session data. The PDF route also reads the session — both proxy to GET /sessions/{id} on your external service when DATA_PROVIDER=api.

GET/api/report/{id}/pdf
none

Download a fully-formatted A4 PDF report for a session. The session data is fetched via the active provider (local file or external GET /sessions/{id}). Safe to share directly with students — no login required.

Request

javascript
const id = 'a3f82c1d-9e4b-4f7a-b0c5-2d1e8f9a3c7b';
const res = await fetch(`http://localhost:3002/api/report/${id}/pdf`);
const blob = await res.blob();

// In browser — trigger download
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `math-report-${id.slice(0,8)}.pdf`;
a.click();

Response

json
// 200 OK
Content-Type: application/pdf
Content-Disposition: attachment; filename="math-test-report-grade5-a3f82c1d.pdf"
Content-Length: <bytes>

<binary PDF data>

// 404
{ "error": "Session not found" }

4. Review

Used by teachers (reviewers) and managers to review and edit questions. All endpoints require the user to be logged in AND assigned to the requested grade.

GET/api/review/questions?grade=5
adminreviewermanager

Fetch all questions for a grade together with their current review status.

Request

javascript
const res = await fetch('http://localhost:3002/api/review/questions?grade=5', {
  credentials: 'include'
});
const { questions, reviews } = await res.json();

// questions → Question[]
// reviews   → Record<questionId, QuestionReview>

Response

json
// 200 OK
{
  "questions": [
    {
      "id": "q_g5_001",
      "grade": 5,
      "topic": "Fractions & Decimals",
      "subtopic": "Adding Fractions",
      "difficulty": 2,
      "source": "Common Core",
      "type": "multiple-choice",
      "question": "What is 3/4 + 1/2?",
      "options": ["1", "5/4", "1 1/4", "7/4"],
      "answer": "C",
      "explanation": "Convert 1/2 to 2/4: 3/4 + 2/4 = 5/4 = 1 1/4."
    }
  ],
  "reviews": {
    "q_g5_001": {
      "status": "approved",
      "reviewedBy": "teacher@school.com",
      "reviewedByName": "Jane Smith",
      "reviewedAt": "2025-05-03T10:00:00Z",
      "note": "Correct and clear."
    },
    "q_g5_002": { "status": "pending" }
  }
}

// 403 — not assigned to this grade
{ "error": "Not assigned to this grade" }
POST/api/review/submit
adminreviewermanager

Submit or update a review decision (approve / flag / reset to pending) for one question.

Request

javascript
const res = await fetch('http://localhost:3002/api/review/submit', {
  method: 'POST',
  credentials: 'include',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    questionId: 'q_g5_001',
    grade: 5,
    status: 'approved',      // "approved" | "flagged" | "pending"
    note: 'Question is clear and answer is correct.'
  })
});
const data = await res.json();  // { ok: true }

Response

json
// 200 OK
{ "ok": true }

// 403
{ "error": "Not assigned to this grade" }
GET/api/review/edit?grade=5
adminreviewermanager

Get the full edit history for a grade — who edited what, when, and what changed.

Request

javascript
const res = await fetch('http://localhost:3002/api/review/edit?grade=5', {
  credentials: 'include'
});
const { editLog } = await res.json();

Response

json
// 200 OK
{
  "editLog": [
    {
      "id": "edit_7f3a2b",
      "questionId": "q_g5_001",
      "grade": 5,
      "editedBy": "teacher@school.com",
      "editedByName": "Jane Smith",
      "editedAt": "2025-05-03T11:30:00Z",
      "changes": [
        { "field": "Question", "before": "What is 3/4 + 1/4?", "after": "What is 3/4 + 1/2?" },
        { "field": "Answer",   "before": "A",                  "after": "C" }
      ],
      "comment": "Fixed the fraction in the question — was 1/4 should be 1/2."
    }
  ]
}
POST/api/review/edit
adminreviewermanager

Save edits to a question. The API automatically diffs the changes and logs them.

Request

javascript
const res = await fetch('http://localhost:3002/api/review/edit', {
  method: 'POST',
  credentials: 'include',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    questionId: 'q_g5_001',
    grade: 5,
    comment: 'Fixed typo in option C',
    updates: {
      // Only include fields you want to change
      question: 'What is 3/4 + 1/2?',
      answer: 'C',
      explanation: 'Convert 1/2 to 2/4: 3/4 + 2/4 = 5/4 = 1 1/4.',
      options: ['1', '5/4', '1 1/4', '7/4']
      // image, explanation can also be updated
    }
  })
});
const data = await res.json();  // { ok: true, editId: "edit_7f3a2b" }

Response

json
// 200 OK
{ "ok": true, "editId": "edit_7f3a2b" }

// 404 — question not found in that grade
{ "error": "Question not found" }

// 403
{ "error": "Not assigned to this grade" }

5. Manager

GET/api/manager/stats
adminmanager

Per-grade review progress. Managers see only their assigned grades; admins see all 10.

Request

javascript
const res = await fetch('http://localhost:3002/api/manager/stats', {
  credentials: 'include'
});
const { perGrade, totalQuestions, totalApproved,
        totalFlagged, totalPending, allowedGrades } = await res.json();
// allowedGrades: number[] for managers, null for admin (= all grades)

Response

json
// 200 OK
{
  "allowedGrades": [4, 5, 6],
  "totalQuestions": 360,
  "totalApproved": 298,
  "totalFlagged": 22,
  "totalPending": 40,
  "perGrade": [
    {
      "grade": 5,
      "total": 120,
      "approved": 98,
      "flagged": 8,
      "pending": 14,
      "reviewers": [
        {
          "email": "teacher@school.com",
          "name": "Jane Smith",
          "reviewedCount": 106,
          "approvedCount": 98,
          "flaggedCount": 8
        }
      ],
      "assignedReviewers": [
        { "email": "teacher@school.com", "name": "Jane Smith" }
      ]
    }
  ]
}
GET/api/manager/approvals
adminmanager

List grade-level approval decisions. Managers see only their own decisions on assigned grades.

Request

javascript
const res = await fetch('http://localhost:3002/api/manager/approvals', {
  credentials: 'include'
});
const { approvals } = await res.json();

Response

json
// 200 OK
{
  "approvals": [
    {
      "grade": 5,
      "managerEmail": "manager@school.com",
      "managerName": "John Manager",
      "status": "approved",
      "comment": "All 120 questions reviewed and signed off.",
      "decidedAt": "2025-05-03T15:00:00Z"
    },
    {
      "grade": 6,
      "managerEmail": "manager@school.com",
      "managerName": "John Manager",
      "status": "revision_requested",
      "comment": "15 questions still flagged — needs another pass.",
      "decidedAt": "2025-05-03T16:00:00Z"
    }
  ]
}
POST/api/manager/approvals
adminmanager

Submit a grade-level approval or revision request. Manager must be assigned to the grade.

Request

javascript
const res = await fetch('http://localhost:3002/api/manager/approvals', {
  method: 'POST',
  credentials: 'include',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    grade: 5,
    status: 'approved',                        // or "revision_requested"
    comment: 'All 120 questions reviewed.'     // optional
  })
});
const data = await res.json();  // { ok: true }

Response

json
// 200 OK
{ "ok": true }

// 400 — invalid status value
{ "error": "Invalid status" }

// 403 — not assigned to that grade
{ "error": "Not assigned to this grade" }

6. Admin — Questions

GET/api/admin/questions?grade=5
admin

Get all questions for a grade with any overrides merged in.

Request

javascript
const res = await fetch('http://localhost:3002/api/admin/questions?grade=5', {
  credentials: 'include'
});
const questions = await res.json();  // Question[]

Response

json
// 200 OK — array of Question objects
[
  {
    "id": "q_g5_001",
    "grade": 5,
    "topic": "Fractions & Decimals",
    "subtopic": "Adding Fractions",
    "difficulty": 2,
    "source": "Common Core",
    "type": "multiple-choice",
    "question": "What is 3/4 + 1/2?",
    "options": ["1", "5/4", "1 1/4", "7/4"],
    "answer": "C",
    "explanation": "Convert 1/2 to 2/4: 3/4 + 2/4 = 5/4 = 1 1/4.",
    "image": ""
  }
]

// 400 — grade out of 1–10 range
{ "error": "Invalid grade" }
GET/api/admin/export?grade=5
admin

Download an Excel workbook with all questions for a grade. Use ?grade=all for every grade.

The workbook includes an Instructions sheet explaining how to re-import the file after editing.

Request

javascript
// Download for grade 5
const res = await fetch('http://localhost:3002/api/admin/export?grade=5', {
  credentials: 'include'
});
const blob = await res.blob();

// Trigger browser download
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'mathiq-questions-grade5.xlsx';
a.click();

// All grades
await fetch('http://localhost:3002/api/admin/export?grade=all', { credentials: 'include' });

Response

json
// 200 OK
Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet
Content-Disposition: attachment; filename="mathiq-questions-grade5.xlsx"

<binary .xlsx file>
POST/api/admin/import
admin

Bulk import questions from an Excel file. Rows with empty id are added; rows with an existing id overwrite that question.

Request

javascript
const file = document.querySelector('input[type=file]').files[0];
const form = new FormData();
form.append('file', file);

const res = await fetch('http://localhost:3002/api/admin/import', {
  method: 'POST',
  credentials: 'include',
  body: form           // do NOT set Content-Type — browser adds multipart boundary
});
const { added, updated, errors } = await res.json();

Response

json
// 200 OK
{
  "added": 5,
  "updated": 2,
  "errors": []
}

// Partial success (some rows failed validation)
{
  "added": 3,
  "updated": 1,
  "errors": [
    "Row 8: answer must be A, B, C, or D — got 'E'",
    "Row 12: grade is required"
  ]
}

7. Admin — Users

GET/api/admin/users
admin

List all registered users.

Request

javascript
const res = await fetch('http://localhost:3002/api/admin/users', {
  credentials: 'include'
});
const users = await res.json();  // User[]

Response

json
// 200 OK
[
  {
    "email": "vishal.m@moonpreneur.com",
    "name": "Vishal Malhotra",
    "image": "https://lh3.googleusercontent.com/...",
    "role": "admin",
    "createdAt": "2025-04-01T09:00:00Z",
    "lastLogin": "2025-05-03T14:00:00Z"
  },
  {
    "email": "teacher@school.com",
    "name": "Jane Smith",
    "image": "",
    "role": "reviewer",
    "createdAt": "2025-04-15T10:30:00Z",
    "lastLogin": "2025-05-03T08:00:00Z"
  }
]
PATCH/api/admin/users/{email}
admin

Change a user's role. The email address in the URL path must be URL-encoded.

Request

javascript
const email = 'teacher@school.com';
const res = await fetch(
  `http://localhost:3002/api/admin/users/${encodeURIComponent(email)}`,
  {
    method: 'PATCH',
    credentials: 'include',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ role: 'manager' })
    // role: "student" | "reviewer" | "manager" | "admin"
  }
);
const user = await res.json();  // updated User object

Response

json
// 200 OK — updated user
{
  "email": "teacher@school.com",
  "name": "Jane Smith",
  "role": "manager",
  "createdAt": "2025-04-15T10:30:00Z"
}

// 404
{ "error": "User not found" }
DELETE/api/admin/users/{email}
admin

Permanently delete a user account.

Request

javascript
const email = 'teacher@school.com';
const res = await fetch(
  `http://localhost:3002/api/admin/users/${encodeURIComponent(email)}`,
  { method: 'DELETE', credentials: 'include' }
);
const data = await res.json();  // { ok: true }

Response

json
// 200 OK
{ "ok": true }

// 404
{ "error": "User not found" }

8. Admin — Review Management

GET/api/admin/review/stats
admin

Full review statistics across all 10 grades with per-reviewer breakdowns.

Request

javascript
const res = await fetch('http://localhost:3002/api/admin/review/stats', {
  credentials: 'include'
});
const { perGrade, totalQuestions, totalApproved,
        totalFlagged, totalPending } = await res.json();

Response

json
// 200 OK
{
  "totalQuestions": 1200,
  "totalApproved": 980,
  "totalFlagged": 72,
  "totalPending": 148,
  "perGrade": [
    {
      "grade": 1,
      "total": 120, "approved": 108, "flagged": 4, "pending": 8,
      "reviewers": [
        { "email": "t1@school.com", "name": "Alice",
          "reviewedCount": 112, "approvedCount": 108, "flaggedCount": 4 }
      ]
    }
  ]
}
GET/api/admin/review/assignments
admin

List all teacher reviewer assignments along with all user accounts (for the assignment UI).

Request

javascript
const res = await fetch('http://localhost:3002/api/admin/review/assignments', {
  credentials: 'include'
});
const { assignments, users } = await res.json();

Response

json
// 200 OK
{
  "assignments": [
    {
      "email": "teacher@school.com",
      "name": "Jane Smith",
      "grades": [4, 5, 6],
      "assignedBy": "admin@school.com",
      "assignedAt": "2025-04-20T09:00:00Z"
    }
  ],
  "users": [ /* User[] — all registered users */ ]
}
POST/api/admin/review/assignments
admin

Assign a teacher to review specific grades. Replaces any existing assignment for that email.

Request

javascript
const res = await fetch('http://localhost:3002/api/admin/review/assignments', {
  method: 'POST',
  credentials: 'include',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    email: 'teacher@school.com',
    name: 'Jane Smith',
    grades: [4, 5, 6]
  })
});
const data = await res.json();  // { ok: true }

Response

json
// 200 OK
{ "ok": true }
DELETE/api/admin/review/assignments/{email}
admin

Remove all grade assignments from a teacher reviewer.

Request

javascript
const res = await fetch(
  `http://localhost:3002/api/admin/review/assignments/${encodeURIComponent('teacher@school.com')}`,
  { method: 'DELETE', credentials: 'include' }
);
const data = await res.json();  // { ok: true }

Response

json
// 200 OK
{ "ok": true }
POST/api/admin/review/managers
admin

Assign a manager to oversee specific grades. Same pattern as reviewer assignments.

Request

javascript
const res = await fetch('http://localhost:3002/api/admin/review/managers', {
  method: 'POST',
  credentials: 'include',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    email: 'manager@school.com',
    name: 'John Manager',
    grades: [4, 5, 6, 7, 8]
  })
});

Response

json
// 200 OK
{ "ok": true }
GET/api/admin/review/log?grade=5
admin

Get the complete edit log for a grade (admin view — no grade-assignment check).

Request

javascript
const res = await fetch('http://localhost:3002/api/admin/review/log?grade=5', {
  credentials: 'include'
});
const { editLog } = await res.json();

Response

json
// 200 OK — same shape as GET /api/review/edit
{
  "editLog": [
    {
      "id": "edit_7f3a2b",
      "questionId": "q_g5_001",
      "grade": 5,
      "editedBy": "teacher@school.com",
      "editedByName": "Jane Smith",
      "editedAt": "2025-05-03T11:30:00Z",
      "changes": [
        { "field": "Answer", "before": "A", "after": "C" }
      ],
      "comment": "Fixed answer key"
    }
  ]
}

9. External Data Provider API

When to use this section: Only relevant when you set DATA_PROVIDER=api. These are the endpoints your external service must implement. The app calls them internally — your frontend never calls these directly.

All requests from the app include these headers:

http
X-API-Key: <EXTERNAL_API_KEY>
Content-Type: application/json

Questions

GET/questions?grade=5
gray

Return all questions for a grade (with overrides merged).

Request

javascript
// Called internally by ApiDataProvider — not from your frontend
GET https://your-api.example.com/v1/questions?grade=5
X-API-Key: your-secret-key

Response

json
// 200 OK
{ "questions": [ /* Question[] */ ] }
PUT/questions/override
gray

Upsert a question override. If the question id exists, replace it; otherwise add it.

Request

javascript
PUT https://your-api.example.com/v1/questions/override
X-API-Key: your-secret-key
Content-Type: application/json

{
  "id": "q_g5_001",
  "grade": 5,
  "topic": "Fractions",
  "answer": "C",
  ...full Question object...
}

Response

json
// 200 OK
{ "ok": true }
POST/questions/import
gray

Bulk upsert questions. Empty-id rows get new IDs; existing-id rows are overwritten.

Request

javascript
POST https://your-api.example.com/v1/questions/import
X-API-Key: your-secret-key

{ "rows": [ /* Partial<Question>[] from the uploaded spreadsheet */ ] }

Response

json
// 200 OK
{ "added": 3, "updated": 1, "errors": [] }

Users

POST/users
gray

Create a user. Called on first login when the user doesn't exist yet.

Request

javascript
POST https://your-api.example.com/v1/users
X-API-Key: your-secret-key

{
  "email": "newuser@school.com",
  "name": "New User",
  "image": "https://lh3.googleusercontent.com/...",
  "role": "student"
}

Response

json
// 201 Created
{
  "email": "newuser@school.com",
  "name": "New User",
  "image": "",
  "role": "student",
  "createdAt": "2025-05-03T14:00:00Z"
}
PATCH/users/{email}
gray

Update a user's role or record their last login timestamp.

Request

javascript
// Update role
PATCH https://your-api.example.com/v1/users/teacher%40school.com
X-API-Key: your-secret-key

{ "role": "manager" }

// Or record last login
{ "lastLoginAt": "2025-05-03T14:00:00Z" }

Response

json
// 200 OK — updated User object

Sessions

PUT/sessions/{id}
gray

Create or replace a session. Called every time a test session is saved or updated.

Request

javascript
PUT https://your-api.example.com/v1/sessions/a3f82c1d-...
X-API-Key: your-secret-key

{
  "sessionId": "a3f82c1d-9e4b-4f7a-b0c5-2d1e8f9a3c7b",
  "grade": 5,
  "timestamp": "2025-05-03T14:32:08.421Z",
  "startedAt": "2025-05-03T14:12:00.000Z",
  "completedAt": "2025-05-03T14:32:08.421Z",
  "webhookSent": false,
  "reportUrl": "https://app.example.com/report/a3f82c1d-...",
  "result": { ... }
}

Response

json
// 200 OK
{ "ok": true }

Reviews, Edit Log, Assignments, Approvals

MethodEndpointDescription
GET/reviewsAll reviews as Record<questionId, QuestionReview>
PUT/reviews/{questionId}Upsert a review for one question
GET/edit-log?grade=NEdit log entries (optional grade filter)
POST/edit-logAppend a new edit log entry
GET/assignmentsList all reviewer grade assignments
GET/assignments/{email}Get one reviewer's assignment
PUT/assignments/{email}Upsert a reviewer assignment
DELETE/assignments/{email}Remove a reviewer assignment
GET/manager-assignmentsList all manager assignments
PUT/manager-assignments/{email}Upsert a manager assignment
DELETE/manager-assignments/{email}Remove a manager assignment
GET/grade-approvalsList all grade approval records
PUT/grade-approvals/{grade}/{managerEmail}Upsert a grade approval decision

Error Envelope (all external endpoints)

json
// All errors must return this shape with an appropriate HTTP status code:
{ "error": "Human-readable description of what went wrong" }

// Common status codes:
// 400 — invalid input / missing required field
// 404 — resource not found
// 500 — internal server error