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).
Authentication
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/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.
POST /api/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/auth/refresh with refresh token → new access token, no re-login neededimport 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
| Endpoint | Limit | Window | Response |
|---|---|---|---|
/login, /admin/login | 3 attempts | Per IP + identifier | 15-min lockout |
POST /api/chat/messages/:id | 1 msg / 2s | Per user | 429 |
POST /api/discussion/:id | 1 msg / 10s | Per user | 429 |
POST /api/study-chat | 50 / day | Per user, resets midnight | 429 with limit_reached:true |
| All other endpoints | Unlimited | — | — |
Error Handling
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/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.
{
"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"
}
}Auth JSON Endpoints
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"Exam Endpoints
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
}AI Study Assistant
Groq LLM (LLaMA 3.3 70B) powered study chat. Responses include LaTeX for math/science. Daily limit: 50 messages/user/day.
{
"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" }
]
}{ "message": "Explain conservation of momentum with an example" }{
"success": true,
"response": "[FINAL ANSWER]\nConservation of momentum states that...",
"questions_remaining": 37
}{ "success": false, "message": "Daily limit reached.", "limit_reached": true }ai_chat_history. Daily usage counter is NOT reset.{ "success": true, "message": "Chat history cleared." }Chat Endpoints
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] }Discussion Endpoints
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 } }Admin JSON APIs
All admin endpoints require an active admin session or JWT with admin role. Base path: /admin/
admin in role. Regular user JWT returns 403.{ success: true }{ success, released: bool }{ success, question }{ ids: [1,2,3] } → { success, deleted: N }{ exam_id, questions: [...] }{ success, inserted, skipped, errors }user | admin | both{ total_users, user_role, admin_role, both_roles }{ user_id, new_role }. Roles: user | admin | user,admin{ updates: [{user_id, new_role}] }{ total_users, total_exams, total_results, total_responses }partial=1 returns table HTML fragment onlymode determines what else is required.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
{
"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
}
]
}{ questions: [...] } → { success, count }{ questions: [...] } → CSV download{ categories: [...] }name, image (optional, max 5MB)unlimited | available | exhausted{ student_id, exam_id, action: "reset|increase|decrease", amount }{ items: [{student_id, exam_id}], action, amount }pending | completed | denied{ pending, completed, denied, total }{ approved_access: "admin" }{ reason: "..." }