Comprehensive technical specification and integration contracts for the TutorBox backend REST API.
| 🏠 TutorBox | 📚 Docs | ⚙️ Backend | 📱 PWA | 🔌 Infra |
|---|
📍 Docs › REST API Reference • Related: Database Schema • Backend Guide
- 1. System Overview & Base URL
- 2. Authentication & Session Flow
- 3. Role-Based Access Control (RBAC) Matrix
- 4. Security Policies & Guards
- 5. Unified Error Response Format
- 6. Detailed Endpoint Contracts
- 6.1 System & Health (
GET /health) - 6.2 Authentication (
POST /login,POST /logout) - 6.3 User Self-Service (
POST /signup,GET /users/me,PATCH /users/me/pin,PATCH /users/me/username) - 6.4 Staff Administration (
GET /users,POST /users,POST /users/{user_id}/reset-pin,DELETE /users/{user_id},POST /users/{user_id}/recover) - 6.5 System Audit (
GET /audit-logs) - 6.6 Hardware Clicker & Device Fleet Management (
GET /devices,POST /devices,POST /devices/{device_id}/assign,POST /devices/{device_id}/unassign,DELETE /devices/{device_id}) - 6.7 Quiz & Diagnostic Question Bank (
GET /quiz/topics,GET /quiz/schema,POST /quiz/validate,POST /quiz/generate,GET /quiz/questions,GET /quiz/questions/{id},POST /quiz/questions,DELETE /quiz/questions/{id})
- 6.1 System & Health (
- Next Steps
The TutorBox API runs on the NVIDIA Jetson Orin Nano edge appliance and communicates with the React/Vite Progressive Web Application (PWA) over the local classroom WLAN/Ethernet network.
- Base URL:
http://<appliance-ip>:8000(e.g.,http://127.0.0.1:8000in local development) - Interactive Swagger UI:
http://<appliance-ip>:8000/docs - Raw OpenAPI JSON Schema:
http://<appliance-ip>:8000/openapi.json - Content-Type:
application/json(unless otherwise noted)
TutorBox uses stateful Bearer Session Tokens stored in the local SQLite database.
sequenceDiagram
autonumber
actor Client as PWA Client
participant API as FastAPI Backend
participant DB as SQLite DB
Client->>API: POST /login {"username": "student1", "pin": "1234"}
API->>DB: Query user & verify bcrypt hash
API->>DB: INSERT INTO sessions (id, user_id, is_active) VALUES (uuid, id, 1)
API-->>Client: 200 OK {"session_id": "<uuid4>", "username": "student1", "must_change_pin": false}
Note over Client,API: Subsequent requests include Bearer Header
Client->>API: GET /users/me (Authorization: Bearer <uuid4>)
API->>DB: Query sessions JOIN users WHERE id = uuid AND is_active = 1
API-->>Client: 200 OK {"user_id": 1, "username": "student1", "role": "student", ...}
Client->>API: POST /logout (Authorization: Bearer <uuid4>)
API->>DB: UPDATE sessions SET is_active = 0 WHERE id = uuid
API-->>Client: 200 OK {"detail": "Logged out."}
For all protected routes, the client must transmit the session token in the HTTP Authorization header:
Authorization: Bearer <session_id>TutorBox enforces strict role-based access across three user roles:
student: Self-service learner account.teacher: Classroom supervisor (can manage students, other teachers, and hardware clickers).admin: System administrator (can manage all accounts, create/recover admins, view audit logs, and manage devices).
| Endpoint | Method | Public | Student | Teacher | Admin | Gated by Pending Rotation? |
|---|---|---|---|---|---|---|
/health |
GET |
✅ | ✅ | ✅ | ✅ | No (Public) |
/signup |
POST |
✅ | ✅ | ✅ | ✅ | No (Public) |
/login |
POST |
✅ | ✅ | ✅ | ✅ | No (Public) |
/logout |
POST |
❌ | ✅ | ✅ | ✅ | No (Allowlist) |
/users/me |
GET |
❌ | ✅ | ✅ | ✅ | No (Allowlist) |
/users/me/pin |
PATCH |
❌ | ✅ | ✅ | ✅ | No (Allowlist) |
/users/me/username |
PATCH |
❌ | ✅ | ✅ | ✅ | Yes (403 if rotation pending) |
/users |
GET |
❌ | ❌ | ✅ | ✅ | Yes (403 if rotation pending) |
/users |
POST |
❌ | ❌ | ✅ (student/teacher) | ✅ (any role) | Yes (403 if rotation pending) |
/users/{user_id}/reset-pin |
POST |
❌ | ❌ | ✅ (student/teacher) | ✅ (any role) | Yes (403 if rotation pending) |
/users/{user_id} |
DELETE |
❌ | ❌ | ✅ (student/teacher) | ✅ (any role) | Yes (403 if rotation pending) |
/users/{user_id}/recover |
POST |
❌ | ❌ | ✅ (student/teacher) | ✅ (any role) | Yes (403 if rotation pending) |
/audit-logs |
GET |
❌ | ❌ | ❌ | ✅ | Yes (403 if rotation pending) |
/devices |
GET |
❌ | ❌ | ✅ | ✅ | Yes (403 if rotation pending) |
/devices |
POST |
❌ | ❌ | ✅ | ✅ | Yes (403 if rotation pending) |
/devices/{device_id}/assign |
POST |
❌ | ❌ | ✅ | ✅ | Yes (403 if rotation pending) |
/devices/{device_id}/unassign |
POST |
❌ | ❌ | ✅ | ✅ | Yes (403 if rotation pending) |
/devices/{device_id} |
DELETE |
❌ | ❌ | ✅ | ✅ | Yes (403 if rotation pending) |
/quiz/topics |
GET |
✅ | ✅ | ✅ | ✅ | No (Public) |
/quiz/schema |
GET |
✅ | ✅ | ✅ | ✅ | No (Public) |
/quiz/validate |
POST |
✅ | ✅ | ✅ | ✅ | No (Public) |
/quiz/generate |
POST |
❌ | ❌ | ✅ | ✅ | Yes (403 if rotation pending) |
/quiz/questions |
GET |
❌ | ❌ | ✅ | ✅ | Yes (403 if rotation pending) |
/quiz/questions/{id} |
GET |
❌ | ❌ | ✅ | ✅ | Yes (403 if rotation pending) |
/quiz/questions |
POST |
❌ | ❌ | ✅ | ✅ | Yes (403 if rotation pending) |
/quiz/questions/{id} |
DELETE |
❌ | ❌ | ✅ | ✅ | Yes (403 if rotation pending) |
- When a staff member resets an account's PIN or recovers an account,
must_change_pinis set to1in SQLite. - Upon login, the client receives
"must_change_pin": true. - Allowlist Routes: The user can only call
GET /users/me,PATCH /users/me/pin, andPOST /logout. - Gated Routes: All other operational and administrative endpoints immediately reject the request with
403 Forbidden({"detail": "PIN change required."}). - Once
PATCH /users/me/pinsucceeds,must_change_pinis cleared to0.
To prevent timing or side-channel oracle attacks during credential modifications:
- The backend first verifies the caller's
current_pin. If incorrect, it immediately returns401 Unauthorized. - Only after
current_pinis cryptographically validated does the backend compare whethernew_pin == current_pinornew_username == current_username(returning422 Unprocessable Entity).
- Credential Lockout (
InMemoryRateLimiter): 5 consecutive failed login/credential attempts result in a temporary lockout on the targeted username (returning429 Too Many Requests). - Registration Throttle (
SlidingWindowLimiter): Global signup requests are capped to prevent brute-force storage exhaustion on edge hardware.
All error responses return a standardized JSON object:
{
"detail": "Descriptive error explanation."
}200 OK: Request succeeded.201 Created: Resource created successfully.400 Bad Request: Malformed payload syntax.401 Unauthorized: Missing, invalid, or expired session token, or invalid username/PIN.403 Forbidden: Insufficient role privileges or forced PIN rotation required.404 Not Found: Target user ID does not exist or is soft-deleted.409 Conflict: Username collision or violation of the Last-Admin Guard.422 Unprocessable Entity: Validation failure (field regex, length, or new PIN equal to current PIN).429 Too Many Requests: Rate limit exceeded or account temporarily locked out.500 Internal Server Error: Unexpected backend failure.
System and database diagnostic probe.
- Authorization: Public
- Responses:
200 OK:{ "status": "ok", "service": "TutorBox Backend", "database": "healthy" }
Authenticate a user with username and PIN to obtain a session token.
- Authorization: Public (Rate-limited)
- Request Body:
{ "username": "student1", "pin": "1234" } - Responses:
200 OK:{ "session_id": "9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d", "username": "student1", "status": "authenticated", "must_change_pin": false }401 Unauthorized: Invalid credentials.422 Unprocessable Entity: Username format or PIN digits invalid.429 Too Many Requests: Account locked due to repeated failed attempts.
Invalidate the current caller's active session.
- Authorization: Bearer Token
- Responses:
200 OK:{ "detail": "Logged out." }401 Unauthorized: Missing, invalid, or already revoked session token.
Public student self-registration.
- Authorization: Public (Rate-limited)
- Request Body:
{ "username": "maria_g", "pin": "5678" } - Responses:
201 Created:{ "username": "maria_g", "role": "student" }409 Conflict: Username is already taken.422 Unprocessable Entity: Validation constraint violated.429 Too Many Requests: Global signup rate limit exceeded.
Retrieve profile metadata for the authenticated user.
- Authorization: Bearer Token (Permitted during pending rotation)
- Responses:
200 OK:{ "user_id": 1, "username": "student1", "role": "student", "must_change_pin": false }401 Unauthorized: Invalid or expired session.
Change personal PIN. Clears forced rotation flag and invalidates the current session.
- Authorization: Bearer Token (Permitted during pending rotation)
- Request Body:
{ "current_pin": "1234", "new_pin": "9876" } - Responses:
200 OK:{ "detail": "Credentials updated. Please sign in again." }401 Unauthorized: Invalidcurrent_pin.422 Unprocessable Entity:new_pinequalscurrent_pinor fails validation.
Change personal username. Frees the old username and invalidates the current session.
- Authorization: Bearer Token (Gated by pending rotation)
- Request Body:
{ "current_pin": "1234", "new_username": "maria_new" } - Responses:
200 OK:{ "detail": "Credentials updated. Please sign in again." }401 Unauthorized: Invalidcurrent_pin.403 Forbidden: PIN rotation pending.409 Conflict:new_usernameis already taken.422 Unprocessable Entity:new_usernameequals current username.
List accounts in the school roster.
- Authorization: Teacher, Admin
- Query Parameters:
include_deleted(bool, optional, default:false): Iftrue, returns soft-deleted accounts.
- Responses:
200 OK(Active Roster):{ "users": [ { "id": 1, "username": "student1", "role": "student", "created_at": "2026-08-27 10:00:00", "must_change_pin": false } ] }200 OK(include_deleted=true):{ "users": [ { "id": 2, "role": "student", "former_username": "student2", "deleted_at": "2026-08-27 12:30:00" } ] }403 Forbidden: Caller is a student or has a pending PIN rotation.
Create a new user account.
- Authorization: Teacher (can create
student,teacher), Admin (can create any role) - Request Body:
{ "username": "carlos_p", "pin": "1234", "role": "student" } - Responses:
201 Created:{ "username": "carlos_p", "role": "student" }403 Forbidden: Teacher attempting to create an admin account.409 Conflict: Username already taken.
Issue a random 6-digit temporary PIN and require rotation.
- Authorization: Teacher (targets
student,teacher), Admin (targets any role) - Path Parameters:
user_id(integer, required): Target user ID.
- Responses:
200 OK:{ "username": "student1", "temporary_pin": "583921" }403 Forbidden: Teacher attempting to reset an admin PIN.404 Not Found: User not found or soft-deleted.
Soft-delete an account, anonymize username, deactivate sessions, and retain educational logs.
- Authorization: Teacher (targets
student,teacher), Admin (targets any role) - Path Parameters:
user_id(integer, required): Target user ID.
- Responses:
200 OK:{ "detail": "Account deleted." }403 Forbidden: Teacher attempting to delete an admin account.404 Not Found: Target user ID not found or already deleted.409 Conflict: Attempting to delete the last remaining admin.
Restore a soft-deleted account under a new username with a temporary PIN.
- Authorization: Teacher (targets
student,teacher), Admin (targets any role) - Path Parameters:
user_id(integer, required): Target user ID.
- Request Body:
{ "username": "student2_restored" } - Responses:
200 OK:{ "username": "student2_restored", "temporary_pin": "847291", "detail": "Account recovered. User must set a new PIN on next login." }403 Forbidden: Teacher attempting to recover an admin account.404 Not Found: Target is not soft-deleted or does not exist.409 Conflict: Target recovery username is already taken.
Read up to 500 append-only audit trail records.
- Authorization: Admin Only
- Responses:
200 OK:{ "logs": [ { "id": 10, "actor_user_id": 3, "action": "pin_reset", "target_user_id": 1, "created_at": "2026-08-27 14:15:00" }, { "id": 9, "actor_user_id": null, "action": "signup", "target_user_id": 5, "created_at": "2026-08-27 14:10:00" } ] }403 Forbidden: Caller is not an admin.
List all registered physical ESP32 clickers with current student pairing info.
- Authorization: Teacher, Admin
- Responses:
200 OK:{ "devices": [ { "device_id": "1", "assigned_user_id": 12, "assigned_username": "juan_p", "created_at": "2026-08-29 14:00:00" }, { "device_id": "ESP32_02", "assigned_user_id": null, "assigned_username": null, "created_at": "2026-08-29 14:05:00" } ] }403 Forbidden: Caller is a student or has a pending PIN rotation.
Register a new physical clicker identifier into the appliance fleet.
- Authorization: Teacher, Admin
- Request Body:
{ "device_id": "1" } - Responses:
201 Created:{ "device_id": "1", "assigned_user_id": null, "assigned_username": null, "created_at": "2026-08-29 14:00:00" }403 Forbidden: Caller is a student or has a pending PIN rotation.409 Conflict:device_idis already registered.422 Unprocessable Entity:device_idis empty or invalid format.
Link a physical clicker to an active student user account.
- Authorization: Teacher, Admin
- Path Parameters:
device_id(string, required): Unique device identifier.
- Request Body:
{ "user_id": 12 } - Responses:
200 OK:{ "device_id": "1", "assigned_user_id": 12, "assigned_username": "juan_p" }403 Forbidden: Caller is a student or has a pending PIN rotation.404 Not Found: Device or student account not found.422 Unprocessable Entity: Target user is not a student account.
Unlink a physical clicker from any student.
- Authorization: Teacher, Admin
- Path Parameters:
device_id(string, required): Unique device identifier.
- Responses:
200 OK:{ "detail": "Device unassigned successfully." }403 Forbidden: Caller is a student or has a pending PIN rotation.404 Not Found: Device not found.
Remove a physical clicker from the appliance fleet.
- Authorization: Teacher, Admin
- Path Parameters:
device_id(string, required): Unique device identifier.
- Responses:
200 OK:{ "detail": "Device removed from fleet." }403 Forbidden: Caller is a student or has a pending PIN rotation.404 Not Found: Device not found.
Retrieve the full primary mathematics curriculum taxonomy including topics, subconcepts, and diagnostic misconception codes.
- Authorization: Public
- Responses:
200 OK:[ { "name": "arithmetic", "subconcepts": [ { "name": "addition_subtraction", "misconceptions": [ "sign_error", "borrowing_error", "alignment_error", "added_instead_of_subtracted" ] }, { "name": "order_of_operations", "misconceptions": [ "left_to_right_precedence", "addition_before_multiplication", "ignored_parentheses" ] } ] }, { "name": "fractions", "subconcepts": [ { "name": "addition_subtraction", "misconceptions": [ "added_denominators", "ignored_common_denominator", "subtracted_denominators" ] } ] } ]
Retrieve the canonical versioned JSON Schema (Draft 2020-12) for diagnostic quiz questions. Used by frontend PWAs, offline clickers, and sync engines for dynamic schema discovery and client-side payload validation.
- Authorization: Public
- Responses:
200 OK:{ "$defs": { "DistractorDetail": { "properties": { "misconception": { "description": "Slug of the diagnosed misconception", "maxLength": 100, "minLength": 2, "title": "Misconception", "type": "string" }, "explanation": { "description": "Primary-school friendly explanation", "maxLength": 500, "minLength": 5, "title": "Explanation", "type": "string" } }, "required": ["misconception", "explanation"], "title": "DistractorDetail", "type": "object" } }, "properties": { "schema_version": { "default": "1.0.0", "description": "Contract schema version", "title": "Schema Version", "type": "string" }, "topic": { "maxLength": 64, "minLength": 2, "title": "Topic", "type": "string" }, "subconcept": { "maxLength": 64, "minLength": 2, "title": "Subconcept", "type": "string" }, "question_text": { "maxLength": 500, "minLength": 5, "title": "Question Text", "type": "string" }, "options": { "additionalProperties": { "type": "string" }, "title": "Options", "type": "object" }, "correct_option": { "enum": ["A", "B", "C", "D"], "title": "Correct Option", "type": "string" }, "distractors": { "additionalProperties": { "$ref": "#/$defs/DistractorDetail" }, "title": "Distractors", "type": "object" }, "id": { "maxLength": 64, "minLength": 1, "title": "Id", "type": "string" } }, "required": ["topic", "subconcept", "question_text", "options", "correct_option", "distractors", "id"], "title": "QuizQuestion", "type": "object", "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://tutorbox.local/schemas/v1/quiz_question.schema.json", "version": "1.0.0", "description": "Canonical versioned contract schema for TutorBox diagnostic multiple-choice quiz questions." }
Execute deterministic SymPy validation on an arbitrary multiple-choice diagnostic item without persisting it.
- Authorization: Public
- Request Body:
{ "question": { "id": "q_val_001", "topic": "pre_algebra", "subconcept": "one_step_equations", "question_text": "¿Cuál es el valor de x en la ecuación x + 4 = 10?", "options": { "A": "6", "B": "14", "C": "4", "D": "5" }, "correct_option": "A", "distractors": { "B": { "misconception": "sign_flip_error", "explanation": "Sumaste 4 a 10 en vez de restar 4." }, "C": { "misconception": "wrong_inverse_operation", "explanation": "Restaste 6 en vez de restar 4." }, "D": { "misconception": "table_lookup_error", "explanation": "Error menor al calcular 10 - 4." } } } } - Responses:
200 OK(Valid Math):{ "is_valid": true, "errors": [], "details": { "eval_mode": "equation", "target_solution": "6" } }200 OK(Invalid Math):{ "is_valid": false, "errors": [ "Correct option 'A' ('99') does not equal computed truth '6'" ], "details": { "eval_mode": "equation", "target_solution": "6" } }422 Unprocessable Entity: JSON schema violation (missing distractor, invalid option key).
Generate a new diagnostic question on-demand using the rejection and retry pipeline.
- Authorization: Teacher, Admin
- Request Body:
{ "topic": "arithmetic", "subconcept": "addition_subtraction", "save_to_bank": true } - Responses:
200 OK:{ "id": "q_gen_a1b2c3d4", "topic": "arithmetic", "subconcept": "addition_subtraction", "question_text": "¿Cuánto es 54 + 38?", "options": { "A": "82", "B": "16", "C": "92", "D": "812" }, "correct_option": "C", "distractors": { "A": { "misconception": "alignment_error", "explanation": "Olvidaste sumar la decena que llevabas." }, "B": { "misconception": "added_instead_of_subtracted", "explanation": "Restaste 54 - 38 en vez de sumarlos." }, "D": { "misconception": "borrowing_error", "explanation": "Escribiste el 12 completo al lado de la suma de decenas." } }, "source": "llm", "sympy_verified": true, "created_at": "2026-08-31 12:00:00" }403 Forbidden: Caller is a student or has pending PIN rotation.422 Unprocessable Entity: Invalid topic or subconcept slug.502 Bad Gateway: SLM generation failed all retry attempts.
Query and filter diagnostic questions from the question bank.
- Authorization: Teacher, Admin
- Query Parameters:
topic(string, optional): Filter by curriculum topic slug.subconcept(string, optional): Filter by subconcept slug.limit(integer, optional, default:50, min:1, max:200): Pagination limit.offset(integer, optional, default:0, min:0): Pagination offset.include_deleted(bool, optional, default:false): Include soft-deleted questions.
- Responses:
200 OK:{ "questions": [ { "id": "seed_arith_add_01", "topic": "arithmetic", "subconcept": "addition_subtraction", "question_text": "¿Cuánto es 54 + 38?", "options": { "A": "82", "B": "16", "C": "92", "D": "812" }, "correct_option": "C", "distractors": { "A": { "misconception": "alignment_error", "explanation": "Olvidaste sumar la decena que llevabas." }, "B": { "misconception": "added_instead_of_subtracted", "explanation": "Restaste 54 - 38 en vez de sumarlos." }, "D": { "misconception": "borrowing_error", "explanation": "Escribiste el 12 completo al lado de la suma de decenas." } }, "source": "seed", "sympy_verified": true, "created_at": "2026-08-31 10:00:00" } ], "total": 66 }403 Forbidden: Caller is a student or has pending PIN rotation.
Fetch a single diagnostic question by its unique identifier.
- Authorization: Teacher, Admin
- Path Parameters:
id(string, required): Unique question identifier.
- Responses:
200 OK: Question JSON model.404 Not Found: Question ID does not exist or is soft-deleted.
Manually create a teacher-authored diagnostic question with deterministic SymPy verification.
- Authorization: Teacher, Admin
- Request Body:
{ "id": "q_teacher_manual_01", "topic": "fractions", "subconcept": "addition_subtraction", "question_text": "¿Cuánto es 1/4 + 2/4?", "options": { "A": "3/4", "B": "3/8", "C": "2/8", "D": "1/2" }, "correct_option": "A", "distractors": { "B": { "misconception": "added_denominators", "explanation": "Sumaste los denominadores 4+4=8 en vez de mantener el común denominador." }, "C": { "misconception": "multiplied_only_numerators", "explanation": "Multiplicaste los numeradores y sumaste denominadores." }, "D": { "misconception": "subtracted_denominators", "explanation": "Confundiste 3/4 con 1/2." } } } - Responses:
201 Created: CreatedQuizQuestionResponse.403 Forbidden: Caller is a student or has pending PIN rotation.409 Conflict: Question with specifiedidalready exists in the bank.422 Unprocessable Content: Mathematical validation failure or schema/taxonomy error.
Soft-delete a diagnostic question from the question bank while retaining telemetry integrity.
- Authorization: Teacher, Admin
- Path Parameters:
id(string, required): Question identifier.
- Responses:
200 OK:{ "detail": "Question deleted." }403 Forbidden: Caller is a student or has pending PIN rotation.404 Not Found: Question not found or already deleted.
- Database Schema Reference: Explore the SQLite schema, table data dictionaries, and migration logs.
- Documentation Portal: Return to the documentation hub.
- Backend Developer Guide: Setup, local execution, and testing procedures.