EduTrack Online - API Overview

Version: 1.3
Date: July 16, 2026
Target Audience: Next.js Frontend Developers
Backend: Django REST Framework

Base URL

https://apionline.edutrackeg.com

Authentication

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

Inactive User Blocking:

Single-Session Enforcement (Students Only):

CSRF / Cross-Origin Notes:

JWT Token Structure

Tokens are RS256-signed JWTs (asymmetric RSA keys). The payload is not encrypted — anyone with the public key can decode and read the claims. The signature prevents tampering.

Access Token (30 minutes)

{ "token_type": "access", "exp": "Integer — Expiration timestamp (Unix)", "iat": "Integer — Issued at timestamp (Unix)", "jti": "String — Unique token ID (used for blacklisting)", "user_id": "Integer — The user's primary key", "role": "String — User's role (siteowner|teacher|assistant|student)", "token_version": "Integer — Incremented on each student login (used for single-session enforcement)" }

Refresh Token (7 days)

{ "token_type": "refresh", "exp": "Integer — Expiration timestamp (Unix)", "iat": "Integer — Issued at timestamp (Unix)", "jti": "String — Unique token ID (used for blacklisting)", "user_id": "Integer — The user's primary key", "role": "String — User's role (siteowner|teacher|assistant|student)", "token_version": "Integer — Incremented on each student login (used for single-session enforcement)" }

Decoding a Token (Frontend)

On the frontend, you can decode the payload without the public key (the payload is base64-encoded, not encrypted):

// Decode JWT payload (NOT verification — just reading claims) function decodeJwtPayload(token) { const payload = token.split('.')[1]; return JSON.parse(atob(payload)); }

To verify the signature, use the public key from GET /accounts/public-key/ with a JWT library (e.g., jsonwebtoken on npm).

The backend sets HTTP-Only cookies (inaccessible to JavaScript) for JWT tokens:

Cookie Name Value Max Age Secure HttpOnly SameSite Domain Path
access_token JWT access token 30 min true true None Configurable via JWT_AUTH_COOKIE_DOMAIN env var /
refresh_token JWT refresh token 7 days true true None Same as above /

Important: SameSite=None + Secure=true means cookies are only sent over HTTPS and are included in cross-origin requests. During localhost development, the frontend must either use a proxy or enable chrome://flags/#allow-insecure-localhost.

Reading Role from the Token

Both the access and refresh tokens contain the role claim in their JWT payload. The frontend can decode the role from the access token like this:

// Decode role from token payload function getRoleFromToken(token) { const payload = JSON.parse(atob(token.split('.')[1])); return payload.role; // "siteowner" | "teacher" | "assistant" | "student" }

However, since access_token is httpOnly, JavaScript cannot read it via document.cookie. The role is always available from the login response (POST /accounts/login/) and from GET /accounts/me/. Use GET /accounts/me/ for session restoration on page refresh — it returns {role, name, is_active, student_code, status}. For students, student_code is the 7-digit student identifier and status is (pending, verified, declined, suspended_temporary, suspended_permanent).

Production domain setup: Set the environment variable JWT_AUTH_COOKIE_DOMAIN=.edutrackeg.com on the server so cookies are shared across all subdomains (frontend online.edutrackeg.com + backend apionline.edutrackeg.com). If unset, cookies are scoped to the exact host that set them.

Common Errors

These error formats are shared across all endpoints:

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

Pagination

List endpoints that support pagination return this wrapper by default:

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

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

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

Query Parameters:

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

Appendix A: Permission Matrix

Endpoint SiteOwner Teacher Assistant Student Public
POST /accounts/login/ Yes Yes Yes Yes Yes
GET /accounts/me/ Yes Yes Yes Yes No
POST /accounts/student/register/ No No No No Yes
GET /accounts/teachers/ Yes No No No No
POST /accounts/teachers/ Yes No No No No
GET /accounts/teachers/<id>/ Yes No No No No
POST /accounts/teachers/<id>/create-collection/ Yes No No No No
GET /accounts/video-security/ Yes No No No No
GET /accounts/students/ Yes No No No No
GET /accounts/profile/me/ No No No Yes No
GET /accounts/subjects/ Yes Yes Yes Yes Yes
GET /accounts/public/teachers/ Yes Yes Yes Yes Yes
GET /accounts/public/stats/ Yes Yes Yes Yes Yes
GET /courses/ Yes Yes Yes Yes No
GET /courses/public/ Yes Yes Yes Yes Yes
GET /courses/public/<id>/ Yes Yes Yes Yes Yes
POST /courses/ Yes No No No No
GET /courses/<id>/ Yes Yes Yes Yes No
GET /courses/<id>/preview/ Yes Yes Yes Yes No

| GET /courses/<id>/lectures/ | No | No | No | Yes* | No | | GET /courses/lectures/<id>/ | Yes | Yes | Yes | Yes* | No | | GET /courses/topics/ | Yes | Yes | Yes | Yes | No | | GET /courses/videos/ | Yes | Yes | Yes | Yes | No | | GET /courses/videos/<id>/ | Yes | Yes | Yes | Yes | No | | GET /courses/videos/<id>/play/ | Yes | Yes | Yes | Yes* | No | | POST /courses/videos/<id>/create-upload/ | No | Yes | Yes | No | No | | POST /courses/enrollments/enroll/ | No | No | No | Yes | No | | POST /courses/enrollments/approve/ | No | Yes | Yes | No | No | | POST /courses/enrollments/reject/ | No | Yes | Yes | No | No | | POST /courses/enrollments/<id>/block/ | Yes | Yes | Yes | No | No | | POST /courses/enrollments/<id>/unblock/ | Yes | Yes | Yes | No | No | | GET /courses/purchases/ | Yes | Yes | Yes | Yes | No | | GET /courses/my-lectures/ | No | No | No | Yes | No | | GET /courses/student/dashboard/ | No | No | No | Yes | No | | POST /courses/purchases/buy/ | No | No | No | Yes | No | | PATCH /courses/purchases/<id>/reopen/ | No | Yes** | Yes** | No | No | | POST /courses/purchases/<id>/start-watching/ | No | No | No | Yes* | No | | GET /courses/purchases/<id>/active-session/ | No | No | No | Yes* | No | | GET /courses/progress/ | No | No | No | Yes* | No | | POST /courses/progress/update/ | No | No | No | Yes* | No | | GET /courses/progress/<video_id>/ | No | No | No | Yes* | No | | GET /courses/teacher/dashboard/ | No | Yes | Yes | No | No | | GET /learning/homeworks/ | Yes | Yes | Yes | Yes* | No | | POST /learning/homeworks/ | Yes | Yes | Yes | No | No | | GET /learning/homeworks/<id>/submissions/ | No | Yes | Yes | No | No | | POST /learning/homeworks/<id>/submit/ | No | No | No | Yes* | No | | GET /learning/quizzes/ | Yes | Yes | Yes | Yes* | No | | POST /learning/quizzes/ | Yes | Yes | Yes | No | No | | GET /learning/quizzes/<id>/ | Yes | Yes | Yes | Yes* | No | | POST /learning/quizzes/<id>/start/ | No | No | No | Yes* | No | | POST /learning/quizzes/<id>/submit/ | No | No | No | Yes* | No | | GET /learning/quizzes/<id>/results/ | No | Yes | Yes | No | No | | POST /learning/quizzes/<id>/release-scores/ | No | Yes | Yes | No | No | | POST /learning/quizzes/<id>/unrelease-scores/ | No | Yes | Yes | No | No | | POST /learning/quizzes/<id>/release-answers/ | No | Yes | Yes | No | No | | POST /learning/quizzes/<id>/unrelease-answers/ | No | Yes | Yes | No | No | | POST /learning/quiz-submissions/<id>/grade-written/ | No | Yes | Yes | No | No | | DELETE /learning/quiz-submissions/<pk>/delete/ | No | Yes** | Yes** | No | No | | GET /learning/exams/ | Yes | Yes | Yes | Yes* | No | | POST /learning/exams/ | Yes | Yes | Yes | No | No | | GET /learning/exams/<id>/ | Yes | Yes | Yes | Yes* | No | | POST /learning/exams/<id>/start/ | No | No | No | Yes* | No | | POST /learning/exams/<id>/submit/ | No | No | No | Yes* | No | | GET /learning/exams/<id>/results/ | No | Yes | Yes | No | No | | POST /learning/exams/<id>/release-scores/ | No | Yes | Yes | No | No | | POST /learning/exams/<id>/unrelease-scores/ | No | Yes | Yes | No | No | | POST /learning/exams/<id>/release-answers/ | No | Yes | Yes | No | No | | POST /learning/exams/<id>/unrelease-answers/ | No | Yes | Yes | No | No | | POST /learning/exam-submissions/<id>/grade-written/ | No | Yes | Yes | No | No | | DELETE /learning/exam-submissions/<pk>/delete/ | No | Yes** | Yes** | No | No | | GET /materials/ | Yes | Yes | Yes | Yes* | No | | POST /materials/ | No | Yes | Yes | No | No | | GET /materials/<id>/ | Yes | Yes | Yes | Yes* | No | | PUT /materials/<id>/ | No | Yes | Yes | No | No | | DELETE /materials/<id>/ | No | Yes | Yes | No | No | | GET /payments/balance/ | Yes | Yes | Yes | Yes | No | | GET /payments/transactions/ | Yes | Yes | Yes | Yes | No | | POST /payments/codes/ | Yes | Yes | No (List only) | No | No | | POST /payments/codes/redeem/ | No | No | No | Yes | No | | POST /payments/recharge/ | No | Yes | Yes | No | No | | GET /payments/codes/history/ | Yes | Yes | Yes | No | No | | GET /payments/codes/export/ | Yes | Yes | Yes | No | No | | POST /payments/codes/request-blacklist-otp/ | Yes | No | No | No | No | | POST /payments/codes/batch-blacklist/ | Yes | No | No | No | No | | GET /payments/cuts/overview/ | Yes | Yes | Yes | No | No | | GET /payments/cuts/lectures/ | Yes | No | No | No | No | | GET /payments/cuts/invoices/ | Yes | No | No | No | No | | POST /payments/cuts/invoices/ | Yes | No | No | No | No | | GET /payments/cuts/invoices/<id>/ | Yes | No | No | No | No | | PATCH /payments/cuts/invoices/<id>/ | Yes | No | No | No | No | | DELETE /payments/cuts/invoices/<id>/ | Yes | No | No | No | No | | GET /payments/cuts/my/ | No | Yes | No | No | No | | GET /courses/<id>/analytics/ | No | Yes | Yes | No | No | | GET /courses/<id>/balance/ | No | No | No | Yes | No | | GET /courses/lectures/<pk>/available-assessments/ | Yes | Yes | Yes | Yes | No | | GET /courses/lectures/<pk>/prerequisites/ | Yes | Yes | Yes | Yes | No | | GET /courses/lectures/<pk>/students-progress/ | No | Yes | Yes | No | No | | GET /courses/<course_id>/students/<student_id>/lectures/ | No | Yes | Yes | No | No | | GET /learning/quizzes/<id>/resume/ | No | No | No | Yes | No | | PATCH /learning/quiz-answers/<id>/save-draft/ | No | No | No | Yes | No | | PATCH /learning/quiz-choices/<id>/ | No | Yes | Yes | No | No | | PATCH /learning/exam-choices/<id>/ | No | Yes | Yes | No | No | | GET /learning/exams/<id>/resume/ | No | No | No | Yes | No | | GET /learning/quizzes/<pk>/written-answers/ | No | Yes | Yes | No | No | | GET /learning/exams/<pk>/written-answers/ | No | Yes | Yes | No | No |

* Must be enrolled and approved in the relevant course. GET /courses/ and GET /courses/<id>/ require authentication in ALL cases (even with ?teacher=); anonymous course browsing is served by the public SEO endpoints GET /courses/public/ + GET /courses/public/<id>/ (no prices/videos) and GET /accounts/public/stats/. Students see only their enrolled courses' exams. Quizzes are multi-attempt (max_attempts in settings); exams are single-attempt.
** Public users see only active courses
** Must own the course (or be the course teacher's assistant)
Cut-invoice endpoints are SiteOwner-only; GET /payments/cuts/my/ is Teacher-only (teachers see only their own invoices).


Appendix B: Enum Reference

User Roles