Version: 1.3
Date: July 16, 2026
Target Audience: Next.js Frontend Developers
Backend: Django REST Framework
https://apionline.edutrackeg.com
The API uses JWT tokens stored in HTTP-Only cookies.
POST /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 /accounts/token/refresh/. The server reads the refresh token from the cookie, generates new tokens, and blacklists the old refresh token.POST /accounts/logout/. The server blacklists the refresh token and clears both cookies.Inactive User Blocking:
Single-Session Enforcement (Students Only):
token_version — all previously issued tokens (access + refresh) are immediately invalidated.401 with {"error": "Session invalidated by a new login on another device."}CSRF / Cross-Origin Notes:
Origin (or Referer) header against CSRF_TRUSTED_ORIGINS for cookie-based auth.CSRF_TRUSTED_ORIGINS on the backend (https://online.edutrackeg.com is configured).CSRF Failed: CSRF cookie not set, the origin is not trusted.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.
{
"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)"
}
{
"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)"
}
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.
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.
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"} |
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 |
| 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/andGET /courses/<id>/require authentication in ALL cases (even with?teacher=); anonymous course browsing is served by the public SEO endpointsGET /courses/public/+GET /courses/public/<id>/(no prices/videos) andGET /accounts/public/stats/. Students see only their enrolled courses' exams. Quizzes are multi-attempt (max_attemptsin 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).
siteowner — System ownerteacher — Course creator and managerassistant — Teacher's assistantstudent — Content consumer