Complete reference for all JSON-returning endpoints. HTML routes are excluded. Supports both session cookie auth (web) and JWT Bearer auth (mobile/external).
Two auth methods. Web browser uses session cookies. External clients (mobile, Postman, third-party) use JWT Bearer tokens.
POST /login via browser, server sets an HttpOnly session cookie. All web UI routes use this automatically. Session lifetime: 3 hours.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.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.
POST /api/v01/auth/login returns access_token (1h) + refresh_token (30d)Authorization: Bearer <access_token> header with every API call401 token_expired_or_invalid when access token expiresPOST /api/v01/auth/refresh with refresh token → new access token, no re-login neededimport 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())
| Endpoint | Limit | Window | Response |
|---|---|---|---|
/login, /admin/login | 3 attempts | Per IP + identifier | 15-min lockout |
POST /api/v01/chat/conversations/:id/messages | 1 msg / 2s | Per user | 429 |
POST /api/v01/discussions/:id | 1 msg / 10s | Per user | 429 |
POST /api/v01/assistant/messages | 50 / day | Per user, resets midnight (independent of conversation count) | 429 with limit_reached:true |
POST /api/v01/assistant/messages | 100 / conversation | Per conversation (~50 exchanges), start a new chat to continue | 400 with limit_reached:"conversation" |
| All other endpoints | Unlimited | — | — |
All JSON errors return a consistent shape with status: "error".
{ "status": "error", "message": "Human-readable error description" }
token_expired_or_invalid means JWT expired. Call /api/v01/auth/refresh to get a new access token.Token-based auth for external clients. These endpoints do not require an existing session — they create tokens from credentials.
{
"username": "john.doe", // username OR email accepted
"password": "MyPass@2025"
}{
"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.{ "status": "error", "message": "Invalid credentials. 2 attempts remaining." }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.{ "id_token": "<Google ID token from client SDK>" }{
"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" }
}
}{ "refresh_token": "dGhpcyBpcyBhIHJlZnJlc2ggdG9rZW4..." }{
"status": "success",
"data": {
"access_token": "eyJ...",
"token_type": "Bearer",
"expires_in": 3600
}
}{ "refresh_token": "dGhp..." }{ "status": "success", "message": "Logged out" }Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
{
"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"
}
}Password reset, access requests, and account deletion. HTML form routes /login, /logout, /create_account are excluded.
{ "email": "john@example.com" }{ "success": true, "message": "If an account exists with this email, a reset link has been sent." }{ "username": "john.doe", "email": "john@example.com" }{
"success": true,
"user": { "username": "john.doe", "current_access": "user" },
"available_requests": ["admin", "user,admin"],
"has_pending_request": false,
"can_request": true
}{
"username": "john.doe",
"email": "john@example.com",
"current_access": "user",
"requested_access": "admin",
"user_reason": "I need to manage exams for my batch"
}{ "success": true, "message": "Request submitted.", "request_id": 7 }{ "confirm": "DELETE" } // must be exactly "DELETE"Start, sync answers, submit, and check attempt status. All require auth.
{ "success": true, "cached": true, "question_count": 30 }{
"success": true,
"redirect_url": "/exam/42",
"resumed": false,
"attempt_id": 17,
"attempt_number": 1,
"fresh_start": true
}{ "success": true, "resumed": true, "attempt_id": 17, "redirect_url": "/exam/42" }{
"answers": {
"101": "A", // MCQ — single letter
"102": ["A", "C"], // MSQ — array
"103": "42.5" // NUMERIC — string
},
"markedForReview": [101, 104]
}result_mode.{
"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
}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.
{
"success": true,
"dailyLimit": 50,
"questionsUsed": 12,
"conversations": [
{ "id": 4, "title": "Bernoulli's Theorem", "messageCount": 6, "createdAt": "...", "updatedAt": "..." }
],
"hasMoreConversations": false
}search matches conversation title or message text (case-insensitive). Max limit is 50.{ "success": true, "conversations": [ "..." ], "hasMore": true }{ "success": true, "messages": [ { "text": "...", "isUser": true, "timestamp": "..." } ], "hasMore": true, "conversation": { "..." } }{ "message": "Explain conservation of momentum with an example", "conversation_id": null }null for conversation_id to start a new conversation — no empty conversation is created unless a message is actually sent.{
"success": true,
"response": "[FINAL ANSWER]\nConservation of momentum states that...",
"conversation_id": 4,
"title": "Conservation Of Momentum",
"questions_remaining": 37
}{ "success": false, "message": "Daily limit reached. Resets at midnight.", "limit_reached": true }{ "title": "Bernoulli's Theorem" }{ "success": true, "conversation": { "..." } }ai_chat_history rows. Does not affect the daily usage counter.{ "success": true, "message": "Conversation deleted." }Real-time peer chat with connection requests, groups, and unread tracking. All require auth.
{
"success": true,
"users": [
{
"id": 5,
"username": "jane.smith",
"full_name": "Jane Smith",
"online": true,
"connection_status": "accepted" // null | "pending" | "accepted" | "rejected"
}
]
}{
"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" }
}
]
}{
"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
}
]
}before ISO timestamp to load older messages (pagination).{
"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
}{ "success": true, "message": { "id": 89, "message": "Hey, how are you?", "is_own": true, "created_at": "..." } }{ "message": "Edited message text" }{ "recipient_id": 5 }{
"success": true,
"requests": [
{ "conn_id": 3, "from_id": 5, "from_name": "Jane Smith", "created_at": "2025-01-15T09:00:00" }
]
}{ "action": "accept" } // or "reject"{ "success": true, "conv_id": 12 }{ "success": true, "unread": 5, "requests": 2 }{ "success": true, "status": { "1": true, "2": false, "5": true } }{ "name": "Physics Study Group", "member_ids": [3, 5, 7] }Per-question threaded comments with replies, pinning, and best-answer marking.
{
"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": []
}
]
}username: "Deleted Account" with a placeholder. Thread structure is preserved.{
"message": "The answer is B because...",
"exam_id": 42, // optional
"parent_id": 1 // optional — for replies
}{ "message": "Updated explanation..." }{ "question_ids": [101, 102, 103] } // max 100{ "success": true, "counts": { "101": 5, "102": 0, "103": 12 } }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.
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.GET/POST /admin/exams, GET/POST /admin/exams/edit/{exam_id} — see Web Routes.{ "success": true, "message": "Exam 'Midterm Physics' deleted." }results_released flag — call again to toggle back.{ "success": true, "message": "Results for 'Midterm Physics' have been released.", "released": true }POST /admin/questions/delete/{question_id}, GET /admin/questions/export-csv/{exam_id} — see Web Routes.Content-Type: multipart/form-data (form fields, not JSON). exam_id required; all other fields optional and default as shown.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
{ "success": true, "message": "Question added." }{
"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
}
}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.{ "success": true, "message": "Updated." }{ "ids": [101, 102, 103] }{ "success": true, "deleted": 3 }question_text are silently dropped before insert. Per-question fields default the same way as POST /api/v01/admin/questions.{
"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
}
]
}{ "success": true, "added": 1 }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.{
"exam_id": 42,
"question_type": "MCQ",
"positive_marks": 5,
"negative_marks": 1
}{ "success": true, "updated": 12 }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.{
"success": true,
"message": "Imported 24 question(s).",
"inserted": 24,
"skipped": 0,
"errors": null
}{
"success": true,
"message": "Imported 22 question(s). Skipped 2.",
"inserted": 22,
"skipped": 2,
"errors": ["Row 5: skipped", "Row 9: skipped"]
}q (matches username/email/full_name), role (user | admin | both), page (default 1, 50 per page).{
"users": [
{
"id": 7,
"username": "jane.doe",
"email": "jane@example.com",
"full_name": "Jane Doe",
"role": "user",
"created_at": "15 January 2025 10:30 AM",
"updated_at": "15 January 2025 10:30 AM"
}
],
"total": 312,
"page": 1,
"per_page": 50,
"total_pages": 7
}{ "total_users": 312, "user_role": 298, "admin_role": 9, "both_roles": 5 }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.{ "user_id": 7, "new_role": "user,admin" }{ "success": true, "message": "Role updated to user,admin" }errors for what was skipped.{
"updates": [
{ "user_id": 7, "new_role": "admin" },
{ "user_id": 12, "new_role": "user" }
]
}{ "success": true, "message": "Successfully updated 2 user(s)", "errors": null }/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}.{ "total_users": 312, "total_exams": 18, "total_results": 1204, "total_responses": 30512 }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.{
"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 }
]
}
}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.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
{ "success": true, "job_id": "a1b2c3d4e5f6" }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".status is running, done, or failed; questions is populated once done (or partially populated on failed, if any batches succeeded before the error).{
"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
}{
"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
}
]
}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": [
{ "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-..." }
]
}Content-Type: multipart/form-data. name required; image optional (png/jpg/jpeg/gif/webp, max 500 KB).name: "JEE Main" // required image: <file> // optional
{ "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-..." } }Content-Type: multipart/form-data. Both fields optional, but at least one must be present.name: "JEE Main (Updated)" // optional image: <file> // optional — replaces the existing image
{ "success": true }{ "success": true }q (username/full_name search), exam_id, status (unlimited | available | exhausted), page (default 1, 50 per page).{
"rows": [
{
"student_id": 7,
"username": "jane.doe",
"exam_id": 42,
"exam_name": "Midterm Physics",
"max_attempts": "3",
"attempts_used": 2,
"remaining": 1,
"status": "available"
}
],
"total": 1,
"page": 1,
"per_page": 50,
"total_pages": 1
}max_attempts/remaining are "∞" strings for exams with no attempt cap.action: reset (delete all attempts), increase (add amount manual attempts), decrease (remove the amount most recent attempts). amount defaults to 1.{ "student_id": 7, "exam_id": 42, "action": "increase", "amount": 1 }{ "success": true }action/amount semantics as /attempts/modify, applied to every item. Failures on individual items don't stop the rest — check errors.{
"items": [
{ "student_id": 7, "exam_id": 42 },
{ "student_id": 12, "exam_id": 42 }
],
"action": "reset",
"amount": 1
}{ "success": true, "processed": 2, "errors": null }status: pending (default) or anything else, which returns both completed and denied. page default 1, 25 per page.{
"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
}{ "pending": 4, "completed": 18, "denied": 2, "total": 24 }{ "approved_access": "admin" }{ "success": true, "message": "Approved. User now has admin access." }{ "reason": "Not eligible for admin access at this time." }{ "success": true, "message": "Request denied." }Content-Type: multipart/form-data. Files matching an existing filename in the target folder are overwritten (updated in place) rather than duplicated.subject_folder_id: "1AbCFolderId..." // required images: <file>, <file>, ... // required, one or more
{
"success": true,
"uploaded": 2,
"failed": [
{ "filename": "q17.bmp", "error": "Not allowed (.bmp)" }
]
}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.{ "success": true, "is_pinned": true }{ "success": true, "is_best_answer": true }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.
{ objects, deleted_ids, start_index }image)pages (client-rendered) → PDF downloadkind: like | bookmarkHTML 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.
STORAGE_LOCAL_URL_PREFIX — do not rename)