Version: 1.1
Date: April 29, 2026
Target Audience: Next.js Frontend Developers
Backend: Django REST Framework
http://localhost:8000/api/
All endpoints are prefixed with /api/.
The API uses JWT tokens stored in HTTP-Only cookies.
POST /api/accounts/login/. The server sets two cookies:
access_token — short-lived (30 minutes)refresh_token — long-lived (7 days)Authorization header needed.Authorization: Bearer <token> header.POST /api/accounts/token/refresh/. The server reads the refresh token from the cookie, generates new tokens, and blacklists the old refresh token.POST /api/accounts/logout/. The server blacklists the refresh token and clears both cookies.Inactive User Blocking:
CSRF / Cross-Origin Notes:
Origin (or Referer) header against CSRF_TRUSTED_ORIGINS for cookie-based auth.CSRF_TRUSTED_ORIGINS on the backend (e.g., https://eduonline-five.vercel.app).CSRF Failed: CSRF cookie not set, the origin is not trusted.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"} |
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 |
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_tokenandrefresh_tokenare 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:
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:
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_tokenandrefresh_tokencookies 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:
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."} |
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:
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"} |
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:
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:
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:
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"} |
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.
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"
}
]
}
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"} |
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"
}
]
}
Authentication: Public
Success Response — 200 OK:
{
"id": "Integer",
"name": "String"
}
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"
}
]
}
]
}
Authentication: Public
Success Response — 200 OK: Same structure as list item.
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]"
}
]
}
Authentication: Public
Success Response — 200 OK: Same structure as list item.
These endpoints are accessible without authentication for browsing 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"
}
]
}
]
}
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"} |
These endpoints require authentication and are used by SiteOwner to manage 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"
}
]
}
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:
subject is a single Subject ID (changed from Array in Apr 29, 2026 refactor).Authentication: SiteOwner
Success Response — 200 OK: Same structure as list item.
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."} |
These endpoints allow a teacher to manage their own 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"
}
]
}
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"
}
Authentication: Teacher (must own the assistant)
Authentication: Teacher (must own the assistant)
Authentication: Teacher (must own the assistant)
Success Response — 204 No Content
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:
school_type, grade, division, school_name, father_number, mother_number.area must belong to the selected governorate.student_code is auto-generated and guaranteed unique.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)"
}
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 |
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/).
Authentication: SiteOwner
Success Response — 200 OK: Single student profile.
Authentication: SiteOwner
Content-Type: application/json
Request Body: Same as student update serializer.
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"
}
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:
grade, school_type (via subject), division (via subject), and educational_state.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>/).
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:
teacher.subject — it is NOT sent in the request.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)"
}
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:
Authentication: SiteOwner only
Error Responses:
| Status | Condition | Response Body |
|---|---|---|
400 |
Students enrolled | {"detail": "Cannot delete a course that has students enrolled in it."} |
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"} |
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."} |
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"
}
]
}
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:
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"} |
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"} |
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
purchaseobject is only included ifis_purchasedistrue.
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"} |
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)"
}
]
}
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"
}
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)"
}
Authentication: SiteOwner, Teacher, Assistant
Authentication: SiteOwner, Teacher, Assistant
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:
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.
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.
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)"
}
Authentication: SiteOwner, Teacher, Assistant
Authentication: SiteOwner, Teacher, Assistant
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:
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).
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."]} |
Authentication: Any authenticated user
Authentication: SiteOwner, Teacher, Assistant
Authentication: SiteOwner, Teacher, Assistant
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:
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:
Description: Student views their own enrollments.
Authentication: Student
Success Response — 200 OK: Array of enrollments.
Description: List pending enrollment requests for the teacher's courses.
Authentication: Teacher, Assistant
Success Response — 200 OK: Array of pending enrollments.
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"} |
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.
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"} |
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:
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:
final_price is deducted from the student's course balance.expires_at is calculated as purchased_at + available_days.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:
Description: Student views their own purchased lectures.
Authentication: Student
Success Response — 200 OK: Array of purchased lectures.
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:
effective_expiry is recalculated as expires_at + extra_days.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)"
}
]
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:
XXXX-XXXX-XXXX-XXXX (16 random chars, cryptographically secure).BalanceTransaction ledger entry.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:
Decimal — no float precision errors.BalanceTransaction with balance_before and balance_after.StudentActivity log entry with type BALANCE_ADDED.Notification to the student.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)"
}
]
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"
}
]
}
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"
}
]
}
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)"
}
]
}
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"
}
]
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:
end_date provided, start_date = end_date - 30 days.start_date provided, end_date = today.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"
}
]
}
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:
is_completed is auto-set to true when progress_percentage >= 90%.watch_count increments on each update.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."} |
Chapters and Lessons form the structured curriculum for each Subject + Grade combination.
Description: List all chapters. Filter by subject and/or grade.
Authentication: None (public read)
Query Parameters:
subject — Filter by subject IDgrade — Filter by grade IDSuccess 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
}
]
}
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"
}
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"
}
Description: Update a chapter.
Authentication: SiteOwner, Teacher
Description: Delete a chapter and all its lessons.
Authentication: SiteOwner, Teacher
Description: List all lessons. Filter by chapter.
Authentication: None (public read)
Query Parameters:
chapter — Filter by chapter IDDescription: Create a new lesson.
Authentication: SiteOwner, Teacher
Request Body:
{
"chapter": 1,
"name": "Ionic Bonds",
"order": 1
}
Description: Retrieve a single lesson.
Authentication: None (public read)
Description: Update a lesson.
Authentication: SiteOwner, Teacher
Description: Delete a lesson.
Authentication: SiteOwner, Teacher
Description: Get all chapters with nested lessons for a specific subject + grade combination.
Authentication: None (public read)
Query Parameters:
subject (required) — Subject IDgrade (required) — Grade IDSuccess 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}
]
}
]
Reusable questions organized by curriculum lesson. Questions can be Global (visible to all teachers) or Private (visible only to the creator).
Description: List questions accessible to the authenticated teacher.
Authentication: Teacher, Assistant
Query Parameters:
lesson — Filter by lesson IDquestion_type — mcq_single, mcq_multiple, writtendifficulty — easy, medium, hardis_global — true or falselesson__chapter__subject — Filter by subjectlesson__chapter__grade — Filter by gradesearch — Search in question text and explanationSuccess 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"
}
]
}
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:
choices is optional for written type questionstext or image (or both)text or image (or both)Success Response — 201 Created:
{
"id": 1,
"lesson": 1,
"question_type": "mcq_single",
"text": "What is the chemical formula for water?",
...
}
Description: Retrieve a single question with all choices (including is_correct).
Authentication: Teacher, Assistant
Description: Update a question. Sending choices will replace all existing choices.
Authentication: Teacher, Assistant, SiteOwner
Description: Delete a question.
Authentication: Teacher, Assistant, SiteOwner
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]
}
PDF study materials attached to lectures. Only visible to students who have purchased the lecture.
Description: List study materials.
Authentication: Any authenticated user (students see only purchased lecture materials; teachers see their course materials)
Query Parameters:
lecture — Filter by lecture IDis_active — true or falseDescription: 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."]} |
Description: Retrieve a single material.
Authentication: SiteOwner, Teacher, Assistant (students must have purchased the lecture)
Description: Update a material (replace PDF, change title, etc.).
Authentication: SiteOwner, Teacher, Assistant
Description: Delete a material.
Authentication: SiteOwner, Teacher, Assistant
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"
}
]
Quizzes are attached to lectures and contain questions from the Question Bank. They support timers, multiple attempts, randomized ordering, and configurable score/answer release.
Description: List quizzes.
Authentication: Any authenticated user (students see published quizzes for purchased lectures; teachers see their course quizzes)
Query Parameters:
lecture — Filter by lecture IDis_active — true or falseis_published — true or falseSuccess 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"
}
]
}
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:
passing_score is optional (null = no passing threshold)question_ids are QuestionBank IDs; they will be assigned in orderscore_visibility / answers_visibility: immediate, after_close, manualDescription: Retrieve a quiz with questions and settings.
Authentication: Teacher, Assistant, SiteOwner (students must have purchased the lecture)
Description: Update a quiz. Sending question_ids replaces all questions.
Authentication: Teacher, Assistant, SiteOwner
Description: Delete a quiz and all its submissions.
Authentication: Teacher, Assistant, SiteOwner
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}
]
}
]
}
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:
choice_ids for MCQ types (array of choice IDs)written_answer for written type questionsSuccess Response — 200 OK:
{
"detail": "Quiz submitted successfully.",
"submission_id": 15,
"score": "8.50",
"passed": true,
"score_visible": true,
"answers_visible": true
}
Description: List all student submissions for a quiz.
Authentication: Teacher, Assistant
Description: View a specific submission.
Authentication: Student (own submissions), Teacher, Assistant
Notes:
score_visibility settinganswers_visibility settingDescription: 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."
}
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."
}
Description: Release correct answers for all submissions of a quiz (for manual visibility).
Authentication: Teacher, Assistant
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
}
]
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.
Description: List homeworks.
Authentication: Any authenticated user (students see published homeworks for purchased lectures; teachers see their course homeworks)
Query Parameters:
lecture — Filter by lecture IDis_active — true or falseis_published — true or falseSuccess 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"
}
]
}
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:
model_answer is shown to students after they submitDescription: Retrieve a homework with questions.
Authentication: Teacher, Assistant, SiteOwner (students must have purchased the lecture)
Description: Update a homework.
Authentication: Teacher, Assistant, SiteOwner
Description: Delete a homework.
Authentication: Teacher, Assistant, SiteOwner
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"
}
Description: List all submissions for a homework.
Authentication: Teacher, Assistant
Description: View a specific submission (includes model answer for the student).
Authentication: Student (own submissions), Teacher, Assistant
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"
}
]
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:
See GET /courses/
| 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)
siteowner — System ownerteacher — Course creator and managerassistant — Teacher's assistantstudent — Content consumerverified — Verified and activepending — Pending approval (default)declined — Registration declinedsuspended_temporary — Temporarily suspendedsuspended_permanent — Permanently suspendedpending — Waiting for approvalapproved — Enrolled and can purchase lecturesrejected — Enrollment rejectedyoutube — YouTube Unlistedbunnystream — Bunny Streamvdocipher — VDO Ciphermale — Malefemale — FemaleDocumentation generated by OpenCode AI Agent
Project: EduTrack Online Backend
Last Updated: April 29, 2026 (Round 5 — Teacher Single Subject, Admin Overhaul, CSRF Origin Fix)