smartaiexam.in
v1.1 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
smartaiexam.in
Version
v1.1
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/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/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/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/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/chat/conversations",
    headers=headers,
)
print(convs.json())

Rate Limits

EndpointLimitWindowResponse
/login, /admin/login3 attemptsPer IP + identifier15-min lockout
POST /api/chat/messages/:id1 msg / 2sPer user429
POST /api/discussion/:id1 msg / 10sPer user429
POST /api/study-chat50 / dayPer user, resets midnight429 with limit_reached:true
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/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/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/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/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/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/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/request-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/validate-user-for-request 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/submit-access-request 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
POST /api/delete-account 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 /preload-exam/{exam_id} Pre-cache exam questions in session Auth
json — 200
{ "success": true, "cached": true, "question_count": 30 }
ℹ️Call before starting exam. Resolves Google Drive image URLs and stores questions in session. Returns instantly if already cached.
POST /start-exam/{exam_id} 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/sync-exam-answers/{exam_id} 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/exam-attempts-status/{exam_id} 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 /_ping Session heartbeat — check if session is alive Auth
204 — Session alive (no body) 401 — { "reason": "no_session" }

AI Study Assistant

Groq LLM (LLaMA 3.3 70B) powered study chat. Responses include LaTeX for math/science. Daily limit: 50 messages/user/day.

GET /api/assistant-init Get daily limits + full chat history in one call Auth
json — 200
{
  "success":       true,
  "dailyLimit":    50,
  "questionsUsed": 12,
  "history": [
    { "text": "What is Newton's second law?", "isUser": true,  "timestamp": "2025-01-15T10:30:00" },
    { "text": "Newton's second law states $F = ma$...", "isUser": false, "timestamp": "2025-01-15T10:30:05" }
  ]
}
POST /api/study-chat Send message to AI study assistant Auth
json
{ "message": "Explain conservation of momentum with an example" }
json — 200
{
  "success":              true,
  "response":             "[FINAL ANSWER]\nConservation of momentum states that...",
  "questions_remaining":  37
}
400 — Message too short or too long 429 — Daily limit reached
json — 429
{ "success": false, "message": "Daily limit reached.", "limit_reached": true }
POST /api/clear-chat-history Delete all AI chat history for current user Auth
ℹ️No request body. Deletes all rows from ai_chat_history. Daily usage counter is NOT reset.
json — 200
{ "success": true, "message": "Chat history cleared." }

Chat Endpoints

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

GET /api/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/chat/messages/{conv_id}?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/chat/messages/{conv_id} 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/chat/messages/{msg_id}/edit Edit own message Auth
json — request
{ "message": "Edited message text" }
200 — { success: true, message: "Edited text" } 403 — Not own message
DELETE /api/chat/messages/{msg_id} Soft-delete own message Auth
200 — { success: true } 403 — Not own message
POST /api/chat/conversations/{conv_id}/read Mark conversation as read — reset unread count to 0 Auth
200 — { success: true }
POST /api/chat/request Send connection request to a user Auth
json — request
{ "recipient_id": 5 }
200 — { success: true } 409 — Already sent or connected
GET /api/chat/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/chat/request/{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/chat/unread_count Total unread messages + pending connection requests Auth
json — 200
{ "success": true, "unread": 5, "requests": 2 }
GET /api/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/chat/group Create a group conversation Auth
json — request
{ "name": "Physics Study Group", "member_ids": [3, 5, 7] }
200 — { success: true, conv_id: 15 }
DELETE /api/chat/friend/{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/discussion/{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/discussion/{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/discussion/edit/{comment_id} Edit own comment (admins can edit any) Auth
json — request
{ "message": "Updated explanation..." }
200 — { success: true } 403 — Forbidden
DELETE /api/discussion/delete/{comment_id} Soft-delete own comment (admins can delete any) Auth
200 — { success: true } 403 — Forbidden
POST /api/discussion/counts/bulk 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

All admin endpoints require an active admin session or JWT with admin role. Base path: /admin/

⚠️All endpoints below require admin in role. Regular user JWT returns 403.
POST /admin/exams Exam CRUD endpoints Admin
POST
/admin/exams
Create exam. Fields: name, date, start_time, duration, status, instructions, positive_marks, negative_marks, max_attempts, result_mode, result_delay, category_id
POST
/admin/exams/edit/{exam_id}
Update exam fields
POST
/admin/exams/delete/{exam_id}
Delete exam + all questions, results, responses → { success: true }
POST
/admin/exams/{exam_id}/release-results
Toggle result release → { success, released: bool }
GET /admin/questions Question CRUD, bulk ops, CSV import/export Admin
GET
/admin/questions/get/{question_id}
Fetch single question → { success, question }
POST
/admin/questions/add-ajax
Add question. Fields: exam_id, question_text, option_a-d, correct_answer, question_type, image_path, positive_marks, negative_marks, tolerance
POST
/admin/questions/edit-ajax/{question_id}
Update question
POST
/admin/questions/delete/{question_id}
Delete question + FK children
POST
/admin/questions/delete-multiple
Bulk delete. Body: { ids: [1,2,3] }{ success, deleted: N }
POST
/admin/questions/batch-add
Bulk insert. Body: { exam_id, questions: [...] }
GET
/admin/questions/export-csv/{exam_id}
Download CSV of all questions for exam
POST
/admin/questions/import-csv
Upload CSV → { success, inserted, skipped, errors }
GET /admin/api/users/search User search, stats, role management Admin
GET
/admin/api/users/search?q=&role=&page=
Paginated user search. role filter: user | admin | both
GET
/admin/api/users/stats
Role counts → { total_users, user_role, admin_role, both_roles }
POST
/admin/users/update-role
Update role. Body: { user_id, new_role }. Roles: user | admin | user,admin
POST
/admin/users/bulk-update-roles
Bulk update. Body: { updates: [{user_id, new_role}] }
GET /admin/api/users-analytics/stats Analytics, result data, PDF download Admin
GET
/admin/api/users-analytics/stats
Counts → { total_users, total_exams, total_results, total_responses }
GET
/admin/api/users-analytics/data?timePeriod=&exam=&startDate=&endDate=
Full analytics JSON — score distribution, top performers, exam stats, recent activity
GET
/admin/users-analytics/results?user=&exam=&dateFrom=&dateTo=&page=&partial=1
Paginated results. partial=1 returns table HTML fragment only
GET
/admin/users-analytics/download-result/{result_id}
Download result PDF
POST /admin/ai-command-centre/generate AI Question Generation via Gemini Admin
ℹ️Multipart form data. mode determines what else is required.
multipart/form-data
mode:              "extract"  // extract | mine | pure
exam_id:           42
difficulty:        "Medium"   // Easy | Medium | Hard
mcq_count:         5
msq_count:         2
numeric_count:     3
pdf_file:          <file>     // required for extract/mine
topic:             "Newton's Laws"  // required for pure
json — 200
{
  "success":   true,
  "count":     10,
  "questions": [
    {
      "exam_id":        42,
      "question_text":  "A block of mass $\\large m$ slides...",
      "option_a":       "$\\large \\frac{1}{2}mv^2$",
      "correct_answer": "A",
      "question_type":  "MCQ",
      "positive_marks": 4.0,
      "negative_marks": 1.0
    }
  ]
}
POST
/admin/ai-command-centre/save
Save to DB. Body: { questions: [...] }{ success, count }
POST
/admin/ai-command-centre/export-csv
Export as CSV. Body: { questions: [...] } → CSV download
GET /admin/api/categories Category CRUD with Google Drive image upload Admin
GET
/admin/api/categories
List all categories → { categories: [...] }
POST
/admin/categories/create
Create. Multipart: name, image (optional, max 5MB)
POST
/admin/categories/update/{cat_id}
Update name or image
POST
/admin/categories/delete/{cat_id}
Delete (fails if exams linked)
GET /admin/api/attempts/search Attempt management — search, modify, bulk modify Admin
GET
/admin/api/attempts/search?q=&exam_id=&status=&page=
Paginated attempts. status: unlimited | available | exhausted
POST
/admin/attempts/modify
Modify. Body: { student_id, exam_id, action: "reset|increase|decrease", amount }
POST
/admin/attempts/bulk-modify
Bulk. Body: { items: [{student_id, exam_id}], action, amount }
GET /admin/api/requests/list Access request management Admin
GET
/admin/api/requests/list?status=pending&page=1
Paginated requests. status: pending | completed | denied
GET
/admin/api/requests/stats
Counts → { pending, completed, denied, total }
POST
/admin/requests/approve/{request_id}
Approve. Body: { approved_access: "admin" }
POST
/admin/requests/deny/{request_id}
Deny. Body: { reason: "..." }