Version: 1.3 Date: July 16, 2026 Target Audience: Next.js Frontend Developers
Backend: Django REST Framework
This file documents endpoints that require NO authentication.
https://apionline.edutrackeg.com
Description: Authenticates a user with username, email, gmail, or student phone number + password. Returns user data in the JSON body and sets JWT tokens as HTTP-Only cookies.
Cookies set on success:
access_token JWT access token, 30 min expiry, httpOnly, Secure, SameSite=None, Path=/refresh_token JWT refresh token, 7 days expiry, httpOnly, Secure, SameSite=None, Path=/Both tokens contain
rolein their JWT payload. The frontend can decode the access token to readuser_idandrolewithout additional API calls.
Authentication: Public (no login required)
Content-Type: application/json
Request Body:
{
"username": "String (Required) — Username, email, gmail, or student phone number",
"password": "String (Required)"
}
Login Identifier Resolution:
The username field accepts any of the following (resolved in order):
| Input type | Example | Works for |
|---|---|---|
| Username | "hazem123" |
All roles |
"hazem@gmail.com" |
All roles (matches User.email) |
|
| Gmail | "hazem@gmail.com" |
All roles (matches profile's gmail field) |
| Phone number | "01012345678" |
Students only (matches StudentProfile.phone_number) |
Success Response — 200 OK:
{
"role": "String (siteowner|teacher|assistant|student)",
"name": "String",
"is_active": "Boolean",
"student_code": "String | null — Only for students. The 7-digit student identifier.",
"status": "String | null — Only for students: pending|verified|declined|suspended_temporary|suspended_permanent"
}
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 |
Permanently suspended student | {"error": "Your account has been permanently suspended."} |
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. Also refreshes the user-role cookie if present. 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"
}
Cookie set on success:
user-role Refreshed JWT containing {user_id, role}, 7 days expiry, Secure, SameSite=None, Path=/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": "boolean - Whether the OTP is valid",
"reset_token": "String 64-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 (Required) — 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 |
Weak password | {"error": "This password is too common."} or {"error": "This password is entirely numeric."} |
400 |
OTP expired | {"error": "OTP has expired"} |
400 |
Missing reset token | {"error": "Reset token is required. Please verify your OTP first."} |
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,
"email": "String - The verified email address",
"verification_token": "String - Single-use token required for registration",
"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"} |
Business Rules:
verification_token \u2014 it's required when submitting the registration form.Description: Register a new student account. Creates both a User and StudentProfile atomically. Email must be verified first (see verify-email flow above).
Authentication: Public
Content-Type: multipart/form-data
Prerequisite: Call POST /verify-email/confirm/ first to obtain a verification_token. Include it in this request.
Request Body:
{
"verification_token": "String (Required) Token from verify-email/confirm",
"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 (Required)",
"mother_number": "String (Required)",
"school_type": "Integer (Required)",
"grade": "Integer (Required)",
"division": "Integer (Required)",
"school_name": "String (Required)",
"birth_date": "Date (YYYY-MM-DD) (Required)",
"gender": "String (Required) male|female",
"gmail": "String (Required) Must be globally unique, must match verified email",
"governorate": "Integer (Required)",
"area": "Integer (Required) Must belong to the selected governorate"
}
Success Response 201 Created:
{
"message": "String - Success message",
"student_code": "String - Auto-generated 7-digit code",
"username": "String - Student's username",
"status": "String - Always 'pending'"
}
Error Responses:
| Status | Condition | Response Body |
|---|---|---|
400 |
Missing verification token | {"verification_token": ["Email verification is required."]} |
400 |
Invalid/expired token | {"verification_token": ["Invalid or expired verification token..."]} |
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 type or grade | {"school_type": ["School type and grade are required."]} |
400 |
Missing division | {"division": ["Division is required."]} |
400 |
Missing school name | {"school_name": ["School name is required."]} |
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:
POST /verify-email/confirm/ first to get a verification_token.gmail and verification_token must match the email that was verified.area must belong to the selected governorate.student_code is auto-generated and guaranteed unique.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"
},
"subjects_detail": [
{
"id": "Integer",
"name": "String"
}
],
"grades_detail": [
{
"id": "Integer",
"name": "String"
}
],
"courses_count": "Integer — Number of active courses for this teacher"
}
]
}
GET /accounts/public/stats/Description: Public platform stats for the landing page (SEO/marketing). Returns active teacher count, active course count, and subject count. Student counts are intentionally excluded — per-teacher/aggregate student data is private.
Authentication: Public
Success Response — 200 OK:
{
"teacher_count": 12,
"course_count": 34,
"subject_count": 40
}
GET /accounts/public/teachers/<id>/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"
},
"subjects_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"} |
Public course browsing for anonymous users (SEO pages). No prices, no video data, no student data. These endpoints replaced the old anonymous GET /courses/?teacher=X path — course lists and details now require authentication, so the landing page uses these.
GET /courses/public/Description: List active courses for the public catalog.
Authentication: Public
Query Parameters:
| Parameter | Type | Description |
|---|---|---|
subject |
Integer | Filter by subject ID |
teacher |
Integer | Filter by teacher ID |
grade |
Integer | Filter by grade ID |
search |
String | Search by name, description, or teacher name |
Success Response — 200 OK (paginated):
{
"count": "Integer",
"next": "String (URL) | null",
"previous": "String (URL) | null",
"results": [
{
"id": "Integer",
"name": "String",
"description": "String | null",
"cover_picture": "String (URL) | null",
"teacher": {"id": "Integer", "name": "String"},
"grade": {"id": "Integer", "name": "String"},
"subject": {"id": "Integer", "name": "String"},
"topic_count": "Integer",
"total_lectures": "Integer",
"is_active": "Boolean"
}
]
}
GET /courses/public/<id>/Description: Public course detail — course info + topic/lecture names only. No prices, no video IDs, no Bunny data.
Authentication: Public
Success Response — 200 OK:
{
"id": "Integer",
"name": "String",
"description": "String | null",
"cover_picture": "String (URL) | null",
"teacher": {"id": "Integer", "name": "String"},
"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",
"picture": "String (URL) | null",
"lectures": [
{"id": "Integer", "name": "String", "description": "String | null", "order": "Integer"}
]
}
]
}
Error Responses:
| Status | Condition | Response Body |
|---|---|---|
404 |
Course not found / inactive | {"error": "Course not found"} |
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/<id>/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"
}
]
}
GET /accounts/school-types/<id>/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_detail": [
{
"id": "Integer",
"name": "String"
}
],
"school_types_detail": [
{
"id": "Integer",
"name": "String"
}
],
"grade_ids": "Array[Integer] - Grade IDs",
"school_type_ids": "Array[Integer] - School type IDs"
}
]
}
GET /accounts/divisions/<id>/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",
"grade_ids": "Array[Integer] - Grade IDs",
"grades_detail": [
{
"id": "Integer",
"name": "String"
}
],
"division_ids": "Array[Integer] - Division IDs",
"divisions_detail": [
{
"id": "Integer",
"name": "String"
}
],
"school_type_ids": "Array[Integer] - School type IDs",
"school_types_detail": [
{
"id": "Integer",
"name": "String"
}
],
}
]
}
GET /accounts/subjects/<id>/Authentication: Public
Success Response — 200 OK: Same structure as list item.
Description: List all governorates.
Authentication: Public
Success Response — 200 OK:
{
"count": "Integer",
"next": "String (URL) | null",
"previous": "String (URL) | null",
"results": [
{
"id": "Integer",
"name": "String"
}
]
}
GET /accounts/governorates/<id>/Authentication: Public
Success Response — 200 OK:
{
"id": "Integer",
"name": "String"
}
Description: List all areas (filterable by governorate).
Authentication: Public
Query Parameters:
| Parameter | Type | Description |
|---|---|---|
governorate |
Integer | Filter by governorate ID |
Success Response — 200 OK:
{
"count": "Integer",
"next": "String (URL) | null",
"previous": "String (URL) | null",
"results": [
{
"id": "Integer",
"name": "String",
"governorate_id": "Integer - Governorate ID",
"governorate_name": "String - Governorate name"
}
]
}
GET /accounts/areas/<id>/Authentication: Public
Success Response — 200 OK:
{
"id": "Integer",
"name": "String",
"governorate_id": "Integer - Governorate ID",
"governorate_name": "String - Governorate name"
}
Description: Returns the RSA public key used for JWT verification (RS256 algorithm). Frontends can use this to verify token signatures locally.
Authentication: None (public)
Success Response 200 OK:
{
"public_key": "String PEM-formatted RSA public key",
"algorithm": "String Always 'RS256'"
}
Error Responses:
| Status | Condition | Response Body |
|---|---|---|
500 |
Server has no public key configured | {"error": "Public key not configured on the server."} |
Description: Check if a field value is already taken. Used for real-time validation on registration and profile forms.
Authentication: None (public)
Query Parameters:
| Parameter | Type | Description |
|---|---|---|
field |
String | username, gmail, or phone_number |
value |
String | The value to check |
Success Response 200 OK:
{
"available": true,
"message": ""
}
When unavailable:
{
"available": false,
"message": "This username is already taken."
}
Error Responses:
| Status | Condition | Response Body |
|---|---|---|
400 |
Missing or invalid field | {"error": "Both field and value are required."} |
GET /accounts/video-security/Description: List video security options (platform fee per student). SiteOwner only — used when assigning security settings to teachers.
Authentication: SiteOwner
Query Parameters:
| Parameter | Type | Description |
|---|---|---|
search |
String | Filter by name |
Success Response — 200 OK:
{
"count": "Integer",
"results": [
{
"id": "Integer",
"name": "String",
"price_per_student": "String (Decimal)"
}
]
}
GET /accounts/video-security/<id>/Description: Retrieve a single video security option.
Authentication: SiteOwner
Success Response — 200 OK:
{
"id": "Integer",
"name": "String",
"price_per_student": "String (Decimal)"
}
Error Responses:
| Status | Condition | Response Body |
|---|---|---|
403 |
Not a SiteOwner | {"detail": "You do not have permission to perform this action."} |
404 |
Not found | {"error": "Video security option not found"} |