EduTrack Online - REST API Documentation

Version: 1.1
Date: April 29, 2026
Target Audience: Next.js Frontend Developers
Backend: Django REST Framework


Table of Contents

  1. Overview
  2. Authentication
  3. Password Management
  4. Email Verification
  5. Educational Structure
  6. Teachers
  7. Students
  8. Courses
  9. Topics
  10. Lectures
  11. Videos
  12. Enrollments
  13. Purchases
  14. Balance & Revenue
  15. Video Watch Progress
  16. Curriculum
  17. Question Bank
  18. Study Materials
  19. Quizzes
  20. Homeworks
  21. Dashboard & Analytics - Teacher Dashboard - Course Analytics

1. Overview

Base URL

http://localhost:8000/api/

All endpoints are prefixed with /api/.

Authentication

The API uses JWT tokens stored in HTTP-Only cookies.

Inactive User Blocking:

CSRF / Cross-Origin Notes:

Common Errors

These error formats are shared across all endpoints:

Status Condition Response Body
401 Unauthorized Not authenticated (missing/invalid token) {"detail": "Authentication credentials were not provided."}
401 Unauthorized Token expired {"error": "Invalid or expired refresh token"}
403 Forbidden Insufficient permissions {"detail": "You do not have permission to perform this action."}
404 Not Found Object does not exist {"detail": "Not found."} or {"error": "String"}
500 Internal Server Error Unexpected server error {"detail": "Internal server error"}

Pagination

List endpoints that support pagination return this wrapper by default:

{ "count": "Integer", "next": "String (URL) | null", "previous": "String (URL) | null", "results": "Array[Object]" }

Bypass pagination by adding ?all=true to get all results in a single response:

{ "count": "Integer", "results": "Array[Object]" }

Query Parameters:

Parameter Type Description
page Integer Page number (default: 1)
page_size Integer Items per page (default: 50, max: 200)
all String Set to true to bypass pagination

2. Authentication Endpoints


POST /accounts/login/

Description: Authenticates a user with username and password. Returns user data in the JSON body and sets JWT tokens as HTTP-Only cookies.

Authentication: Public (no login required)

Content-Type: application/json

Request Body:

{ "username": "String (Required)", "password": "String (Required)" }

Success Response — 200 OK:

{ "role": "String (siteowner|teacher|assistant|student)", "name": "String", "is_active": "Boolean" }

Note: access_token and refresh_token are set as HTTP-Only cookies and are NOT in the response body.

Error Responses:

Status Condition Response Body
400 Missing username or password {"error": "String"}
401 Invalid credentials {"error": "No active account found with the given credentials"}
401 Account inactive (teacher/assistant) {"error": "Your account is inactive. Please contact an administrator."}
401 Assistant's teacher inactive {"error": "Your assigned teacher's account is inactive..."}

Business Rules:


POST /accounts/logout/

Description: Blacklists the refresh token and clears both JWT cookies from the browser.

Authentication: Any authenticated user

Content-Type: application/json

Request Body: None

Success Response — 200 OK:

{ "message": "Successfully logged out" }

Error Responses:

Status Condition Response Body
401 Not authenticated {"detail": "Authentication credentials were not provided."}

Business Rules:


POST /accounts/token/refresh/

Description: Rotates the refresh token and issues new access and refresh tokens. Reads the refresh token from the cookie.

Authentication: Public (reads token from cookie)

Content-Type: application/json

Request Body: None

Success Response — 200 OK:

{ "role": "String (siteowner|teacher|assistant|student)", "name": "String", "is_active": "Boolean" }

Note: New access_token and refresh_token cookies are set.

Error Responses:

Status Condition Response Body
401 No refresh token in cookie {"error": "Refresh token not found"}
401 Invalid/expired refresh token {"error": "Invalid or expired refresh token"}
401 User not found {"error": "User not found"}
401 Inactive teacher/assistant {"error": "Your account is inactive..."}
401 Assistant's teacher inactive {"error": "Your assigned teacher's account is inactive..."}

Business Rules:


GET /accounts/me/

Description: Returns the current user's role, name, and active status. Used by the frontend to restore session state on page refresh.

Authentication: Any authenticated user

Content-Type: application/json

Request Body: None

Success Response — 200 OK:

{ "role": "String (siteowner|teacher|assistant|student)", "name": "String", "is_active": "Boolean" }

Error Responses:

Status Condition Response Body
401 Not authenticated {"detail": "Authentication credentials were not provided."}

3. Password Management


POST /accounts/forgot-password/

Description: Requests a password reset OTP. Always returns success (even if email doesn't exist) to prevent email enumeration attacks.

Authentication: Public

Content-Type: application/json

Request Body:

{ "email": "String (Required) — Email address associated with the account" }

Success Response — 200 OK:

{ "message": "If an account exists with this email, you will receive a reset code." }

Error Responses:

Status Condition Response Body
400 Missing email {"error": "Email is required"}

Business Rules:


POST /accounts/verify-otp/

Description: Verifies the OTP code sent to the user's email. Returns a reset_token that should be used in the subsequent password reset call.

Authentication: Public

Content-Type: application/json

Request Body:

{ "email": "String (Required)", "otp": "String (Required) — 6-digit code" }

Success Response — 200 OK:

{ "valid": true, "reset_token": "String — 32-character hex token", "message": "OTP verified successfully" }

Error Responses:

Status Condition Response Body
400 Missing email or OTP {"error": "Email and OTP are required"}
400 OTP expired {"valid": false, "error": "OTP has expired"}
400 Invalid OTP {"valid": false, "error": "Invalid OTP"}

POST /accounts/reset-password/

Description: Resets the user's password using the verified OTP and optional reset token. Invalidates all existing refresh tokens, forcing re-login on all devices.

Authentication: Public

Content-Type: application/json

Request Body:

{ "email": "String (Required)", "otp": "String (Required) — 6-digit code", "reset_token": "String (Optional) — From verify-otp response", "new_password": "String (Required) — Minimum 8 characters" }

Success Response — 200 OK:

{ "message": "Password reset successfully. Please log in again." }

Error Responses:

Status Condition Response Body
400 Missing required fields {"error": "Email, OTP, and new password are required"}
400 Password too short {"error": "Password must be at least 8 characters"}
400 OTP expired {"error": "OTP has expired"}
400 Invalid reset token {"error": "Invalid reset token"}
400 Invalid OTP {"error": "Invalid OTP"}

Business Rules:


POST /accounts/change-password/

Description: Allows an authenticated user to change their password. Requires the current password for verification. Invalidates all refresh tokens on success.

Authentication: Any authenticated user

Content-Type: application/json

Request Body:

{ "old_password": "String (Required) — Current password", "new_password": "String (Required) — Minimum 8 characters", "new_password_confirm": "String (Required) — Must match new_password" }

Success Response — 200 OK:

{ "message": "Password changed successfully. Please log in again." }

Error Responses:

Status Condition Response Body
400 Missing fields {"error": "All password fields are required"}
400 Passwords don't match {"error": "New passwords do not match"}
400 Password too short {"error": "Password must be at least 8 characters"}
400 Wrong old password {"error": "Current password is incorrect"}
401 Not authenticated {"detail": "Authentication credentials were not provided."}

Business Rules:


4. Email Verification


POST /accounts/verify-email/request/

Description: Requests an email verification OTP.

Authentication: Public

Content-Type: application/json

Request Body:

{ "email": "String (Required) — Email address to verify" }

Success Response — 200 OK:

{ "message": "Verification code sent to your email." }

Error Responses:

Status Condition Response Body
400 Missing email {"error": "Email is required"}

Business Rules:


POST /accounts/verify-email/confirm/

Description: Confirms email ownership by verifying the OTP code.

Authentication: Public

Content-Type: application/json

Request Body:

{ "email": "String (Required)", "otp": "String (Required) — 6-digit code" }

Success Response — 200 OK:

{ "verified": true, "message": "Email verified successfully" }

Error Responses:

Status Condition Response Body
400 Missing fields {"error": "Email and OTP are required"}
400 OTP expired {"verified": false, "error": "OTP has expired"}
400 Invalid OTP {"verified": false, "error": "Invalid OTP"}

5. Educational Structure

These endpoints manage the foundational data used throughout the platform: grades, school types, divisions, and subjects.

All educational structure endpoints are public read-only. They are configured via Django Admin and should not be modified via the API.


Grades

GET /accounts/grades/

Description: List all grades. Supports search and pagination bypass.

Authentication: Public

Query Parameters:

Parameter Type Description
search String Filter by name (case-insensitive)
all String Set to true to bypass pagination
page Integer Page number
page_size Integer Items per page

Success Response — 200 OK:

{ "count": "Integer", "next": "String (URL) | null", "previous": "String (URL) | null", "results": [ { "id": "Integer", "name": "String" } ] }

GET /accounts/grades//

Description: Retrieve a single grade.

Authentication: Public

Success Response — 200 OK:

{ "id": "Integer", "name": "String" }

Error Responses:

Status Condition Response Body
404 Grade not found {"error": "Grade not found"}

School Types

GET /accounts/school-types/

Description: List all school types. Same pattern as grades.

Authentication: Public

Query Parameters: Same as grades (search, all, page, page_size)

Success Response — 200 OK:

{ "count": "Integer", "next": "String (URL) | null", "previous": "String (URL) | null", "results": [ { "id": "Integer", "name": "String" } ] }

GET /accounts/school-types//

Authentication: Public

Success Response — 200 OK:

{ "id": "Integer", "name": "String" }

Divisions

GET /accounts/divisions/

Description: List all divisions with nested grades and school types.

Authentication: Public

Query Parameters:

Parameter Type Description
school_type Integer Filter by school type ID
grade Integer Filter by grade ID
search String Filter by name
all String Set to true to bypass pagination

Success Response — 200 OK:

{ "count": "Integer", "next": "String (URL) | null", "previous": "String (URL) | null", "results": [ { "id": "Integer", "name": "String", "grades": [ { "id": "Integer", "name": "String" } ], "school_types": [ { "id": "Integer", "name": "String" } ] } ] }

GET /accounts/divisions//

Authentication: Public

Success Response — 200 OK: Same structure as list item.


Subjects

GET /accounts/subjects/

Description: List all subjects with nested grades, divisions, and school types. This is the public catalog that unauthenticated users can browse.

Authentication: Public

Query Parameters:

Parameter Type Description
school_type Integer Filter by school type ID
grade Integer Filter by grade ID
division Integer Filter by division ID
search String Filter by name
all String Set to true to bypass pagination

Success Response — 200 OK:

{ "count": "Integer", "next": "String (URL) | null", "previous": "String (URL) | null", "results": [ { "id": "Integer", "name": "String", "grades": "Array[Integer] — Grade IDs", "grades_detail": [ { "id": "Integer", "name": "String" } ], "divisions": "Array[Integer] — Division IDs", "divisions_detail": [ { "id": "Integer", "name": "String", "grades": [ { "id": "Integer", "name": "String" } ], "school_types": [ { "id": "Integer", "name": "String" } ] } ], "school_types": "Array[Integer] — School type IDs", "school_types_detail": [ { "id": "Integer", "name": "String" } ], "grade_ids": "Array[Integer]", "division_ids": "Array[Integer]", "school_type_ids": "Array[Integer]" } ] }

GET /accounts/subjects//

Authentication: Public

Success Response — 200 OK: Same structure as list item.


6. Teachers

Public Teacher Endpoints

These endpoints are accessible without authentication for browsing teachers.


GET /accounts/public/teachers/

Description: List all active teachers. Supports filtering by subject and grade.

Authentication: Public

Query Parameters:

Parameter Type Description
subject Integer Filter by subject ID
grades Integer Filter by grade ID
search String Search by name or subject name
all String Set to true to bypass pagination

Success Response — 200 OK:

{ "count": "Integer", "next": "String (URL) | null", "previous": "String (URL) | null", "results": [ { "id": "Integer", "name": "String", "profile_picture": "String (URL) | null", "subject_detail": { "id": "Integer", "name": "String" }, "grades_detail": [ { "id": "Integer", "name": "String" } ] } ] }

GET /accounts/public/teachers//

Description: Get detailed public profile for a single teacher.

Authentication: Public

Success Response — 200 OK:

{ "id": "Integer", "name": "String", "profile_picture": "String (URL) | null", "biography": "String | null", "facebook": "String | null", "subject_detail": { "id": "Integer", "name": "String" }, "grades_detail": [ { "id": "Integer", "name": "String" } ] }

Error Responses:

Status Condition Response Body
404 Teacher not found {"error": "Teacher not found"}

Admin Teacher Endpoints

These endpoints require authentication and are used by SiteOwner to manage teachers.


GET /accounts/teachers/

Description: List all teachers with full details.

Authentication: SiteOwner

Query Parameters:

Parameter Type Description
is_active Boolean Filter by active status
subject Integer Filter by subject ID
grades Integer Filter by grade ID
search String Search by name
all String Set to true to bypass pagination

Success Response — 200 OK:

{ "count": "Integer", "next": "String (URL) | null", "previous": "String (URL) | null", "results": [ { "id": "Integer", "user_id": "Integer", "username": "String", "name": "String", "phone": "String", "secondary_phone": "String | null", "gmail": "String", "gender": "String (male|female) | null", "profile_picture": "String (URL) | null", "biography": "String | null", "facebook": "String | null", "subject": "Integer — Write-only (Subject ID)", "subject_detail": { "id": "Integer", "name": "String", "grades": "Array[Integer]", "grades_detail": [ { "id": "Integer", "name": "String" } ], "divisions": "Array[Integer]", "divisions_detail": [ { "id": "Integer", "name": "String", "grades": [ { "id": "Integer", "name": "String" } ], "school_types": [ { "id": "Integer", "name": "String" } ] } ], "school_types": "Array[Integer]", "school_types_detail": [ { "id": "Integer", "name": "String" } ], "grade_ids": "Array[Integer]", "division_ids": "Array[Integer]", "school_type_ids": "Array[Integer]" }, "grades": "Array[Integer] — Write-only", "grades_detail": [ { "id": "Integer", "name": "String" } ], "video_security": "Integer | null — Write-only", "video_security_detail": { "id": "Integer", "name": "String", "price_per_student": "Decimal" }, "is_active": "Boolean", "created_at": "DateTime (ISO 8601)", "subject_id": "Integer | null", "grade_ids": "Array[Integer]", "video_security_id": "Integer | null" } ] }

POST /accounts/teachers/

Description: Create a new teacher user and profile atomically. Supports profile picture upload via multipart/form-data.

Authentication: SiteOwner

Content-Type: multipart/form-data

Request Body:

{ "username": "String (Required)", "password": "String (Required) — Minimum 8 characters", "name": "String (Required)", "phone": "String (Required) — Egyptian format: 010/011/012/015 + 8 digits", "secondary_phone": "String (Optional) — Egyptian format", "gmail": "String (Required) — Must be globally unique", "gender": "String (Optional) — male|female", "profile_picture": "File (Optional) — image/jpeg|image/png|image/webp", "biography": "String (Optional)", "facebook": "String (Optional)", "subject": "Integer (Required) — Subject ID", "grades": "Array[Integer] (Required) — Grade IDs", "video_security": "Integer (Optional) — VideoSecurity ID" }

Success Response — 201 Created: Same structure as GET list item.

Error Responses:

Status Condition Response Body
400 Username exists {"username": ["Username already exists."]}
400 Email exists {"gmail": ["This email is already associated with an account."]}
400 Invalid phone {"phone": ["Phone number must be exactly 11 digits..."]}
400 Weak password {"password": ["Password validation error"]}
403 Not authorized {"detail": "You do not have permission..."}

Business Rules:


GET /accounts/teachers//

Authentication: SiteOwner

Success Response — 200 OK: Same structure as list item.


PUT/PATCH /accounts/teachers//

Authentication: SiteOwner

Content-Type: multipart/form-data

Request Body: Same as POST (all fields optional for PATCH).

Success Response — 200 OK: Same structure as list item.

Error Responses:

Status Condition Response Body
400 Email exists (other user) {"gmail": ["This email is already associated with an account."]}
403 Not authorized {"detail": "You do not have permission..."}
404 Teacher not found {"detail": "Not found."}

Teacher Assistant Management

These endpoints allow a teacher to manage their own assistants.


GET /accounts/teacher/assistants/

Description: List all assistants belonging to the authenticated teacher.

Authentication: Teacher

Success Response — 200 OK:

{ "count": "Integer", "next": "String (URL) | null", "previous": "String (URL) | null", "results": [ { "id": "Integer", "user_id": "Integer", "username": "String", "name": "String", "phone": "String", "gmail": "String", "gender": "String (male|female) | null", "profile_picture": "String (URL) | null", "is_active": "Boolean", "created_at": "DateTime (ISO 8601)", "teacher": "Integer — Teacher ID" } ] }

POST /accounts/teacher/assistants/

Description: Create a new assistant for the authenticated teacher.

Authentication: Teacher

Content-Type: multipart/form-data

Request Body:

{ "username": "String (Required)", "password": "String (Required) — Minimum 8 characters", "name": "String (Required)", "phone": "String (Required)", "gmail": "String (Required) — Must be globally unique", "gender": "String (Optional) — male|female", "profile_picture": "File (Optional) — image/jpeg|image/png|image/webp" }

GET /accounts/teacher/assistant//

Authentication: Teacher (must own the assistant)


PUT/PATCH /accounts/teacher/assistant//

Authentication: Teacher (must own the assistant)


DELETE /accounts/teacher/assistant//

Authentication: Teacher (must own the assistant)

Success Response — 204 No Content


7. Students

Student Registration


POST /accounts/student/register/

Description: Register a new student account. Creates both a User and StudentProfile atomically.

Authentication: Public

Content-Type: multipart/form-data

Request Body:

{ "username": "String (Required) — Unique, case-insensitive", "password": "String (Required) — Minimum 8 characters", "password_confirm": "String (Required) — Must match password", "name_ar": "String (Required) — Arabic name", "name_en": "String (Required) — English name", "phone_number": "String (Required) — Egyptian format", "father_number": "String (Optional) — Required for school students", "mother_number": "String (Optional) — Required for school students", "father_job": "String (Required)", "educational_state": "String (Required) — school", "school_type": "Integer (Optional) — Required for school students", "grade": "Integer (Optional) — Required for school students", "division": "Integer (Optional) — Required for school students", "school_name": "String (Optional) — Required for school students", "national_id": "String (Required) — 14 digits", "birth_date": "Date (YYYY-MM-DD) (Required)", "gender": "String (Required) — male|female", "gmail": "String (Required) — Must be globally unique", "facebook_link": "String (Optional)", "governorate": "Integer (Required)", "area": "Integer (Required) — Must belong to the selected governorate" }

Success Response — 201 Created:

{ "id": "Integer", "user_id": "Integer", "username": "String", "student_code": "String — Auto-generated 7-digit code", "name_ar": "String", "name_en": "String", "phone_number": "String", "father_number": "String | null", "mother_number": "String | null", "father_job": "String", "educational_state": "String (school)", "school_type": "Integer | null", "grade": "Integer | null", "division": "Integer | null", "school_name": "String | null", "national_id": "String", "birth_date": "Date (YYYY-MM-DD)", "gender": "String (male|female)", "gmail": "String", "facebook_link": "String | null", "governorate": "Integer", "area": "Integer", "status": "String — active", "is_active": "Boolean — true", "created_at": "DateTime (ISO 8601)", "updated_at": "DateTime (ISO 8601)" }

Error Responses:

Status Condition Response Body
400 Username exists {"username": ["Username already exists."]}
400 Email exists {"gmail": ["This email is already associated with an account."]}
400 Passwords don't match {"password_confirm": ["Passwords do not match."]}
400 Missing school fields {"educational_state": ["School type and grade are required..."]}
400 Missing division {"division": ["Division is required for school students."]}
400 Missing school name {"school_name": ["School name is required for school students."]}
400 Missing parent phones {"father_number": ["Father phone number is required..."]}
400 Area doesn't match governorate {"area": ["The selected area does not belong to the governorate..."]}

Business Rules:


Student Profile Management


GET /accounts/profile/me/

Description: Get the current student's own profile.

Authentication: Student

Success Response — 200 OK:

{ "id": "Integer", "user_id": "Integer", "username": "String", "student_code": "String", "name_ar": "String", "name_en": "String", "phone_number": "String", "father_number": "String | null", "mother_number": "String | null", "father_job": "String", "educational_state": "String (school)", "school_type": "Integer | null", "grade": "Integer | null", "division": "Integer | null", "school_name": "String | null", "national_id": "String", "birth_date": "Date (YYYY-MM-DD)", "gender": "String (male|female)", "gmail": "String", "facebook_link": "String | null", "governorate": "Integer", "area": "Integer", "status": "String", "is_active": "Boolean", "created_at": "DateTime (ISO 8601)", "updated_at": "DateTime (ISO 8601)" }

PUT/PATCH /accounts/profile/me/

Description: Update the current student's own profile.

Authentication: Student

Content-Type: multipart/form-data or application/json

Request Body: All fields from the profile are optional for PATCH. Same validation rules as registration apply.

Success Response — 200 OK: Same structure as GET.

Error Responses:

Status Condition Response Body
400 Email exists (other user) {"gmail": ["This email is already associated with an account."]}
400 Area/governorate mismatch {"area": ["The selected area does not belong..."]}
400 Missing required fields for state Same as registration validation errors

Admin Student Management


GET /accounts/students/

Description: List all students.

Authentication: SiteOwner

Query Parameters:

Parameter Type Description
status String Filter by status
grade Integer Filter by grade ID
school_type Integer Filter by school type ID
search String Search by name, student code, or national ID
all String Set to true to bypass pagination

Success Response — 200 OK: Array of student profiles (same structure as GET /accounts/profile/me/).


GET /accounts/students//

Authentication: SiteOwner

Success Response — 200 OK: Single student profile.


PATCH /accounts/students//

Authentication: SiteOwner

Content-Type: application/json

Request Body: Same as student update serializer.


PATCH /accounts/students//status/

Description: Update a student's status and active flag.

Authentication: SiteOwner

Content-Type: application/json

Request Body:

{ "status": "String (Optional) — verified|pending|declined|suspended_temporary|suspended_permanent", "is_active": "Boolean (Optional)" }

Success Response — 200 OK:

{ "status": "String", "is_active": "Boolean" }

8. Courses

Course Discovery


GET /courses/by-subject/<subject_id>/

Description: List courses for a specific subject. Auto-filtered by the authenticated student's grade, division, and school type. Only active courses are returned.

Authentication: Any authenticated user

Query Parameters:

Parameter Type Description
teacher Integer Filter by teacher ID
search String Search by name, description, or teacher name
ordering String created_at, name, -created_at (default: -created_at)

Success Response — 200 OK:

{ "count": "Integer", "next": "String (URL) | null", "previous": "String (URL) | null", "results": [ { "id": "Integer", "name": "String", "teacher": "Integer", "teacher_name": "String", "grade": "Integer", "grade_name": "String", "subject": "Integer", "subject_name": "String", "description": "String | null", "cover_picture": "String (URL) | null", "is_active": "Boolean", "topic_count": "Integer", "created_at": "DateTime (ISO 8601)" } ] }

Error Responses:

Status Condition Response Body
401 Not authenticated {"detail": "Authentication credentials were not provided."}

Business Rules:


Course Management


GET /courses/

Description: List all courses.

Authentication: Any authenticated user

Query Parameters:

Parameter Type Description
teacher Integer Filter by teacher ID
grade Integer Filter by grade ID
subject Integer Filter by subject ID
is_active Boolean Filter by active status
search String Search by name, description, teacher name
ordering String created_at, name, updated_at

Success Response — 200 OK: Array of courses (same structure as GET /courses/by-subject/<subject_id>/).


POST /courses/

Description: Create a new course.

Authentication: SiteOwner only

Content-Type: multipart/form-data

Request Body:

{ "name": "String (Required)", "teacher": "Integer (Required) — Teacher ID", "grade": "Integer (Required) — Grade ID", "description": "String (Optional)", "cover_picture": "File (Optional) — image/jpeg|image/png|image/webp", "is_active": "Boolean (Optional) — Default: true" }

Success Response — 201 Created:

{ "id": "Integer", "name": "String", "teacher": "Integer", "teacher_id": "Integer", "teacher_name": "String", "grade": "Integer", "grade_name": "String", "subject": "Integer", "subject_name": "String", "description": "String | null", "cover_picture": "String (URL) | null", "is_active": "Boolean", "topics": "Array[Object]", "topic_count": "Integer", "total_lectures": "Integer", "total_videos": "Integer", "created_at": "DateTime (ISO 8601)", "updated_at": "DateTime (ISO 8601)" }

Error Responses:

Status Condition Response Body
400 Teacher doesn't teach grade {"grade": ["Teacher X does not teach grade Y."]}
400 Duplicate course {"non_field_errors": ["A course with this Teacher, Grade, and Name already exists."]}
403 Not site owner {"detail": "You do not have permission..."}

Business Rules:


GET /courses//

Authentication: Any authenticated user

Success Response — 200 OK: Full course with nested topics.

{ "id": "Integer", "name": "String", "teacher": "Integer", "teacher_id": "Integer", "teacher_name": "String", "grade": "Integer", "grade_name": "String", "subject": "Integer", "subject_name": "String", "description": "String | null", "cover_picture": "String (URL) | null", "is_active": "Boolean", "topics": [ { "id": "Integer", "course": "Integer", "course_name": "String", "name": "String", "description": "String | null", "order": "Integer", "is_active": "Boolean", "lecture_count": "Integer", "created_at": "DateTime (ISO 8601)", "updated_at": "DateTime (ISO 8601)" } ], "topic_count": "Integer", "total_lectures": "Integer", "total_videos": "Integer", "created_at": "DateTime (ISO 8601)", "updated_at": "DateTime (ISO 8601)" }

PUT/PATCH /courses//

Authentication: SiteOwner only

Content-Type: multipart/form-data

Request Body: Same as POST (all fields optional for PATCH).

Error Responses:

Status Condition Response Body
400 Changing teacher {"teacher": ["Cannot change the teacher of an existing course."]}
400 Changing grade {"grade": ["Cannot change the grade of an existing course."]}

Business Rules:


DELETE /courses//

Authentication: SiteOwner only

Error Responses:

Status Condition Response Body
400 Students enrolled {"detail": "Cannot delete a course that has students enrolled in it."}

Course Preview


GET /courses//preview/

Description: Preview a course before enrolling. Shows topics and lectures with prices, but NO video URLs. Available to any authenticated user.

Authentication: Any authenticated user

Success Response — 200 OK:

{ "id": "Integer", "name": "String", "description": "String | null", "cover_picture": "String (URL) | null", "teacher": { "id": "Integer", "name": "String", "profile_picture": "String (URL) | null" }, "grade": { "id": "Integer", "name": "String" }, "subject": { "id": "Integer", "name": "String" }, "topic_count": "Integer", "total_lectures": "Integer", "topics": [ { "id": "Integer", "name": "String", "description": "String | null", "order": "Integer", "lecture_count": "Integer", "lectures": [ { "id": "Integer", "name": "String", "description": "String | null", "price": "String — Decimal as string", "final_price": "String — Decimal as string", "discount": "String — Decimal as string", "available_days": "Integer", "order": "Integer", "video_count": "Integer" } ] } ], "created_at": "DateTime (ISO 8601)" }

Error Responses:

Status Condition Response Body
401 Not authenticated {"detail": "Authentication credentials were not provided."}
404 Course not found {"error": "Course not found"}

Teacher Courses


GET /courses/my_courses/

Description: List courses taught by the authenticated teacher.

Authentication: Teacher

Success Response — 200 OK: Array of courses (same structure as course list).

Error Responses:

Status Condition Response Body
403 Not a teacher {"detail": "You do not have permission..."}
404 Teacher profile not found {"detail": "Teacher profile not found."}

Student Enrolled Courses


GET /courses/enrolled/

Description: Get all courses the student is enrolled in (approved status only).

Authentication: Student

Success Response — 200 OK:

{ "count": "Integer", "next": "String (URL) | null", "previous": "String (URL) | null", "results": [ { "id": "Integer", "name": "String", "teacher": { "id": "Integer", "name": "String" }, "grade": { "id": "Integer", "name": "String" }, "subject": { "id": "Integer", "name": "String" }, "description": "String | null", "cover_picture": "String (URL) | null", "enrolled_at": "DateTime (ISO 8601)", "status": "String — approved" } ] }

Course Topics


GET /courses//topics/

Description: Get all topics for a specific course.

Authentication: Any authenticated user

Success Response — 200 OK:

[ { "id": "Integer", "course": "Integer", "course_name": "String", "name": "String", "description": "String | null", "order": "Integer", "is_active": "Boolean", "lecture_count": "Integer", "created_at": "DateTime (ISO 8601)", "updated_at": "DateTime (ISO 8601)" } ]

Business Rules:


Course Analytics


GET /courses//analytics/

Description: Get detailed analytics for a course. Includes enrollment stats, purchase stats, revenue, and per-topic/per-lecture breakdowns.

Authentication: Teacher (course owner), Assistant (teacher's assistant), SiteOwner

Success Response — 200 OK:

{ "course": { "id": "Integer", "name": "String", "teacher_name": "String", "total_students": "Integer", "pending_enrollments": "Integer", "approved_enrollments": "Integer", "rejected_enrollments": "Integer" }, "revenue": { "total_revenue": "Decimal", "total_purchases": "Integer" }, "topics": [ { "id": "Integer", "name": "String", "lecture_count": "Integer", "total_purchases": "Integer", "topic_revenue": "Decimal" } ], "lectures": [ { "id": "Integer", "name": "String", "purchase_count": "Integer", "revenue": "Decimal" } ] }

Error Responses:

Status Condition Response Body
403 Not course owner/assistant {"error": "You can only view analytics for your own courses"}
404 Course not found {"error": "Course not found"}

Course Balance


GET /courses//balance/

Description: Get the course balance for the authenticated student. Shows how much money the student has deposited for this course and how much they've spent.

Authentication: Student

Success Response — 200 OK:

{ "id": "Integer", "student": "Integer", "course": "Integer", "balance": "Decimal", "total_deposited": "Decimal", "total_spent": "Decimal", "created_at": "DateTime (ISO 8601)", "updated_at": "DateTime (ISO 8601)" }

Error Responses:

Status Condition Response Body
400 No student profile {"error": "Student profile not found"}
404 Course not found {"error": "Course not found"}

Student Course Lectures


GET /courses//lectures/

Description: Get lectures for a course the student is enrolled in. Shows purchase status for each lecture.

Authentication: Student (must be approved enrolled)

Success Response — 200 OK:

{ "course": { "id": "Integer", "name": "String" }, "topics": [ { "id": "Integer", "name": "String", "order": "Integer", "lectures": [ { "id": "Integer", "name": "String", "description": "String | null", "price": "String — Decimal as string", "final_price": "String — Decimal as string", "discount": "String — Decimal as string", "available_days": "Integer", "order": "Integer", "video_count": "Integer", "is_purchased": "Boolean", "purchase": { "id": "Integer", "purchased_at": "DateTime (ISO 8601)", "expires_at": "DateTime (ISO 8601)", "effective_expiry": "DateTime (ISO 8601)", "is_expired": "Boolean", "extra_days": "Integer", "amount_paid": "String — Decimal as string" } } ] } ] }

Note: The purchase object is only included if is_purchased is true.

Error Responses:

Status Condition Response Body
400 No student profile {"error": "Student profile not found"}
403 Not enrolled {"error": "You are not enrolled in this course"}
404 Course not found {"error": "Course not found"}

9. Topics


GET /courses/topics/

Authentication: Any authenticated user

Query Parameters:

Parameter Type Description
course Integer Filter by course ID
is_active Boolean Filter by active status
search String Search by name, description, or course name
ordering String order, created_at, name

Success Response — 200 OK: Array of topics.

{ "count": "Integer", "next": "String (URL) | null", "previous": "String (URL) | null", "results": [ { "id": "Integer", "course": "Integer", "course_name": "String", "name": "String", "description": "String | null", "order": "Integer", "is_active": "Boolean", "lecture_count": "Integer", "created_at": "DateTime (ISO 8601)", "updated_at": "DateTime (ISO 8601)" } ] }

POST /courses/topics/

Authentication: SiteOwner, Teacher, Assistant

Content-Type: application/json

Request Body:

{ "course": "Integer (Required) — Course ID", "name": "String (Required)", "description": "String (Optional)", "order": "Integer (Optional) — Default: 0", "is_active": "Boolean (Optional) — Default: true" }

GET /courses/topics//

Authentication: Any authenticated user

Success Response — 200 OK: Single topic with nested lectures.

{ "id": "Integer", "course": "Integer", "course_name": "String", "name": "String", "description": "String | null", "order": "Integer", "is_active": "Boolean", "lectures": [ { "id": "Integer", "topic": "Integer", "topic_name": "String", "course_name": "String", "teacher_name": "String", "name": "String", "description": "String | null", "price": "Decimal", "discount": "Decimal", "final_price": "Decimal", "formatted_price": "String", "available_days": "Integer", "is_visible": "Boolean", "picture": "String (URL) | null", "order": "Integer", "videos": "Array[Object]", "video_count": "Integer", "created_at": "DateTime (ISO 8601)", "updated_at": "DateTime (ISO 8601)" } ], "created_at": "DateTime (ISO 8601)", "updated_at": "DateTime (ISO 8601)" }

PUT/PATCH /courses/topics//

Authentication: SiteOwner, Teacher, Assistant


DELETE /courses/topics//

Authentication: SiteOwner, Teacher, Assistant


GET /courses/topics//lectures/

Description: Get all lectures for a specific topic.

Authentication: Any authenticated user

Success Response — 200 OK: Array of lectures.

[ { "id": "Integer", "topic": "Integer", "topic_name": "String", "name": "String", "description": "String | null", "price": "Decimal", "discount": "Decimal", "final_price": "Decimal", "formatted_price": "String", "available_days": "Integer", "is_visible": "Boolean", "picture": "String (URL) | null", "order": "Integer", "video_count": "Integer", "created_at": "DateTime (ISO 8601)", "updated_at": "DateTime (ISO 8601)" } ]

Business Rules:


10. Lectures


GET /courses/lectures/

Authentication: Any authenticated user

Query Parameters:

Parameter Type Description
topic Integer Filter by topic ID
is_visible Boolean Filter by visibility
search String Search by name, description, or topic name
ordering String order, price, created_at, name

Success Response — 200 OK: Array of lectures.


POST /courses/lectures/

Authentication: SiteOwner, Teacher, Assistant

Content-Type: multipart/form-data

Request Body:

{ "topic": "Integer (Required) — Topic ID", "name": "String (Required)", "description": "String (Optional)", "price": "Decimal (Required)", "discount": "Decimal (Optional) — Default: 0.00", "available_days": "Integer (Required) — Days of access after purchase (1-365)", "is_visible": "Boolean (Optional) — Default: true", "picture": "File (Optional) — image/jpeg|image/png|image/webp", "order": "Integer (Optional) — Default: 0" }

Success Response — 201 Created: Single lecture.


GET /courses/lectures//

Authentication: Any authenticated user

Success Response — 200 OK: Single lecture with nested videos.

{ "id": "Integer", "topic": "Integer", "topic_name": "String", "course_name": "String", "teacher_name": "String", "name": "String", "description": "String | null", "price": "Decimal", "discount": "Decimal", "final_price": "Decimal", "formatted_price": "String", "available_days": "Integer", "is_visible": "Boolean", "picture": "String (URL) | null", "order": "Integer", "videos": [ { "id": "Integer", "lecture": "Integer", "name": "String", "platform": "String (youtube|bunnystream|vdocipher)", "platform_display": "String", "video_url": "String", "order": "Integer", "duration_minutes": "Integer | null", "duration_display": "String | null", "is_active": "Boolean", "created_at": "DateTime (ISO 8601)", "updated_at": "DateTime (ISO 8601)" } ], "video_count": "Integer", "created_at": "DateTime (ISO 8601)", "updated_at": "DateTime (ISO 8601)" }

PUT/PATCH /courses/lectures//

Authentication: SiteOwner, Teacher, Assistant


DELETE /courses/lectures//

Authentication: SiteOwner, Teacher, Assistant


GET /courses/lectures//videos/

Description: Get all videos for a specific lecture.

Authentication: Any authenticated user

Success Response — 200 OK: Array of videos.

[ { "id": "Integer", "lecture": "Integer", "name": "String", "platform": "String (youtube|bunnystream|vdocipher)", "platform_display": "String", "video_url": "String", "order": "Integer", "duration_minutes": "Integer | null", "duration_display": "String | null", "is_active": "Boolean", "created_at": "DateTime (ISO 8601)", "updated_at": "DateTime (ISO 8601)" } ]

Business Rules:


11. Videos


GET /courses/videos/

Authentication: Any authenticated user

Query Parameters:

Parameter Type Description
lecture Integer Filter by lecture ID
platform String Filter by platform (youtube, bunnystream, vdocipher)
is_active Boolean Filter by active status
search String Search by name or video URL
ordering String order, created_at, duration_minutes

Success Response — 200 OK: Array of videos (same structure as lecture videos).


POST /courses/videos/

Authentication: SiteOwner, Teacher, Assistant

Content-Type: application/json

Request Body:

{ "lecture": "Integer (Required) — Lecture ID", "name": "String (Required)", "platform": "String (Required) — youtube|bunnystream|vdocipher", "video_url": "String (Required) — Must match platform format", "order": "Integer (Optional) — Default: 0", "duration_minutes": "Integer (Optional)", "is_active": "Boolean (Optional) — Default: true" }

Error Responses:

Status Condition Response Body
400 Invalid YouTube URL {"video_url": ["YouTube URL must contain 'youtube.com' or 'youtu.be'."]}
400 Invalid Bunny Stream URL {"video_url": ["Bunny Stream URL must contain 'bunnycdn' or 'b-cdn.net'."]}
400 Invalid VDO Cipher URL {"video_url": ["VDO Cipher URL must be a valid HTTPS URL."]}

GET /courses/videos//

Authentication: Any authenticated user


PUT/PATCH /courses/videos//

Authentication: SiteOwner, Teacher, Assistant


DELETE /courses/videos//

Authentication: SiteOwner, Teacher, Assistant


12. Enrollments

Request Enrollment


POST /courses/enrollments/enroll/

Description: Student requests enrollment in a course. Creates a PENDING enrollment.

Authentication: Student

Content-Type: application/json

Request Body:

{ "course": "Integer (Required) — Course ID" }

Success Response — 201 Created:

{ "id": "Integer", "student": "Integer", "student_name": "String", "student_code": "String", "course": "Integer", "course_name": "String", "grade_name": "String", "teacher_name": "String", "status": "String — pending", "status_display": "String — Pending", "enrolled_at": "DateTime (ISO 8601)", "approved_by": "Integer | null", "approved_by_name": "String | null", "responded_at": "DateTime | null", "response_note": "String | null" }

Error Responses:

Status Condition Response Body
400 No student profile {"detail": "Student profile not found."}
400 Already enrolled {"detail": "You are already enrolled or have a pending request for this course."}
400 Grade mismatch {"detail": "String"}

Business Rules:


List Enrollments


GET /courses/enrollments/

Description: List enrollments based on the caller's role.

Authentication: SiteOwner, Teacher, Assistant, Student

Query Parameters:

Parameter Type Description
status String Filter by status (pending, approved, rejected)
course Integer Filter by course ID
ordering String Default: -enrolled_at

Success Response — 200 OK: Array of enrollments.

Business Rules:


My Enrollments


GET /courses/enrollments/my/

Description: Student views their own enrollments.

Authentication: Student

Success Response — 200 OK: Array of enrollments.


Pending Enrollments


GET /courses/enrollments/pending/

Description: List pending enrollment requests for the teacher's courses.

Authentication: Teacher, Assistant

Success Response — 200 OK: Array of pending enrollments.


Approve/Reject Enrollment


POST /courses/enrollments//approve/

Description: Approve a pending enrollment.

Authentication: Teacher (course owner), Assistant (teacher's assistant)

Content-Type: application/json

Request Body:

{ "response_note": "String (Optional) — Note for the student" }

Success Response — 200 OK: Enrollment object with status: approved.

Error Responses:

Status Condition Response Body
403 Not course owner {"error": "You can only manage enrollments for your own courses"}
400 Not pending {"error": "Cannot approve enrollment with status: X"}
404 Enrollment not found {"error": "Enrollment not found"}

POST /courses/enrollments//reject/

Description: Reject a pending enrollment.

Authentication: Teacher (course owner), Assistant (teacher's assistant)

Content-Type: application/json

Request Body:

{ "response_note": "String (Optional) — Note for the student" }

Success Response — 200 OK: Enrollment object with status: rejected.

Error Responses: Same as approve endpoint.


Cancel Enrollment


POST /courses/enrollments//cancel/

Description: Student cancels their own pending enrollment. The enrollment is deleted.

Authentication: Student

Success Response — 200 OK:

{ "message": "Enrollment cancelled successfully" }

Error Responses:

Status Condition Response Body
403 Not own enrollment {"error": "You can only cancel your own enrollments"}
400 Not pending {"error": "Cannot cancel enrollment with status: X"}
404 Enrollment not found {"error": "Enrollment not found"}

Bulk Enrollment Action


POST /courses/enrollments/bulk-action/

Description: Approve or reject multiple enrollments at once.

Authentication: Teacher, Assistant

Content-Type: application/json

Request Body:

{ "enrollment_ids": "Array[Integer] (Required) — Minimum 1 ID", "action": "String (Required) — approve|reject" }

Success Response — 200 OK:

{ "processed": "Integer", "total_requested": "Integer", "action": "String (approve|reject)", "errors": [ { "enrollment_id": "Integer", "error": "String" } ] }

Error Responses:

Status Condition Response Body
400 Invalid action {"action": ["\"X\" is not a valid choice."]}
400 Empty IDs list {"enrollment_ids": ["Ensure this field has at least 1 elements."]}

Business Rules:


13. Purchases

Buy Lecture


POST /courses/purchases/buy/

Description: Student purchases a lecture. Deducts the lecture's final price from the student's course balance.

Authentication: Student

Content-Type: application/json

Request Body:

{ "lecture": "Integer (Required) — Lecture ID" }

Success Response — 201 Created:

{ "id": "Integer", "student": "Integer", "lecture": "Integer", "lecture_name": "String", "topic_name": "String", "course_name": "String", "teacher_name": "String", "purchased_at": "DateTime (ISO 8601)", "expires_at": "DateTime (ISO 8601)", "effective_expiry": "DateTime (ISO 8601)", "amount_paid": "Decimal", "extra_days": "Integer", "is_expired": "Boolean", "reopened_by": "Integer | null", "reopened_by_name": "String | null", "reopened_at": "DateTime | null", "reopen_logs": [ { "extra_days": "Integer", "reopened_by": "String | null", "reopened_at": "DateTime", "note": "String" } ] }

Error Responses:

Status Condition Response Body
400 No student profile {"detail": "Student profile not found."}
403 Not enrolled/approved {"detail": "You must be enrolled and approved in this course before purchasing lectures."}
400 Already purchased {"detail": "You have already purchased this lecture."}
400 Insufficient balance {"detail": "Insufficient balance. You need X more."}

Business Rules:


List Purchases


GET /courses/purchases/

Description: List purchased lectures based on caller's role.

Authentication: SiteOwner, Teacher, Assistant, Student

Query Parameters:

Parameter Type Description
student Integer Filter by student ID
lecture Integer Filter by lecture ID
is_expired Boolean Filter by expiration status

Success Response — 200 OK: Array of purchased lectures.

Business Rules:


My Purchases


GET /courses/purchases/my/

Description: Student views their own purchased lectures.

Authentication: Student

Success Response — 200 OK: Array of purchased lectures.


Reopen Lecture Access


PATCH /courses/purchases//reopen/

Description: Teacher/Assistant extends a student's access to a lecture by adding extra days.

Authentication: Teacher (lecture course owner), Assistant

Content-Type: application/json

Request Body:

{ "extra_days": "Integer (Required) — Minimum 1" }

Success Response — 200 OK: Updated purchased lecture.

Error Responses:

Status Condition Response Body
403 Not course owner {"error": "You can only extend access for lectures in your own courses"}
400 Invalid extra_days {"extra_days": ["Ensure this value is greater than or equal to 1."]}
404 Purchase not found {"detail": "Not found."}

Business Rules:


14. Balance & Revenue


List My Balances


GET /payments/balance/my/

Description: Get all course balances for the authenticated student. Shows available balance per course.

Authentication: Student

Success Response — 200 OK:

[ { "id": "Integer", "student": "Integer", "student_name": "String", "student_code": "String", "course": "Integer", "course_name": "String", "teacher_name": "String", "balance": "String — Decimal as string (e.g. '150.00')", "updated_at": "DateTime (ISO 8601)" } ]

Redeem Recharge Code


POST /payments/codes/redeem/

Description: Student redeems a physical voucher code to add balance to a specific course. Codes are course-specific and can only be used once.

Authentication: Student (must be approved enrolled in the course)

Request Body:

{ "code": "String (Required) — Recharge code (e.g. 'X7K9-M2P4-QR1W-L5D8')", "course": "Integer (Required) — Course ID" }

Success Response — 200 OK:

{ "detail": "Code redeemed successfully. 100.00 EGP added to your balance for 'Course Name'.", "new_balance": "150.00", "transaction_id": "Integer" }

Error Responses:

Status Condition Response Body
400 Code already used {"detail": "This code has already been used."}
400 Code expired {"detail": "This code has expired."}
400 Wrong course {"detail": "This code cannot be used for this course."}
403 Not enrolled {"detail": "You must be enrolled and approved..."}
404 Invalid code {"detail": "Invalid code."}

Business Rules:


Teacher Add Balance


POST /payments/balance/add/

Description: Teacher or Assistant directly adds balance to a student's course account. Creates a full audit trail.

Authentication: Teacher / Assistant

Request Body:

{ "student_id": "String (Required) — StudentProfile PK or student_code", "course_id": "Integer (Required) — Course ID", "amount": "String (Required) — Positive Decimal (e.g. '75.50')", "reason": "String (Optional) — Note/reason for the addition" }

Success Response — 200 OK:

{ "detail": "75.50 EGP added to student balance successfully.", "student": "Test / Test", "student_code": "String", "course": "Course Name", "amount_added": "75.50", "new_balance": "225.50", "transaction_id": "Integer" }

Error Responses:

Status Condition Response Body
400 Invalid amount {"amount": ["Ensure this value is greater than or equal to 0.01."]}
403 Not your course {"error": "You can only add balance for your own courses"}
403 Student not enrolled {"error": "Student is not enrolled in this course"}
404 Student not found {"error": "Student not found"}
404 Course not found {"error": "Course not found"}

Business Rules:


Teacher View Student Balances


GET /payments/balance/students/

Description: Teacher/Assistant views balances of all students enrolled in their courses.

Authentication: Teacher / Assistant

Query Parameters:

Parameter Type Description
course Integer Filter by course ID
student String Filter by student ID or student_code

Success Response — 200 OK:

[ { "student_id": "Integer", "student_name": "String", "student_code": "String", "course_id": "Integer", "course_name": "String", "balance": "String — Decimal as string", "enrolled_at": "DateTime (ISO 8601)" } ]

Revenue Summary


GET /payments/revenue/summary/

Description: SiteOwner revenue overview across all courses.

Authentication: SiteOwner

Success Response — 200 OK:

{ "summary": { "total_revenue": "String — Decimal as string", "total_purchases": "Integer", "total_students": "Integer", "total_courses": "Integer" }, "courses": [ { "id": "Integer", "name": "String", "is_active": "Boolean", "enrolled_count": "Integer", "purchase_count": "Integer", "revenue": "String — Decimal as string" } ] }

Revenue Per Course


GET /payments/revenue/course//

Description: Deep revenue dive for a specific course. Includes per-topic and per-lecture breakdowns, plus student spending distribution.

Authentication: SiteOwner

Success Response — 200 OK:

{ "course_id": "Integer", "course_name": "String", "total_revenue": "String — Decimal as string", "total_purchases": "Integer", "topics": [ { "topic_id": "Integer", "topic_name": "String", "lecture_count": "Integer", "purchase_count": "Integer", "revenue": "String — Decimal as string" } ], "lectures": [ { "lecture_id": "Integer", "lecture_name": "String", "topic_id": "Integer", "topic_name": "String", "price": "String", "final_price": "String", "purchase_count": "Integer", "revenue": "String — Decimal as string" } ], "student_spending": [ { "student_id": "Integer", "student_name": "String", "student_code": "String", "total_spent": "String — Decimal as string", "lectures_purchased": "Integer" } ] }

Revenue Per Lecture


GET /payments/revenue/lecture//

Description: Revenue details for a specific lecture, including purchase timeline and average time-to-purchase.

Authentication: SiteOwner

Success Response — 200 OK:

{ "lecture_id": "Integer", "lecture_name": "String", "course_name": "String", "price": "String", "final_price": "String", "total_revenue": "String — Decimal as string", "buyer_count": "Integer", "purchase_count": "Integer", "average_days_to_purchase": "Float", "purchase_timeline": [ { "student_name": "String", "student_code": "String", "amount_paid": "String — Decimal as string", "purchased_at": "DateTime (ISO 8601)" } ] }

Revenue Per Student


GET /payments/revenue/students/

Description: Per-student revenue breakdown across all courses.

Authentication: SiteOwner

Query Parameters:

Parameter Type Description
course Integer Filter by course ID

Success Response — 200 OK:

[ { "student_id": "Integer", "student_name": "String", "student_code": "String", "courses_enrolled": "Integer", "total_spent": "String — Decimal as string", "lectures_purchased": "Integer", "last_purchase_at": "DateTime (ISO 8601) | null" } ]


GET /payments/revenue/trends/

Description: Revenue trends over time. Defaults to the past 30 days. Supports custom date ranges.

Authentication: SiteOwner

Query Parameters:

Parameter Type Description
start_date String (YYYY-MM-DD) Start of date range
end_date String (YYYY-MM-DD) End of date range

Success Response — 200 OK:

{ "start_date": "2026-03-27", "end_date": "2026-04-26", "total_revenue": "String — Decimal as string", "total_purchases": "Integer", "daily_trends": [ { "date": "String (YYYY-MM-DD)", "revenue": "String — Decimal as string", "purchase_count": "Integer" } ] }

Business Rules:


15. Video Watch Progress


GET /courses/progress/

Description: List the student's video watch progress.

Authentication: Student

Success Response — 200 OK:

{ "count": "Integer", "next": "String (URL) | null", "previous": "String (URL) | null", "results": [ { "id": "Integer", "student": "Integer", "student_name": "String", "student_code": "String", "video": "Integer", "video_name": "String", "lecture_name": "String", "course_name": "String", "progress_seconds": "Integer", "duration_seconds": "Integer | null", "progress_percentage": "Decimal", "is_completed": "Boolean", "watch_count": "Integer", "last_watched_at": "DateTime (ISO 8601)", "platform_data": "Object | null" } ] }

POST /courses/progress/update/

Description: Update watch progress for a video.

Authentication: Student

Content-Type: application/json

Request Body:

{ "video": "Integer (Required) — Video ID", "progress_seconds": "Integer (Required) — Total seconds watched", "duration_seconds": "Integer (Optional) — Total video duration", "platform_data": "Object (Optional) — Platform-specific data" }

Success Response — 200 OK:

{ "id": "Integer", "student": "Integer", "video": "Integer", "progress_seconds": "Integer", "duration_seconds": "Integer | null", "progress_percentage": "Decimal", "is_completed": "Boolean", "watch_count": "Integer", "last_watched_at": "DateTime (ISO 8601)", "platform_data": "Object | null" }

Error Responses:

Status Condition Response Body
400 Missing video {"video": ["This field is required."]}
403 Lecture not purchased {"detail": "You must purchase this lecture to track progress."}

Business Rules:


GET /courses/progress/<video_id>/

Description: Get progress for a specific video.

Authentication: Student

Success Response — 200 OK: Single progress object.

Error Responses:

Status Condition Response Body
404 Progress not found {"detail": "Not found."}

16. Curriculum

Chapters and Lessons form the structured curriculum for each Subject + Grade combination.

Chapters

GET /curriculum/chapters/

Description: List all chapters. Filter by subject and/or grade.

Authentication: None (public read)

Query Parameters:

Success Response — 200 OK:

{ "count": 1, "results": [ { "id": 1, "subject": 1, "subject_name": "Chemistry", "grade": 1, "grade_name": "3rd Secondary", "name": "Chemical Bonding", "order": 1, "lesson_count": 3 } ] }

POST /curriculum/chapters/

Description: Create a new chapter.

Authentication: SiteOwner, Teacher

Request Body:

{ "subject": 1, "grade": 1, "name": "Chemical Bonding", "order": 1 }

Success Response — 201 Created:

{ "id": 1, "subject": 1, "subject_name": "Chemistry", "grade": 1, "grade_name": "3rd Secondary", "name": "Chemical Bonding", "order": 1, "lessons": [], "created_at": "2026-04-26T10:00:00Z", "updated_at": "2026-04-26T10:00:00Z" }

GET /curriculum/chapters/{id}/

Description: Retrieve a single chapter with nested lessons.

Authentication: None (public read)

Success Response — 200 OK:

{ "id": 1, "subject": 1, "subject_name": "Chemistry", "grade": 1, "grade_name": "3rd Secondary", "name": "Chemical Bonding", "order": 1, "lessons": [ {"id": 1, "name": "Ionic Bonds", "order": 1}, {"id": 2, "name": "Covalent Bonds", "order": 2} ], "created_at": "2026-04-26T10:00:00Z", "updated_at": "2026-04-26T10:00:00Z" }

PUT /curriculum/chapters/{id}/

Description: Update a chapter.

Authentication: SiteOwner, Teacher


DELETE /curriculum/chapters/{id}/

Description: Delete a chapter and all its lessons.

Authentication: SiteOwner, Teacher


Lessons

GET /curriculum/lessons/

Description: List all lessons. Filter by chapter.

Authentication: None (public read)

Query Parameters:


POST /curriculum/lessons/

Description: Create a new lesson.

Authentication: SiteOwner, Teacher

Request Body:

{ "chapter": 1, "name": "Ionic Bonds", "order": 1 }

GET /curriculum/lessons/{id}/

Description: Retrieve a single lesson.

Authentication: None (public read)


PUT /curriculum/lessons/{id}/

Description: Update a lesson.

Authentication: SiteOwner, Teacher


DELETE /curriculum/lessons/{id}/

Description: Delete a lesson.

Authentication: SiteOwner, Teacher


Curriculum by Subject & Grade

GET /curriculum/by-subject-grade/?subject={id}&grade={id}

Description: Get all chapters with nested lessons for a specific subject + grade combination.

Authentication: None (public read)

Query Parameters:

Success Response — 200 OK:

[ { "id": 1, "subject": 1, "subject_name": "Chemistry", "grade": 1, "grade_name": "3rd Secondary", "name": "Chemical Bonding", "order": 1, "lessons": [ {"id": 1, "name": "Ionic Bonds", "order": 1}, {"id": 2, "name": "Covalent Bonds", "order": 2} ] } ]

17. Question Bank

Reusable questions organized by curriculum lesson. Questions can be Global (visible to all teachers) or Private (visible only to the creator).

Questions

GET /question-bank/questions/

Description: List questions accessible to the authenticated teacher.

Authentication: Teacher, Assistant

Query Parameters:

Success Response — 200 OK:

{ "count": 1, "results": [ { "id": 1, "lesson": 1, "lesson_name": "Ionic Bonds", "chapter_name": "Chemical Bonding", "subject_name": "Chemistry", "question_type": "mcq_single", "difficulty": "medium", "points": 2, "choice_count": 4, "is_global": false, "created_by": 5, "created_at": "2026-04-26T10:00:00Z" } ] }

POST /question-bank/questions/

Description: Create a new question with optional choices.

Authentication: Teacher, Assistant, SiteOwner

Request Body:

{ "lesson": 1, "question_type": "mcq_single", "text": "What is the chemical formula for water?", "image": null, "difficulty": "easy", "points": 1, "explanation": "H2O is the universally accepted formula for water.", "is_global": false, "choices": [ {"text": "H2O", "is_correct": true, "order": 0}, {"text": "CO2", "is_correct": false, "order": 1}, {"text": "NaCl", "is_correct": false, "order": 2}, {"text": "O2", "is_correct": false, "order": 3} ] }

Notes:

Success Response — 201 Created:

{ "id": 1, "lesson": 1, "question_type": "mcq_single", "text": "What is the chemical formula for water?", ... }

GET /question-bank/questions/{id}/

Description: Retrieve a single question with all choices (including is_correct).

Authentication: Teacher, Assistant


PUT /question-bank/questions/{id}/

Description: Update a question. Sending choices will replace all existing choices.

Authentication: Teacher, Assistant, SiteOwner


DELETE /question-bank/questions/{id}/

Description: Delete a question.

Authentication: Teacher, Assistant, SiteOwner


Random Selection

POST /question-bank/questions/random-select/

Description: Randomly select question IDs from the bank for quiz/homework creation.

Authentication: Teacher, Assistant

Request Body:

{ "lesson": 1, "question_type": "mcq_single", "difficulty": "medium", "count": 10 }

Success Response — 200 OK:

{ "total_available": 25, "requested": 10, "selected": [3, 7, 12, 15, 21, 34, 45, 56, 67, 78] }

18. Study Materials

PDF study materials attached to lectures. Only visible to students who have purchased the lecture.

Materials CRUD

GET /materials/

Description: List study materials.

Authentication: Any authenticated user (students see only purchased lecture materials; teachers see their course materials)

Query Parameters:


POST /materials/

Description: Upload a new PDF study material.

Authentication: SiteOwner, Teacher, Assistant

Content-Type: multipart/form-data

Request Body:

{ "lecture": 1, "title": "Lecture Notes - Chapter 1", "description": "Complete notes for this lecture", "file": <PDF_FILE> }

Validation:

Error Responses:

Status Condition Response Body
400 File too large {"file": ["File too large. Size should not exceed 50 MB."]}
400 Invalid file type {"file": ["File extension 'xyz' is not allowed. Allowed extensions are: pdf."]}

GET /materials/{id}/

Description: Retrieve a single material.

Authentication: SiteOwner, Teacher, Assistant (students must have purchased the lecture)


PUT /materials/{id}/

Description: Update a material (replace PDF, change title, etc.).

Authentication: SiteOwner, Teacher, Assistant


DELETE /materials/{id}/

Description: Delete a material.

Authentication: SiteOwner, Teacher, Assistant


Student Materials

GET /materials/my/

Description: Get all study materials for lectures the student has purchased.

Authentication: Student

Success Response — 200 OK:

[ { "id": 1, "lecture": 1, "lecture_name": "Introduction to Chemistry", "title": "Lecture Notes - Chapter 1", "file_url": "https://example.com/media/materials/2026/04/notes.pdf", "is_active": true, "created_at": "2026-04-26T10:00:00Z" } ]

19. Quizzes

Quizzes are attached to lectures and contain questions from the Question Bank. They support timers, multiple attempts, randomized ordering, and configurable score/answer release.

Quiz CRUD

GET /learning/quizzes/

Description: List quizzes.

Authentication: Any authenticated user (students see published quizzes for purchased lectures; teachers see their course quizzes)

Query Parameters:

Success Response — 200 OK:

{ "count": 1, "results": [ { "id": 1, "lecture": 1, "lecture_name": "Introduction to Chemistry", "title": "Chapter 1 Quiz", "description": "Test your knowledge of chemical bonding", "passing_score": "60.00", "is_active": true, "is_published": true, "total_points": 10, "question_count": 5, "settings": { "open_date": null, "close_date": null, "timer_minutes": 30, "score_visibility": "immediate", "answers_visibility": "immediate", "question_order": "fixed", "shuffle_choices": false, "allow_multiple_attempts": false, "max_attempts": 1, "attempt_scoring": "best", "show_correct_after_submission": false }, "created_at": "2026-04-26T10:00:00Z" } ] }

POST /learning/quizzes/

Description: Create a new quiz with settings and question assignments.

Authentication: Teacher, Assistant, SiteOwner

Request Body:

{ "lecture": 1, "title": "Chapter 1 Quiz", "description": "Test your knowledge", "passing_score": 60.00, "is_active": true, "is_published": false, "settings": { "timer_minutes": 30, "score_visibility": "immediate", "answers_visibility": "after_close", "question_order": "random", "shuffle_choices": true, "allow_multiple_attempts": true, "max_attempts": 3, "attempt_scoring": "best", "show_correct_after_submission": true }, "question_ids": [1, 2, 3, 4, 5] }

Notes:


GET /learning/quizzes/{id}/

Description: Retrieve a quiz with questions and settings.

Authentication: Teacher, Assistant, SiteOwner (students must have purchased the lecture)


PUT /learning/quizzes/{id}/

Description: Update a quiz. Sending question_ids replaces all questions.

Authentication: Teacher, Assistant, SiteOwner


DELETE /learning/quizzes/{id}/

Description: Delete a quiz and all its submissions.

Authentication: Teacher, Assistant, SiteOwner


Quiz Attempt

POST /learning/quizzes/{id}/start/

Description: Start a quiz attempt. Creates a QuizSubmission and returns questions in the student's randomized order (if enabled).

Authentication: Student

Access Control:

Success Response — 200 OK:

{ "submission_id": 15, "attempt_number": 1, "timer_minutes": 30, "started_at": "2026-04-26T10:00:00Z", "questions": [ { "answer_id": 45, "quiz_question_id": 3, "question_text": "What is H2O?", "question_image": "https://example.com/media/...", "question_type": "mcq_single", "points": 2, "choices": [ {"id": 10, "text": "Water", "image": null, "order": 0}, {"id": 11, "text": "Oxygen", "image": null, "order": 1} ] } ] }

POST /learning/quizzes/{id}/submit/

Description: Submit quiz answers.

Authentication: Student

Request Body:

{ "answers": [ { "quiz_question_id": 3, "choice_ids": [10] }, { "quiz_question_id": 4, "choice_ids": [12, 14] }, { "quiz_question_id": 5, "written_answer": "The answer is..." } ] }

Notes:

Success Response — 200 OK:

{ "detail": "Quiz submitted successfully.", "submission_id": 15, "score": "8.50", "passed": true, "score_visible": true, "answers_visible": true }

Quiz Results & Grading

GET /learning/quizzes/{id}/results/

Description: List all student submissions for a quiz.

Authentication: Teacher, Assistant


GET /learning/quiz-submissions/{id}/

Description: View a specific submission.

Authentication: Student (own submissions), Teacher, Assistant

Notes:


POST /learning/quiz-submissions/{id}/grade-written/

Description: Manually grade a written answer.

Authentication: Teacher, Assistant

Request Body:

{ "answer_id": 45, "score_override": 4.50, "feedback": "Good explanation but missing one key point." }

POST /learning/quizzes/{id}/release-scores/

Description: Release scores for all submissions of a quiz (for manual visibility).

Authentication: Teacher, Assistant

Success Response — 200 OK:

{ "detail": "Scores released for 25 submissions." }

POST /learning/quizzes/{id}/release-answers/

Description: Release correct answers for all submissions of a quiz (for manual visibility).

Authentication: Teacher, Assistant


Student Quiz History

GET /learning/quizzes/my/

Description: Get all quiz submissions for the logged-in student.

Authentication: Student

Success Response — 200 OK:

[ { "id": 15, "quiz_id": 1, "quiz_title": "Chapter 1 Quiz", "lecture_name": "Introduction to Chemistry", "attempt_number": 1, "score": "8.50", "passed": true, "submitted_at": "2026-04-26T10:30:00Z", "score_visible": true, "answers_visible": true } ]

20. Homeworks

Homeworks are attached to lectures and contain MCQ questions from the Question Bank. They are auto-corrected on submission and show a model answer after submission.

Homework CRUD

GET /learning/homeworks/

Description: List homeworks.

Authentication: Any authenticated user (students see published homeworks for purchased lectures; teachers see their course homeworks)

Query Parameters:

Success Response — 200 OK:

{ "count": 1, "results": [ { "id": 1, "lecture": 1, "lecture_name": "Introduction to Chemistry", "title": "Chapter 1 Homework", "description": "Practice problems", "is_active": true, "is_published": true, "total_points": 5, "question_count": 5, "open_date": null, "close_date": null, "created_at": "2026-04-26T10:00:00Z" } ] }

POST /learning/homeworks/

Description: Create a new homework.

Authentication: Teacher, Assistant, SiteOwner

Request Body:

{ "lecture": 1, "title": "Chapter 1 Homework", "description": "Practice problems", "model_answer": "1-A, 2-B, 3-C, 4-D, 5-A", "is_active": true, "is_published": false, "open_date": null, "close_date": null, "question_ids": [1, 2, 3, 4, 5] }

Notes:


GET /learning/homeworks/{id}/

Description: Retrieve a homework with questions.

Authentication: Teacher, Assistant, SiteOwner (students must have purchased the lecture)


PUT /learning/homeworks/{id}/

Description: Update a homework.

Authentication: Teacher, Assistant, SiteOwner


DELETE /learning/homeworks/{id}/

Description: Delete a homework.

Authentication: Teacher, Assistant, SiteOwner


Homework Submission

POST /learning/homeworks/{id}/submit/

Description: Submit homework answers. Auto-corrected immediately.

Authentication: Student

Access Control:

Request Body:

{ "answers": [ { "homework_question_id": 1, "choice_id": 10 }, { "homework_question_id": 2, "choice_id": 12 } ] }

Success Response — 200 OK:

{ "detail": "Homework submitted and auto-graded.", "submission_id": 8, "score": "4.00", "status": "graded" }

GET /learning/homeworks/{id}/submissions/

Description: List all submissions for a homework.

Authentication: Teacher, Assistant


GET /learning/homework-submissions/{id}/

Description: View a specific submission (includes model answer for the student).

Authentication: Student (own submissions), Teacher, Assistant


Student Homework History

GET /learning/homeworks/my/

Description: Get all homework submissions for the logged-in student.

Authentication: Student

Success Response — 200 OK:

[ { "id": 8, "homework_id": 1, "homework_title": "Chapter 1 Homework", "lecture_name": "Introduction to Chemistry", "score": "4.00", "status": "graded", "submitted_at": "2026-04-26T10:30:00Z" } ]

21. Dashboard & Analytics

Teacher Dashboard


GET /courses/dashboard/teacher/

Description: Get aggregated stats for the teacher's dashboard.

Authentication: Teacher, Assistant

Success Response — 200 OK:

{ "total_courses": "Integer", "total_students": "Integer", "total_revenue": "Decimal", "recent_enrollments": [ { "id": "Integer", "student_name": "String", "course_name": "String", "status": "String", "enrolled_at": "DateTime (ISO 8601)" } ], "recent_purchases": [ { "id": "Integer", "student_name": "String", "lecture_name": "String", "amount_paid": "Decimal", "purchased_at": "DateTime (ISO 8601)" } ], "course_breakdown": [ { "id": "Integer", "name": "String", "student_count": "Integer", "purchase_count": "Integer", "revenue": "Decimal" } ] }

Error Responses:

Status Condition Response Body
403 Not teacher/assistant {"detail": "You do not have permission..."}

Business Rules:


Course Analytics

See GET /courses//analytics/ under Courses section.


Appendix A: Role-Based Permission Matrix

Endpoint SiteOwner Teacher Assistant Student Public
POST /accounts/login/ Yes Yes Yes Yes Yes
GET /accounts/me/ Yes Yes Yes Yes No
POST /accounts/student/register/ No No No No Yes
GET /accounts/subjects/ Yes Yes Yes Yes Yes
GET /accounts/public/teachers/ Yes Yes Yes Yes Yes
GET /accounts/teachers/ Yes No No No No
POST /accounts/teachers/ Yes No No No No
GET /accounts/students/ Yes No No No No
GET /accounts/profile/me/ No No No Yes No
GET /courses/ Yes Yes Yes Yes No
GET /courses/by-subject/<id>/ Yes Yes Yes Yes No
POST /courses/ Yes No No No No
GET /courses/<id>/lectures/ No No No Yes* No
POST /courses/enrollments/enroll/ No No No Yes No
POST /courses/enrollments/<id>/approve/ No Yes** Yes** No No
POST /courses/purchases/buy/ No No No Yes No
GET /courses/dashboard/teacher/ No Yes Yes No No

* Must be enrolled and approved
** Must own the course (or be the course teacher's assistant)


Appendix B: Enum Reference

User Roles

Student Status

Enrollment Status

Video Platforms

Gender


Documentation generated by OpenCode AI Agent
Project: EduTrack Online Backend
Last Updated: April 29, 2026 (Round 5 — Teacher Single Subject, Admin Overhaul, CSRF Origin Fix)