v01 Home
REST API Reference

SmartAIExam
API Docs

Complete reference for all JSON-returning endpoints. HTML routes are excluded. Supports both session cookie auth (web) and JWT Bearer auth (mobile/external).

Base URL
Version
v01
Auth
JWT Bearer / Cookie

Authentication

Two auth methods. Web browser uses session cookies. External clients (mobile, Postman, third-party) use JWT Bearer tokens.

🌐
Web Session (Cookie) — Existing, Unchanged
After POST /login via browser, server sets an HttpOnly session cookie. All web UI routes use this automatically. Session lifetime: 3 hours.
🔑
JWT Bearer Token — For External Clients
Call POST /api/v01/auth/login to get a JWT. Send it as Authorization: Bearer <token> on every request. Access token expires in 1 hour. Use refresh token to renew without re-login.

Dual-role users (role = user,admin) get both portals in one token. The available_portals array in the login response tells the client which portals this user can access. No portal selection needed — both user and admin APIs are accessible with the same token.
STEP 01
Login → Get Tokens
POST /api/v01/auth/login returns access_token (1h) + refresh_token (30d)
STEP 02
Use Access Token
Send Authorization: Bearer <access_token> header with every API call
STEP 03
Token Expires
API returns 401 token_expired_or_invalid when access token expires
STEP 04
Refresh Silently
POST /api/v01/auth/refresh with refresh token → new access token, no re-login needed
Quick Example
python
import requests

# Step 1: Login
res = requests.post(
    "https://smartaiexam.in/api/v01/auth/login",
    json={"username": "john.doe", "password": "MyPass@2025"},
)
data = res.json()["data"]

access_token      = data["access_token"]
refresh_token     = data["refresh_token"]
available_portals = data["available_portals"]  # e.g. ["user"] or ["user", "admin"]

# Step 2: Use token on any API
headers = {"Authorization": f"Bearer {access_token}"}

convs = requests.get(
    "https://smartaiexam.in/api/v01/chat/conversations",
    headers=headers,
)
print(convs.json())

Rate Limits

EndpointLimitWindowResponse
/login, /admin/login3 attemptsPer IP + identifier15-min lockout
POST /api/v01/chat/conversations/:id/messages1 msg / 2sPer user429
POST /api/v01/discussions/:id1 msg / 10sPer user429
POST /api/v01/assistant/messages50 / dayPer user, resets midnight (independent of conversation count)429 with limit_reached:true
POST /api/v01/assistant/messages100 / conversationPer conversation (~50 exchanges), start a new chat to continue400 with limit_reached:"conversation"
All other endpointsUnlimited

Error Handling

All JSON errors return a consistent shape with status: "error".

json — error shape
{ "status": "error", "message": "Human-readable error description" }
400 Bad Request 401 Unauthorized 403 Forbidden 404 Not Found 409 Conflict 429 Rate Limited 500 Server Error
⚠️A 401 with message token_expired_or_invalid means JWT expired. Call /api/v01/auth/refresh to get a new access token.

JWT Auth Endpoints

Token-based auth for external clients. These endpoints do not require an existing session — they create tokens from credentials.

POST /api/v01/auth/login Username or email + password → JWT tokens Public
Body
json
{
  "username": "john.doe",   // username OR email accepted
  "password": "MyPass@2025"
}
200 OK
json
{
  "status": "success",
  "data": {
    "access_token":      "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
    "refresh_token":     "dGhpcyBpcyBhIHJlZnJlc2ggdG9rZW4...",
    "token_type":        "Bearer",
    "expires_in":        3600,
    "available_portals": ["user"],
    "user": {
      "id":        42,
      "username":  "john.doe",
      "full_name": "John Doe",
      "role":      "user"
    }
  }
}
ℹ️available_portals will be ["user","admin"] for dual-role accounts. Client uses this to show portal selection UI.
400 — username/email and password required 401 — Invalid credentials 401 — Password not set, use web portal 429 — Account locked (3 failed attempts)
json — 401
{ "status": "error", "message": "Invalid credentials. 2 attempts remaining." }
POST /api/v01/auth/google Google ID token from client SDK → JWT tokens Public
ℹ️Get id_token from Google Sign-In SDK on client side (Android/iOS/Web), send it here. Server verifies with Google, creates account if new, returns JWT.
json
{ "id_token": "<Google ID token from client SDK>" }
json — 200
{
  "status": "success",
  "data": {
    "access_token":      "eyJ...",
    "refresh_token":     "dGhp...",
    "token_type":        "Bearer",
    "expires_in":        3600,
    "is_new_user":       false,
    "available_portals": ["user"],
    "user": { "id": 42, "username": "john.doe", "role": "user" }
  }
}
POST /api/v01/auth/refresh Exchange refresh token for new access token Public
json
{ "refresh_token": "dGhpcyBpcyBhIHJlZnJlc2ggdG9rZW4..." }
json — 200
{
  "status":  "success",
  "data": {
    "access_token": "eyJ...",
    "token_type":   "Bearer",
    "expires_in":   3600
  }
}
401 refresh_token_expired — re-login required 401 refresh_token_invalid 401 refresh_token_revoked
POST /api/v01/auth/logout Revoke refresh token — invalidate session Public
json — request
{ "refresh_token": "dGhp..." }
json — 200
{ "status": "success", "message": "Logged out" }
ℹ️Access token expires on its own after 1 hour. Just discard it on the client side. This endpoint only revokes the refresh token to prevent renewal.
GET /api/v01/auth/me Get current authenticated user profile JWT
Headers
http
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
json — 200
{
  "status": "success",
  "data": {
    "id":                42,
    "username":          "john.doe",
    "email":             "john@example.com",
    "full_name":         "John Doe",
    "role":              "user",
    "available_portals": ["user"],
    "created_at":        "2025-01-01T10:00:00"
  }
}

Auth JSON Endpoints

Password reset, access requests, and account deletion. HTML form routes /login, /logout, /create_account are excluded.

POST /api/v01/auth/password-reset Send password reset link to email Public
json — request
{ "email": "john@example.com" }
json — 200 (always, prevents email enumeration)
{ "success": true, "message": "If an account exists with this email, a reset link has been sent." }
⚠️Always returns 200 regardless of email existence. Token expires in 1 hour.
POST /api/v01/access-requests/validate-user Validate user before submitting access request Public
json
{ "username": "john.doe", "email": "john@example.com" }
json — 200
{
  "success": true,
  "user": { "username": "john.doe", "current_access": "user" },
  "available_requests": ["admin", "user,admin"],
  "has_pending_request": false,
  "can_request": true
}
404 — User not found
POST /api/v01/access-requests Submit admin/role access request Public
json
{
  "username":         "john.doe",
  "email":            "john@example.com",
  "current_access":   "user",
  "requested_access": "admin",
  "user_reason":      "I need to manage exams for my batch"
}
json — 200
{ "success": true, "message": "Request submitted.", "request_id": 7 }
400 — Already has pending request
DELETE /api/v01/auth/me Permanently delete authenticated user's account Auth
json — request
{ "confirm": "DELETE" }  // must be exactly "DELETE"
200 — { success: true } 400 — Invalid confirmation string 401 — Not authenticated
🚨Irreversible. Deletes all user data in FK-safe order. Group chats preserved with deleted-account placeholder.

Exam Endpoints

Start, sync answers, submit, and check attempt status. All require auth.

GET /api/v01/exams/{exam_id}/preload Pre-cache exam questions in session Auth
json — 200
{ "success": true, "cached": true, "question_count": 30 }
ℹ️Call before starting exam. Resolves question image URLs and stores questions in session. Returns instantly if already cached.
POST /api/v01/exams/{exam_id}/start Create new attempt or resume existing in-progress attempt Auth
json — 200 fresh start
{
  "success":        true,
  "redirect_url":   "/exam/42",
  "resumed":        false,
  "attempt_id":     17,
  "attempt_number": 1,
  "fresh_start":    true
}
json — 200 resumed
{ "success": true, "resumed": true, "attempt_id": 17, "redirect_url": "/exam/42" }
400 — Max attempts (N) reached 401 — Not authenticated 500 — DB failure
POST /api/v01/exams/{exam_id}/answers Sync in-progress answers to server session Auth
json — request
{
  "answers": {
    "101": "A",          // MCQ — single letter
    "102": ["A", "C"],   // MSQ — array
    "103": "42.5"        // NUMERIC — string
  },
  "markedForReview": [101, 104]
}
ℹ️Call every ~30s and on every answer change. Submission reads from session state set here.
POST /submit-exam/{exam_id} Submit exam, calculate score, create result record Auth
ℹ️No request body needed — reads answers from server session. Redirects to result or result-pending page depending on exam's result_mode.
302 → /result/:exam_id (instant mode) 302 → /result-pending/:exam_id (delayed/manual) 401 — Not authenticated
GET /api/v01/exams/{exam_id}/attempt-status Get attempt count and active attempt info Auth
json — 200 (active attempt)
{
  "has_active_attempt": true,
  "attempt_id":         17,
  "attempt_number":     2,
  "start_time":         "2025-01-15 10:30:00",
  "completed_count":    1,
  "max_attempts":       3,
  "attempts_remaining": 1
}
POST /api/v01/ping Session heartbeat — check if session is alive Auth
204 — Session alive (no body) 401 — { "reason": "no_session" }

AI Study Assistant

Multi-conversation AI study chat. Each user can have many independent conversations. Responses include LaTeX for math/science. Daily limit: 50 messages/user/day (shared across all conversations); per-conversation limit: 100 messages/conversation.

GET /api/v01/assistant/init Get daily limits + first page of conversations (sidebar's first paint) Auth
json — 200
{
  "success":       true,
  "dailyLimit":    50,
  "questionsUsed": 12,
  "conversations": [
    { "id": 4, "title": "Bernoulli's Theorem", "messageCount": 6, "createdAt": "...", "updatedAt": "..." }
  ],
  "hasMoreConversations": false
}
GET /api/v01/assistant/conversations?limit={20}&offset={0}&search={query} Paginated, optionally-searched conversation list (server-side) Auth
ℹ️search matches conversation title or message text (case-insensitive). Max limit is 50.
json — 200
{ "success": true, "conversations": [ "..." ], "hasMore": true }
GET /api/v01/assistant/conversations/{id}/messages?limit={30}&offset={0} Paginated messages for one conversation (newest 30 first, then older on demand) Auth
ℹ️Ownership-checked — returns 404 if the conversation doesn't belong to the caller (never a 403, to avoid leaking existence).
json — 200
{ "success": true, "messages": [ { "text": "...", "isUser": true, "timestamp": "..." } ], "hasMore": true, "conversation": { "..." } }
POST /api/v01/assistant/messages Send a message — creates the conversation on first send if conversation_id is omitted Auth
json
{ "message": "Explain conservation of momentum with an example", "conversation_id": null }
ℹ️Omit or pass null for conversation_id to start a new conversation — no empty conversation is created unless a message is actually sent.
json — 200
{
  "success":              true,
  "response":             "[FINAL ANSWER]\nConservation of momentum states that...",
  "conversation_id":      4,
  "title":                "Conservation Of Momentum",
  "questions_remaining":  37
}
400 — Message too short or too long 400 — Conversation message limit reached (limit_reached:"conversation") 404 — Conversation not found / not owned by caller 429 — Daily limit reached, or a previous send is still in flight
json — 429
{ "success": false, "message": "Daily limit reached. Resets at midnight.", "limit_reached": true }
PATCH /api/v01/assistant/conversations/{id} Rename a conversation (max 70 characters) Auth
json
{ "title": "Bernoulli's Theorem" }
json — 200
{ "success": true, "conversation": { "..." } }
DELETE /api/v01/assistant/conversations/{id} Permanently delete one conversation and its messages Auth
ℹ️Ownership-checked — 404 if not owned. Cascades to the conversation's ai_chat_history rows. Does not affect the daily usage counter.
json — 200
{ "success": true, "message": "Conversation deleted." }

Chat Endpoints

Real-time peer chat with connection requests, groups, and unread tracking. All require auth.

GET /api/v01/chat/conversations List all DM and group conversations with unread counts Auth
json — 200
{
  "success": true,
  "conversations": [
    {
      "id":           12,
      "name":         "Jane Smith",
      "is_group":     false,
      "unread":       3,
      "online":       true,
      "last_message": { "message": "Hey!", "created_at": "2025-01-15T10:30:00" }
    }
  ]
}
GET /api/v01/chat/conversations/{conv_id}/messages?before={iso_ts} Get messages — 40 at a time, paginated backwards Auth
json — 200
{
  "success": true,
  "messages": [
    {
      "id":          88,
      "sender_name": "Jane Smith",
      "message":     "Hi there!",
      "created_at":  "2025-01-15T10:30:00",
      "is_own":      false,
      "is_edited":   false,
      "reply_to_id": null
    }
  ]
}
ℹ️Also marks conversation as read. Pass before ISO timestamp to load older messages (pagination).
POST /api/v01/chat/conversations/{conv_id}/messages Send a message — max 1000 chars, 1 per 2s Auth
json
{
  "message":      "Hey, how are you?",
  "reply_to_id":  85,           // optional
  "reply_to_text":"Hi there!",   // optional, truncated to 100 chars
  "reply_to_name":"Jane Smith"   // optional
}
json — 200
{ "success": true, "message": { "id": 89, "message": "Hey, how are you?", "is_own": true, "created_at": "..." } }
429 — Rate limited (2s cooldown) 403 — Not a member
PUT /api/v01/chat/messages/{msg_id} Edit own message Auth
json — request
{ "message": "Edited message text" }
200 — { success: true, message: "Edited text" } 403 — Not own message
DELETE /api/v01/chat/messages/{msg_id} Soft-delete own message Auth
200 — { success: true } 403 — Not own message
POST /api/v01/chat/conversations/{conv_id}/read Mark conversation as read — reset unread count to 0 Auth
200 — { success: true }
POST /api/v01/chat/friend-requests Send connection request to a user Auth
json — request
{ "recipient_id": 5 }
200 — { success: true } 409 — Already sent or connected
GET /api/v01/chat/friend-requests/inbox Get pending incoming connection requests Auth
json — 200
{
  "success": true,
  "requests": [
    { "conn_id": 3, "from_id": 5, "from_name": "Jane Smith", "created_at": "2025-01-15T09:00:00" }
  ]
}
PUT /api/v01/chat/friend-requests/{conn_id} Accept or reject a connection request Auth
json — request
{ "action": "accept" }  // or "reject"
json — 200 (accepted)
{ "success": true, "conv_id": 12 }
GET /api/v01/chat/unread-count Total unread messages + pending connection requests Auth
json — 200
{ "success": true, "unread": 5, "requests": 2 }
GET /api/v01/chat/online-status?ids=1,2,5 Check online status of multiple users at once Auth
json — 200
{ "success": true, "status": { "1": true, "2": false, "5": true } }
POST /api/v01/chat/groups Create a group conversation Auth
json — request
{ "name": "Physics Study Group", "member_ids": [3, 5, 7] }
200 — { success: true, conv_id: 15 }
DELETE /api/v01/chat/friends/{other_id} Remove friend and purge shared DM conversation Auth
ℹ️Deletes connection record + DM conversation entirely. Group chats shared with this user are unaffected.
200 — { success: true }

Discussion Endpoints

Per-question threaded comments with replies, pinning, and best-answer marking.

GET /api/v01/discussions/{question_id} Get threaded comments for a question Auth
json — 200
{
  "success": true,
  "count":   5,
  "comments": [
    {
      "id":             1,
      "username":       "jane.smith",
      "message":        "Approach using energy conservation...",
      "is_own":         false,
      "is_pinned":      false,
      "is_best_answer": true,
      "created_at":     "2025-01-15T10:00:00",
      "replies":        []
    }
  ]
}
ℹ️Deleted account messages show as username: "Deleted Account" with a placeholder. Thread structure is preserved.
POST /api/v01/discussions/{question_id} Post a comment — max 500 chars, 1 per 10s Auth
json — request
{
  "message":   "The answer is B because...",
  "exam_id":   42,   // optional
  "parent_id": 1    // optional — for replies
}
200 — { success: true, message: { id, username, ... } } 429 — Rate limited (10s)
PUT /api/v01/discussions/comments/{comment_id} Edit own comment (admins can edit any) Auth
json — request
{ "message": "Updated explanation..." }
200 — { success: true } 403 — Forbidden
DELETE /api/v01/discussions/comments/{comment_id} Soft-delete own comment (admins can delete any) Auth
200 — { success: true } 403 — Forbidden
POST /api/v01/discussions/counts Get discussion counts for up to 100 questions at once Auth
json
{ "question_ids": [101, 102, 103] }  // max 100
json — 200
{ "success": true, "counts": { "101": 5, "102": 0, "103": 12 } }

Admin JSON APIs

Base path: /api/v01/admin/. Every endpoint below requires an authenticated admin: either the standard admin session cookie (set by POST /admin/login) or a JWT with admin in its role, sent as Authorization: Bearer <token>. JSON request bodies require Content-Type: application/json; multipart endpoints (noted per-endpoint) require multipart/form-data.

⚠️Every endpoint on this page returns the same two auth failures unless noted otherwise — not repeated per endpoint below: 401 { "status": "error", "message": "Authentication required" } when no valid session/token is present, and 403 { "status": "error", "message": "Admin access required" } when the caller is authenticated but not an admin.

Exams

ℹ️Create/edit exam are HTML form pages, not JSON: GET/POST /admin/exams, GET/POST /admin/exams/edit/{exam_id} — see Web Routes.
DELETE /api/v01/admin/exams/{exam_id} Delete an exam and every row that depends on it Admin
ℹ️No request body. Cascades: responses → results → exam_attempts → question_discussions/discussion_counts → questions → the exam row itself. Irreversible.
json — 200
{ "success": true, "message": "Exam 'Midterm Physics' deleted." }
404 — {"success": false, "message": "Exam not found"} 500 — {"success": false, "message": "<db error text>"}
POST /api/v01/admin/exams/{exam_id}/release-results Toggle whether students can see this exam's results Admin
ℹ️No request body. Flips the exam's current results_released flag — call again to toggle back.
json — 200
{ "success": true, "message": "Results for 'Midterm Physics' have been released.", "released": true }
404 — {"success": false, "message": "Exam not found"} 500 — {"success": false, "message": "Failed to update results."}

Questions

ℹ️Single-question delete and CSV export are web routes, not JSON: POST /admin/questions/delete/{question_id}, GET /admin/questions/export-csv/{exam_id} — see Web Routes.
POST /api/v01/admin/questions Add one question to an exam Admin
ℹ️Content-Type: multipart/form-data (form fields, not JSON). exam_id required; all other fields optional and default as shown.
multipart/form-data
exam_id:         42              // required
question_text:   "What is 2 + 2?"
option_a:        "3"
option_b:        "4"
option_c:        "5"
option_d:        "6"
correct_answer:  "B"
question_type:   "MCQ"            // default "MCQ" — MCQ | MSQ | NUMERIC
image_path:      ""               // optional image storage key, e.g. "SubjectName/file.png"
positive_marks:  4                // default 4
negative_marks:  1                // default 1
tolerance:       0                // default 0 — used for NUMERIC answers
json — 200
{ "success": true, "message": "Question added." }
500 — {"success": false, "message": "Failed to add question."}
GET /api/v01/admin/questions/{question_id} Fetch a single question Admin
json — 200
{
  "success":  true,
  "question": {
    "id":             101,
    "exam_id":        42,
    "question_text":  "What is 2 + 2?",
    "option_a":       "3",
    "option_b":       "4",
    "option_c":       "5",
    "option_d":       "6",
    "correct_answer": "B",
    "question_type":  "MCQ",
    "image_path":     null,
    "positive_marks": 4,
    "negative_marks": 1,
    "tolerance":      0
  }
}
404 — {"success": false, "message": "Not found."}
PATCH /api/v01/admin/questions/{question_id} Update a question — same fields as create Admin
ℹ️Content-Type: multipart/form-data, same fields as POST /api/v01/admin/questions. Omitted fields keep their current value except exam_id, which defaults back to the question's existing exam.
json — 200
{ "success": true, "message": "Updated." }
404 — {"success": false, "message": "Not found."}
POST /api/v01/admin/questions/delete-multiple Delete several questions and their FK children in one call Admin
json — request
{ "ids": [101, 102, 103] }
json — 200
{ "success": true, "deleted": 3 }
400 — {"success": false, "message": "No IDs provided"}
POST /api/v01/admin/questions/batch-add Insert several questions into one exam in a single write Admin
ℹ️Rows with an empty question_text are silently dropped before insert. Per-question fields default the same way as POST /api/v01/admin/questions.
json — request
{
  "exam_id": 42,
  "questions": [
    {
      "question_text":  "What is 2 + 2?",
      "option_a":       "3",
      "option_b":       "4",
      "option_c":       "5",
      "option_d":       "6",
      "correct_answer": "B",
      "question_type":  "MCQ",
      "positive_marks": 4,
      "negative_marks": 1,
      "tolerance":      0
    }
  ]
}
json — 200
{ "success": true, "added": 1 }
400 — {"success": false, "message": "No valid rows"}
POST /api/v01/admin/questions/bulk-update Set marks/tolerance for every question of one type within an exam Admin
ℹ️exam_id and question_type are required; the type match is case-insensitive. positive_marks/negative_marks/tolerance are each optional — only the ones present are updated.
json — request
{
  "exam_id":        42,
  "question_type":  "MCQ",
  "positive_marks": 5,
  "negative_marks": 1
}
json — 200
{ "success": true, "updated": 12 }
400 — {"success": false, "message": "exam_id and question_type required"}
POST /api/v01/admin/questions/import-csv Bulk-import questions from a CSV file Admin
ℹ️Content-Type: multipart/form-data, field name csv_file. Required columns: exam_id, question_text, option_a, option_b, option_c, option_d, correct_answer, question_type, image_path, positive_marks, negative_marks, tolerance. Rows with an unknown exam_id or empty question_text are skipped, not rejected — the whole file still imports.
json — 200 (all rows valid)
{
  "success":  true,
  "message":  "Imported 24 question(s).",
  "inserted": 24,
  "skipped":  0,
  "errors":   null
}
json — 200 (some rows skipped)
{
  "success":  true,
  "message":  "Imported 22 question(s). Skipped 2.",
  "inserted": 22,
  "skipped":  2,
  "errors":   ["Row 5: skipped", "Row 9: skipped"]
}
400 — {"success": false, "message": "No file uploaded"} 400 — {"success": false, "message": "File must be a CSV"} 400 — {"success": false, "message": "Missing columns: ..."} 400 — {"success": false, "message": "No questions imported. N errors.", "errors": [...]}

Users

GET /api/v01/admin/users/stats Role-count totals Admin
json — 200
{ "total_users": 312, "user_role": 298, "admin_role": 9, "both_roles": 5 }
POST /api/v01/admin/users/update-role Change one user's role Admin
ℹ️new_role must be exactly user, admin, or user,admin. The system's ghost/deleted-account placeholder (id -1) can never be targeted — always 403.
json — request
{ "user_id": 7, "new_role": "user,admin" }
json — 200
{ "success": true, "message": "Role updated to user,admin" }
400 — {"success": false, "message": "Invalid data"} 403 — {"success": false, "message": "System account cannot be modified."}
POST /api/v01/admin/users/bulk-update-roles Change roles for several users in one call Admin
ℹ️Invalid entries and the ghost account are skipped individually rather than failing the whole batch — check errors for what was skipped.
json — request
{
  "updates": [
    { "user_id": 7,  "new_role": "admin" },
    { "user_id": 12, "new_role": "user" }
  ]
}
json — 200
{ "success": true, "message": "Successfully updated 2 user(s)", "errors": null }
400 — {"success": false, "message": "No updates provided"} 400 — {"success": false, "message": "No updates applied", "errors": [...]}

Analytics

ℹ️The paginated results table, result/response detail popups, and PDF download all render HTML/binary, not JSON — see Web Routes: /admin/users-analytics/results, /admin/users-analytics/view-result/{result_id}/{exam_id}, /admin/users-analytics/view-responses/{result_id}/{exam_id}, /admin/users-analytics/download-result/{result_id}.
GET /api/v01/admin/analytics/stats Site-wide totals for the analytics dashboard header Admin
json — 200
{ "total_users": 312, "total_exams": 18, "total_results": 1204, "total_responses": 30512 }
GET /api/v01/admin/analytics/data?timePeriod=&exam=&startDate=&endDate= Score distribution, top performers, per-exam stats, recent activity Admin
ℹ️Query params, all optional: timePeriod (today | week | month | custom | all, default all), exam (exam id filter), startDate/endDate (YYYY-MM-DD, required together when timePeriod=custom). Computed over at most the 50,000 most recent matching results.
json — 200
{
  "summary": {
    "avgScore":      72.4,
    "totalAttempts": 1204,
    "passRate":      81.2,
    "activeUsers":   298,
    "scoreChange":      0,
    "attemptsChange":   0,
    "passRateChange":   0,
    "usersChange":      0
  },
  "charts": {
    "scoreDistribution": [120, 340, 410, 334],  // counts for >=90, 75-89, 60-74, <60
    "examPerformance":   { "labels": ["Midterm Physics"], "data": [74.1] },
    "performanceTrends": { "labels": [], "data": [] },
    "userActivity":      { "labels": [], "data": [] }
  },
  "tables": {
    "topPerformers": [
      { "student_id": "7", "username": "jane.doe", "full_name": "Jane Doe", "avgScore": 94.5, "attempts": 3 }
    ],
    "recentActivity": [
      { "created_at": "15 January 2025 10:30 AM", "username": "jane.doe", "full_name": "Jane Doe", "exam_name": "Midterm Physics", "score": 76, "max_score": 100, "percentage": 76.0 }
    ],
    "examStats": [
      { "name": "Midterm Physics", "attempts": 204, "avgScore": 74.1, "passRate": 82.4 }
    ]
  }
}
500 — {"error": "Failed", "message": "<error text>"}

AI Question Generation

POST /api/v01/admin/ai/generate Start a background job that AI-generates exam questions Admin
ℹ️Content-Type: multipart/form-data. mode determines what else is required: extract/mine need pdf_file; pure needs topic. This only starts the job — it returns a job_id immediately, it does not wait for generation to finish.
multipart/form-data
mode:                 "extract"  // required — extract | mine | pure
exam_id:              42       // required
difficulty:           "Medium"   // default "Medium" — Easy | Medium | Hard
mcq_count:             5        // default 0
msq_count:             2        // default 0
numeric_count:          3        // default 0
mcq_plus:               4        // default 4 — positive marks per MCQ
mcq_minus:              1        // default 1 — negative marks per MCQ
msq_plus:               4        // default 4
msq_minus:              2        // default 2
numeric_plus:           3        // default 3
numeric_tolerance:      0.01     // default 0.01
custom_instructions:   ""        // optional, free text
excluded_texts:        "[]"       // optional, JSON-encoded array of strings to avoid duplicating
pdf_file:              <file>     // required if mode is extract or mine
topic:                 "Newton's Laws"  // required if mode is pure
json — 200
{ "success": true, "job_id": "a1b2c3d4e5f6" }
ℹ️Poll GET /api/v01/admin/ai/status/{job_id} with the returned job_id to track progress and retrieve the generated questions once status is "done".
400 — {"success": false, "message": "PDF file required"} 400 — {"success": false, "message": "No file selected"} 400 — {"success": false, "message": "Topic required"} 500 — {"success": false, "message": "Failed to start job: <error>"}
GET /api/v01/admin/ai/status/{job_id} Poll a generation job's progress Admin
ℹ️Job state is held in-process (in-memory) — only reliable when the app runs as a single worker. status is running, done, or failed; questions is populated once done (or partially populated on failed, if any batches succeeded before the error).
json — 200 (in progress)
{
  "status":             "running",
  "message":            "Generating batch 2 of 4...",
  "last_event":         "batch_start",
  "total_batches":      4,
  "completed_batches":  1,
  "questions_so_far":   5,
  "percent":            40,
  "questions":          [],
  "error":              null
}
json — 200 (done)
{
  "status":   "done",
  "message":  "Complete — 10 questions generated.",
  "percent":  100,
  "questions_so_far": 10,
  "questions": [
    {
      "exam_id":        42,
      "question_text":  "A block of mass m slides...",
      "option_a":       "...",
      "correct_answer": "A",
      "question_type":  "MCQ",
      "positive_marks": 4.0,
      "negative_marks": 1.0
    }
  ]
}
404 — {"success": false, "message": "Job not found"}
ℹ️Once a generation job's status is done, the admin UI navigates to /admin/ai-command-centre/csv-upload?source=ai&job_id={id} — the CSV Editor re-fetches this same status endpoint to load the generated questions, then reuses the exact same preview/edit/Load More/save pipeline as a manually uploaded CSV (POST /api/v01/admin/questions/import-csv, documented under Questions). There is no separate save or export endpoint for AI-generated questions anymore.

Categories

GET /api/v01/admin/categories List every exam category Admin
json — 200
{
  "categories": [
    { "id": 3, "name": "JEE Main", "drive_file_id": "Category/jee-main_a1b2c3d4.png", "image_url": "https://storage.example.com/note-assets/Category/jee-main_a1b2c3d4.png?X-Amz-..." }
  ]
}
POST /api/v01/admin/categories Create a category, optionally with an image Admin
ℹ️Content-Type: multipart/form-data. name required; image optional (png/jpg/jpeg/gif/webp, max 500 KB).
multipart/form-data
name:  "JEE Main"   // required
image: <file>      // optional
json — 200
{ "success": true, "category": { "id": 3, "name": "JEE Main", "drive_file_id": "Category/jee-main_a1b2c3d4.png", "image_url": "https://storage.example.com/note-assets/Category/jee-main_a1b2c3d4.png?X-Amz-..." } }
400 — {"success": false, "message": "Name is required"} 400 — {"success": false, "message": "Failed — name may already exist"}
PATCH /api/v01/admin/categories/{cat_id} Rename a category and/or replace its image Admin
ℹ️Content-Type: multipart/form-data. Both fields optional, but at least one must be present.
multipart/form-data
name:  "JEE Main (Updated)"  // optional
image: <file>              // optional — replaces the existing image
json — 200
{ "success": true }
404 — {"success": false, "message": "Category not found"} 400 — {"success": false, "message": "Nothing to update"}
DELETE /api/v01/admin/categories/{cat_id} Delete a category (blocked if any exam still references it) Admin
json — 200
{ "success": true }
404 — {"success": false, "message": "Category not found"} 400 — {"success": false, "message": "Cannot delete — exams are linked to this category. Reassign them first."}

Attempts

POST /api/v01/admin/attempts/modify Reset, add, or remove attempts for one student on one exam Admin
ℹ️action: reset (delete all attempts), increase (add amount manual attempts), decrease (remove the amount most recent attempts). amount defaults to 1.
json — request
{ "student_id": 7, "exam_id": 42, "action": "increase", "amount": 1 }
json — 200
{ "success": true }
400 — {"success": false, "message": "Not enough attempts to remove"} 400 — {"success": false, "message": "Invalid action"}
POST /api/v01/admin/attempts/bulk-modify Apply the same reset/increase/decrease action to several student+exam pairs Admin
ℹ️Same action/amount semantics as /attempts/modify, applied to every item. Failures on individual items don't stop the rest — check errors.
json — request
{
  "items": [
    { "student_id": 7,  "exam_id": 42 },
    { "student_id": 12, "exam_id": 42 }
  ],
  "action": "reset",
  "amount": 1
}
json — 200
{ "success": true, "processed": 2, "errors": null }

Access Requests

GET /api/v01/admin/access-requests?status=pending&page=1 Paginated list of role-change requests Admin
ℹ️status: pending (default) or anything else, which returns both completed and denied. page default 1, 25 per page.
json — 200
{
  "requests": [
    {
      "request_id":       14,
      "username":         "jane.doe",
      "email":            "jane@example.com",
      "current_access":   "user",
      "requested_access": "admin",
      "request_date":     "15 January 2025 10:30 AM",
      "status":           "pending",
      "reason":           "[USER REQUEST] I need to manage exams",
      "processed_by":     "Admin",
      "processed_date":   null
    }
  ],
  "total":       1,
  "page":        1,
  "per_page":    25,
  "total_pages": 1
}
GET /api/v01/admin/access-requests/stats Request-count totals by status Admin
json — 200
{ "pending": 4, "completed": 18, "denied": 2, "total": 24 }
POST /api/v01/admin/access-requests/{request_id}/approve Approve a pending request and grant the new role Admin
json — request
{ "approved_access": "admin" }
json — 200
{ "success": true, "message": "Approved. User now has admin access." }
400 — {"success": false, "message": "Please select an access level"} 404 — {"success": false, "message": "Request not found or already processed"} 404 — {"success": false, "message": "User not found"}
POST /api/v01/admin/access-requests/{request_id}/deny Deny a pending request with a reason Admin
json — request
{ "reason": "Not eligible for admin access at this time." }
json — 200
{ "success": true, "message": "Request denied." }
400 — {"success": false, "message": "Please provide a denial reason"} 404 — {"success": false, "message": "Not found or already processed"}

Images

POST /api/v01/admin/images Bulk-upload question images to a subject's storage folder Admin
ℹ️Content-Type: multipart/form-data. Files matching an existing filename in the target folder are overwritten (updated in place) rather than duplicated.
multipart/form-data
subject_folder_id: "1AbCFolderId..."  // required
images:            <file>, <file>, ...  // required, one or more
json — 200
{
  "success":  true,
  "uploaded": 2,
  "failed":   [
    { "filename": "q17.bmp", "error": "Not allowed (.bmp)" }
  ]
}
400 — {"success": false, "message": "No folder selected."} 400 — {"success": false, "message": "No files received."} 500 — {"success": false, "message": "<storage service error>"}
ℹ️Allowed extensions and the per-file size cap are server config (ALLOWED_IMAGE_EXTS, MAX_IMAGE_SIZE_KB, default 500 KB) — oversized/disallowed files are reported per-file in failed, not rejected as a whole request.

Discussion Moderation

PUT /api/v01/admin/discussions/comments/{comment_id}/pin Toggle a discussion comment's pinned state Admin
ℹ️No request body. Flips the comment's current pinned flag.
json — 200
{ "success": true, "is_pinned": true }
404 — {"success": false}
PUT /api/v01/admin/discussions/comments/{comment_id}/best Toggle a comment as the best answer for its question Admin
ℹ️No request body. Marking a comment best-answer automatically un-marks any other best-answer on the same question — at most one per question.
json — 200
{ "success": true, "is_best_answer": true }
404 — {"success": false}

Notes / Notebooks API

Notebook, page, and asset CRUD for the drawing-canvas Notes feature, plus the public library. All require auth. Base path: /api/v01/. Newly documented in the v01 refactor — request/response bodies follow the same { success, ... } shape as the rest of this reference.

POST
/api/v01/notebooks
Create a notebook
GET
/api/v01/notebooks/{notebook_id}
Fetch one owned notebook
PATCH
/api/v01/notebooks/{notebook_id}
Rename / update notebook
DELETE
/api/v01/notebooks/{notebook_id}
Move to Trash
POST
/api/v01/notebooks/{notebook_id}/restore
Restore from Trash
DELETE
/api/v01/notebooks/{notebook_id}/permanent
Permanently delete (must be in Trash)
GET
/api/v01/notebooks/{notebook_id}/pages
List pages
POST
/api/v01/notebooks/{notebook_id}/pages
Create a page
PATCH
/api/v01/notebooks/{notebook_id}/pages/{page_id}
Rename page
DELETE
/api/v01/notebooks/{notebook_id}/pages/{page_id}
Delete page
GET
/api/v01/notebooks/{notebook_id}/pages/{page_id}/objects
Fetch canvas objects for a page
PUT
/api/v01/notebooks/{notebook_id}/pages/{page_id}/objects
Save canvas objects. Body: { objects, deleted_ids, start_index }
POST
/api/v01/notebooks/{notebook_id}/assets
Upload an image asset (multipart image)
GET
/api/v01/assets/{asset_id}/url
Signed/local URL for one asset
GET
/api/v01/assets/{asset_id}/file
Same-origin byte stream (used by PDF export to avoid canvas tainting)
POST
/api/v01/notebooks/import
Import a notebook from exported JSON
GET
/api/v01/notebooks/{notebook_id}/export
Export owned notebook as downloadable JSON
POST
/api/v01/notebooks/{notebook_id}/export-pdf
Multipart pages (client-rendered) → PDF download
GET
/api/v01/library?q=
Search the public notebook library
POST
/api/v01/library/{notebook_id}/{kind}
Toggle engagement, kind: like | bookmark
GET
/api/v01/library/{notebook_id}/pages
List pages of a public notebook
GET
/api/v01/library/{notebook_id}/pages/{page_id}/objects
Fetch canvas objects (public, read-only)
GET
/api/v01/library/{notebook_id}/export
Export a public notebook as downloadable JSON

Web Routes

HTML pages — not JSON APIs. Listed separately per the site's route/API separation. Most require a logged-in session and redirect to /login (or /admin/login) otherwise.

GET
/, /about, /contact, /support, /privacy-policy, /terms-of-service, /account-deletion-policy
Public static pages
GET
/login, /create_account, /select-portal, /logout
Auth pages
GET
/reset-password[/{token}], /setup-password/{token}
Password reset / account setup pages
GET
/auth/google, /auth/google/callback
Google OAuth redirect flow — never rename, registered with Google's console
GET
/dashboard, /results_history, /analytics
Student dashboard, exam history, personal analytics
GET
/select-category, /set-category
Category selection
GET
/exam-instructions/{exam_id}, /exam/{exam_id}
Exam instructions and the exam-taking page
POST
/submit-exam/{exam_id}
Submit exam — redirects to result or result-pending
GET
/result/{exam_id}[/{result_id}], /response/{exam_id}[/{result_id}], /result-pending/{exam_id}[/{result_id}]
Result / response-review / pending pages
GET
/response-pdf/{exam_id}
Student's own response PDF download
GET
/notes, /notes/trash, /notes/library, /notes/notebook/{id}, /notes/public/{id}
Notes pages
GET
/notes/asset-file/{path}
Local-storage-backend asset streaming (also STORAGE_LOCAL_URL_PREFIX — do not rename)
GET
/chat, /ai-assistant
Chat and AI Study Assistant page shells (JSON APIs documented above)
GET
/api-docs
This page
GET
/admin/login, /admin/logout, /admin/dashboard, /admin/publish
Admin auth + dashboard
GET
/admin/subjects[/edit/{id}], /admin/categories, /admin/exams[/edit/{id}], /admin/questions, /admin/upload-images, /admin/latex_editor
Admin content-management pages (JSON APIs documented above)
POST
/admin/subjects/delete/{id}, /admin/questions/delete/{id}
Form-POST deletes (not JSON)
GET
/admin/questions/export-csv/{exam_id}
CSV download
GET
/admin/users/manage, /admin/attempts, /admin/requests, /admin/users-analytics[/results|/analytics], /admin/ai-command-centre
Admin management pages, data loaded via the JSON APIs documented above
GET
/admin/users-analytics/view-result/{result_id}/{exam_id}, /admin/users-analytics/view-responses/{result_id}/{exam_id}
Result/response detail popups
GET
/admin/users-analytics/download-result/{result_id}
Result PDF download