System Atlas
Teacher Journey — 50 Endpoints
Complete API reference grouped by domain. Every endpoint with real request/response examples.
Register Account
Create a teacher account. Returns tokens immediately but email must be verified before login.
Request / Response
{
"fullName": "Ahmed Hassan",
"email": "ahmed@gmail.com",
"username": "ahmed_h",
"password": "Teacher@123",
"phoneNumber": "+201012345678",
"dateOfBirth": "1990-05-15T00:00:00",
"role": "Teacher"
}
{
"data": {
"accessToken": "eyJhbGciOiJIUzI1NiIs...",
"refreshToken": "dGhpcyBpcyBhIHJlZnJl...",
"expiresAt": "2026-09-12T12:15:00Z",
"role": "Teacher",
"userId": "b0000000-0000-00:00:00-000000000001"
},
"meta": { "timestamp": "...", "requestId": "..." }
}
Rate Limit: 10 requests/minute.
Auth: None (public).
Response Fields: nationalId, subscriptionPlan (string like "Basic"/"Premium"), subscriptionEnd (nullable DateTime), bio (nullable), rating (byte 0-5), subjectName (from linked Subject).
Errors: 401 (no/invalid token), 403 (wrong role).
Enums:
·
role: "Teacher" | "Student" | "Parent" — Admin cannot self-register.Validation:
·
fullName: required, max 100 chars.·
email: required, valid format, must be unique.·
username: required, 3-50 chars, must be unique.·
password: required, min 8 chars.·
phoneNumber: required, E.164 format regex ^\+?\d{7,15}$.·
dateOfBirth: required, must be in the past.Errors: 400 (validation), 409 (duplicate email/username).
Verify Email
Enter the 6-digit numeric code sent to your email. Code expires in 2 minutes.
Request / Response
{ "email": "ahmed@gmail.com", "code": "482916" }
{ "data": { "message": "success.emailVerified" }, "meta": { "timestamp": "...", "requestId": "..." } }
Rate Limit: 10 requests/minute per IP.
Validation:
·
email: required, valid format, must match a pending verification.·
code: required, exactly 6 numeric digits (^[0-9]{6}$).Errors: 400 (validation), 401 (expired/invalid code — code expires in 2 minutes).
Resend: Call POST /auth/resend-verification with { "email": "..." } to get a new code.
Login
Authenticate with email + password. Returns JWT access (15min) + refresh (7 days) tokens.
Request / Response
{ "email": "ahmed@gmail.com", "password": "Teacher@123" }
{
"data": {
"accessToken": "eyJhbGciOiJIUzI1NiIs...",
"refreshToken": "dGhpcyBpcyBhIHJlZnJl...",
"expiresAt": "2026-09-12T12:15:00Z",
"role": "Teacher",
"userId": "b0000000-0000-0000-0000-000000000001"
},
"meta": { "timestamp": "...", "requestId": "..." }
}
Rate Limit: 10 requests/minute per IP.
Validation:
·
email: required, valid format.·
password: required, min 6 chars (note: registration requires 8, login only 6 for backward compat).Errors: 400 (validation), 401 (bad credentials OR unverified email).
Token Lifecycle: Access token = 15 minutes. Refresh token = 7 days. Use Bearer token in Authorization header for all subsequent requests. Refresh via POST /auth/refresh-token when access expires.
Get My Profile
View your full teacher profile including subscription status.
Request / Response
{
"data": {
"id": "b0000000-0000-0000-0000-000000000001",
"fullName": "Ahmed Hassan",
"email": "ahmed@gmail.com",
"username": "ahmed_h",
"phoneNumber": "+201012345678",
"dateOfBirth": "1990-05-15T00:00:00",
"nationalId": "29005151234567",
"subscriptionPlan": "Premium",
"subscriptionEnd": "2027-09-12T00:00:00Z",
"bio": "Biology teacher with 10 years experience",
"rating": 5,
"subjectName": "Biology",
"createdAt": "2026-01-15T00:00:00Z"
},
"meta": { "timestamp": "...", "requestId": "..." }
}
Rate Limit: 30 req/min.
Query: search (optional, matches name/email), filter ("my-students"|"available"), page (default 1), pageSize (default 20, max 100).
Behavior: "my-students" = students enrolled in your groups. "available" = students not yet in any of your groups. No filter = all students visible to you.
Errors: 401 (no token), 403 (wrong role).
Update My Profile
Update your name, phone, bio, and subscription plan.
Request / Response
{
"fullName": "Ahmed Hassan",
"phoneNumber": "+201012345678",
"bio": "Biology teacher — 10 years experience",
"subscriptionPlan": "Premium"
}
Rate Limit: 30 req/min.
Validation: fullName (required, max 100), phoneNumber (required, E.164 regex). Email change triggers re-verification (old email stays active until new one is verified).
Errors: 400 (validation), 401 (no token), 403 (wrong role), 409 (duplicate email if changed).
Teacher Dashboard
Full dashboard: profile, overview stats, week-over-week trends, top/bottom groups, recent uploads, top students, platform comparison, students at risk. Fixed-size summary endpoint — no pagination. Cached 2 minutes.
Request / Response
{
"data": {
"profile": { "fullName": "Ahmed Hassan", "email": "ahmed@gmail.com", "subject": "Biology", "subscriptionPlan": "Premium" },
"overview": {
"totalGroups": 8, "activeGroups": 6, "totalStudents": 142,
"totalSessionsThisMonth": 18, "pendingPaymentsEGP": 3500.00,
"totalUploadsThisMonth": 12, "scoreTrend": 4.5,
"topPerformingGroup": "Biology - Secondary 2", "topPerformingGroupScore": "88.5%",
"studentsAtRiskCount": 5, "uploadStreakWeeks": 6,
"classAverageScore": 76.3, "totalSessionsAllTime": 234
},
"trends": {
"scoreChangePercent": 5.2, "attendanceChangePercent": 3.1,
"studentsChange": 8, "scoreTrend": "up", "attendanceTrend": "up"
},
"topGroups": {
"top": [
{ "groupId": "...", "name": "Biology - Secondary 2", "subject": "Biology", "gradeLevel": "SecondaryTwo", "period": "First Term", "studentCount": 45, "averageScore": 88.5, "isActive": true, "attendanceRate": 94.0, "trend": "up" }
],
"bottom": [
{ "groupId": "...", "name": "Math - Secondary 1", "subject": "Mathematics", "gradeLevel": "SecondaryOne", "period": "First Term", "studentCount": 22, "averageScore": 54.2, "isActive": true, "attendanceRate": 72.0, "trend": "down" }
]
},
"recentUploads": [
{ "uploadId": "...", "fileName": "session_2026-09-10.xlsx", "groupName": "Biology - Secondary 2", "status": "Completed", "rowsProcessed": 45, "uploadedAt": "2026-09-12T14:30:00Z" }
],
"topStudents": [
{ "gradeLevel": "SecondaryTwo", "groupName": "Biology - Secondary 2", "students": [
{ "studentId": "...", "fullName": "Ali Hassan", "studentCode": "STU-003", "rank": 1, "avgScore": 95.5, "attendanceRate": 100.0 }
]}
],
"unreadNotifications": 3,
"needsAttention": [
{ "studentId": "...", "fullName": "Omar Ali", "groupName": "Math - Secondary 1", "reason": "Low score", "currentScore": 38.0, "attendanceRate": 55.0 }
],
"platformComparison": { "yourAverageScore": 76.3, "schoolAverageScore": 71.0, "yourRank": 3, "totalTeachers": 15, "performanceLabel": "Above Average" }
},
"meta": { "timestamp": "...", "requestId": "..." }
}
Rate Limit: 30 req/min.
Caching: IMemoryCache, 2 minutes TTL. Cache key:
teacher_home_{userId}.Response Fields: profile, overview, trends (week-over-week), topGroups (top 2 + bottom 2 by average score), recentUploads (last 3 uploads with status), topStudents (grouped by grade level, top 3 per grade from best group), unreadNotifications, needsAttention (nullable — students at risk), platformComparison (nullable).
No pagination — this is a fixed-size summary endpoint.
Errors: 401 (no token), 403 (wrong role).
Dashboard Trends
Score and attendance change percentages for sparkline charts.
Request / Response
{
"data": {
"scoreChangePercent": 5.2,
"attendanceChangePercent": 3.1,
"studentsChange": 8,
"scoreTrend": "up",
"attendanceTrend": "up"
},
"meta": { "timestamp": "...", "requestId": "..." }
}
Rate Limit: 30 req/min.
Response Fields: scoreChangePercent (double, positive = improvement), attendanceChangePercent, studentsChange (int, net change), scoreTrend ("up"|"down"|"stable"), attendanceTrend ("up"|"down"|"stable").
Use Case: Drive sparkline charts on the dashboard. Compare with previous period to show trends.
Errors: 401 (no token), 403 (wrong role).
Create Group
Define group name, grade level, subject, period, and optional weekly schedules.
Request / Response
{
"name": "Biology - Secondary 2",
"academicYear": "2025-2026",
"gradeLevel": 2,
"period": "First Term",
"subjectId": "a1b2c3d4-5678-9abc-def0-1234567890ab",
"schedules": [
{ "dayOfWeek": "Monday", "startTime": "14:00", "endTime": "16:00" },
{ "dayOfWeek": "Wednesday", "startTime": "14:00", "endTime": "16:00" }
]
}
{
"data": {
"id": "c5d6e7f8-9abc-def0-1234-567890abcdef",
"name": "Biology - Secondary 2",
"academicYear": "2025-2026",
"gradeLevel": "SecondaryTwo",
"period": "First Term",
"isActive": true,
"teacherId": "b0000000-0000-0000-0000-000000000001",
"subjectName": "Biology",
"createdAt": "2026-09-12T10:00:00Z",
"studentCount": 0, "averageScore": 0, "attendanceRate": 0, "activeSessions": 0,
"schedules": [
{ "id": "...", "dayOfWeek": "Monday", "startTime": "14:00", "endTime": "16:00", "isActive": true }
]
},
"meta": { "timestamp": "...", "requestId": "..." }
}
Rate Limit: 30 req/min.
Enums:
·
gradeLevel: 1=SecondaryOne, 2=SecondaryTwo, 3=SecondaryThree. Must be integer, not string.·
period: "First Term" | "Second Term". Restricted by CHECK constraint in DB.·
dayOfWeek (in schedules): "Monday" | "Tuesday" | "Wednesday" | "Thursday" | "Friday" | "Saturday" | "Sunday".Validation: name (required, max 200 chars), academicYear (required, e.g. "2025-2026"), subjectId (required, must be valid GUID).
Errors: 400 (validation), 401 (no token), 403 (wrong role), 404 (subject not found).
List My Groups
Paginated list of all your groups with stats.
Request / Response
gradeLevel: string? (filter by grade) page: int = 1 pageSize: int = 20
{
"data": {
"items": [
{
"id": "c5d6e7f8-...", "name": "Biology - Secondary 2",
"academicYear": "2025-2026", "gradeLevel": "SecondaryTwo",
"period": "First Term", "isActive": true,
"subjectName": "Biology", "studentCount": 50,
"averageScore": 88.5, "attendanceRate": 94.0,
"activeSessions": 3, "lastUploadDate": "2026-09-12T14:30:00Z",
"trend": "up",
"schedules": [
{ "id": "...", "dayOfWeek": "Monday", "startTime": "14:00", "endTime": "16:00", "isActive": true }
]
}
],
"page": 1, "pageSize": 20, "totalCount": 5
},
"meta": { "timestamp": "...", "requestId": "..." }
}
Get Group Detail
Single group with full details including schedules.
Request / Response
Rate Limit: 30 req/min.
Errors: 401 (no token), 403 (not your group), 404 (group not found).
Update Group
Update group name, grade, period, subject. Schedules are managed separately.
Request / Response
{
"name": "Biology - Secondary 2 (Updated)",
"academicYear": "2025-2026",
"gradeLevel": 2,
"period": "Second Term",
"subjectId": "a1b2c3d4-5678-9abc-def0-1234567890ab"
}
Delete Group
Soft-delete: sets IsActive = false. Preserves all data.
Request / Response
{ "data": { "message": "Group deleted successfully" }, "meta": { "timestamp": "...", "requestId": "..." } }
Rate Limit: 30 req/min.
Behavior: Soft-delete — sets IsActive = false. All sessions, students, and data are preserved. Group disappears from default list queries but can still be accessed by ID.
Errors: 401 (no token), 403 (not your group), 404 (group not found).
Duplicate Group
Clone a group with its schedules. Students are NOT copied.
Request / Response
{ "name": "Biology - Secondary 2 (Copy)" }
{
"data": {
"id": "...", "name": "Biology - Secondary 2 (Copy)",
"academicYear": "2025-2026", "gradeLevel": "SecondaryTwo",
"period": "First Term", "subjectId": "a1b2c3d4-..."
},
"meta": { "timestamp": "...", "requestId": "..." }
}
Rate Limit: 30 req/min.
Behavior: Clones group metadata + schedules. Does NOT copy students, sessions, or upload logs. New group is independent.
Errors: 400 (validation), 401 (no token), 403 (not your group), 404 (source group not found).
Add Schedules to Group
Add weekly time slots to an existing group.
Request / Response
[
{ "dayOfWeek": "Tuesday", "startTime": "15:00", "endTime": "17:00" },
{ "dayOfWeek": "Thursday", "startTime": "15:00", "endTime": "17:00" }
]
{
"data": [
{ "id": "...", "dayOfWeek": "Tuesday", "startTime": "15:00", "endTime": "17:00", "isActive": true },
{ "id": "...", "dayOfWeek": "Thursday", "startTime": "15:00", "endTime": "17:00", "isActive": true }
],
"meta": { "timestamp": "...", "requestId": "..." }
}
Rate Limit: 30 req/min.
Enums: dayOfWeek: "Monday"|"Tuesday"|"Wednesday"|"Thursday"|"Friday"|"Saturday"|"Sunday". startTime/endTime: "HH:mm" format (24h).
Errors: 400 (validation), 401 (no token), 403 (not your group), 404 (group not found).
Update Schedule
Change day or time of an existing schedule slot.
Request / Response
{ "dayOfWeek": 1, "startTime": "16:00", "endTime": "18:00" }
Rate Limit: 30 req/min.
Enums: dayOfWeek: 0=Sunday, 1=Monday, ..., 6=Saturday (System.DayOfWeek integer enum).
Errors: 400 (validation), 401 (no token), 403 (not your schedule), 404 (schedule not found).
Delete Schedule
Remove a weekly time slot from a group.
Request / Response
Rate Limit: 30 req/min.
Behavior: Permanently deletes the schedule slot. Cannot be undone.
Errors: 401 (no token), 403 (not your schedule), 404 (schedule not found).
List Students
Browse students visible to you. Filter by "my-students" or "available".
Request / Response
search: string? (search by name/email)
filter: string? ("my-students" | "available")
page: int = 1
pageSize: int = 20
{
"data": {
"items": [
{
"studentId": "d4e5f6a7-89ab-cdef-0123-456789abcdef",
"fullName": "Sara Mohamed",
"email": "sara@student.com",
"studentCode": "STU-001",
"isEnrolled": false,
"enrolledGroups": null,
"stats": { "averageScore": 0, "attendanceRate": 0, "totalSessions": 0, "attendedSessions": 0, "assessmentCount": 0, "lastActivityDate": null, "scoreTrend": "stable" }
}
],
"page": 1, "pageSize": 20, "totalCount": 45
},
"meta": { "timestamp": "...", "requestId": "..." }
}
Look Up Student by Code
Find a specific student using their unique student code.
Request / Response
Enroll Student by ID
Add a student to a group by their GUID.
Request / Response
{ "studentId": "d4e5f6a7-89ab-cdef-0123-456789abcdef" }
{
"data": {
"studentId": "d4e5f6a7-...",
"fullName": "Sara Mohamed",
"email": "sara@student.com",
"studentCode": "STU-001",
"isEnrolled": true,
"enrolledGroups": [{ "groupId": "c5d6e7f8-...", "groupName": "Biology - Secondary 2" }],
"stats": { "averageScore": 0, "attendanceRate": 0, "totalSessions": 0 }
},
"meta": { "timestamp": "...", "requestId": "..." }
}
Rate Limit: 30 req/min.
Behavior: Creates a StudentsGroups join record. If student has no StudentCode yet, one is auto-generated (format: "STU-{nnn}").
Errors: 400 (validation), 401 (no token), 403 (not your group), 404 (student not found), 409 (already enrolled).
Enroll Student by Code
Add a student to a group using their student code. No request body needed.
Request / Response
Get Student Progress
Detailed progress: attendance, scores, per-group breakdown.
Request / Response
{
"data": {
"studentId": "d4e5f6a7-...",
"fullName": "Sara Mohamed",
"overallAttendancePercentage": 92.0,
"overallAverageWrittenScore": 85.5,
"overallAverageAssignmentScore": 88.0,
"overallProgressPercentage": 87.0,
"totalSessionsAcrossAllGroups": 25,
"perGroupProgress": [
{
"studentName": "Sara Mohamed",
"totalSessions": 12,
"attendedSessions": 11,
"attendancePercentage": 91.7,
"averageWrittenScore": 86.0,
"averageAssignmentScore": 89.0,
"overallProgressPercentage": 87.5,
"recentAssessments": [
{ "examName": "written_exam", "topic": "Mitosis", "scorePercentage": 90, "maxScore": 50, "sessionDate": "2026-09-12T14:00:00" }
]
}
]
},
"meta": { "timestamp": "...", "requestId": "..." }
}
Get Student's Parent Contact
Retrieve parent name, email, and phone for a student.
Request / Response
{
"data": {
"parentId": "f1a2b3c4-...",
"fullName": "Mohamed Ali",
"email": "mohamed@parent.com",
"phoneNumber": "+201055512345"
},
"meta": { "timestamp": "...", "requestId": "..." }
}
Unenroll Student
Remove a student from a group. Preserves historical data.
Request / Response
{ "data": { "message": "Student unenrolled successfully" }, "meta": { "timestamp": "...", "requestId": "..." } }
Rate Limit: 30 req/min.
Behavior: Removes the StudentsGroups join record. Historical session data (StudentSessions, Assessments) is preserved.
Errors: 401 (no token), 403 (not your group), 404 (student not in group).
Approve Student
Re-activate a previously deactivated student.
Request / Response
{ "data": { "message": "Student approved successfully" }, "meta": { "timestamp": "...", "requestId": "..." } }
Rate Limit: 30 req/min.
Behavior: Sets Student.IsActive = true. Student can log in again and appears in group lists.
Errors: 401 (no token), 403 (wrong role), 404 (student not found).
Export Students as Excel
Download a spreadsheet of all students in a group with their stats.
Request / Response
Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet Content-Disposition: attachment; filename="students_c5d6e7f8.xlsx"
List All Sessions
All sessions across your groups, paginated.
Request / Response
{
"data": {
"items": [
{
"sessionId": "e7f8a9b0-...",
"groupName": "Biology - Secondary 2",
"sessionDate": "2026-09-12T14:00:00",
"sessionType": "Quiz",
"attended": true,
"score": 45,
"maxScore": 50,
"scorePercentage": 90.0,
"comment": null
}
],
"page": 1, "pageSize": 20, "totalCount": 156
},
"meta": { "timestamp": "...", "requestId": "..." }
}
Rate Limit: 30 req/min.
Query: page (default 1), pageSize (default 20, max 100).
Behavior: Returns sessions across all groups owned by this teacher. Sorted by sessionDate descending (newest first).
Errors: 401 (no token), 403 (wrong role).
Create Session
Create a session with optional Excel header definitions. Headers define which columns your upload Excel will have.
Request / Response
{
"sessionDate": "2026-09-12T14:00:00",
"sessionType": 1,
"topic": "Cell Division - Mitosis",
"description": "Chapter 3 review",
"headers": [
{ "key": "attendance", "label": "Attendance", "labelArabic": "الحضور", "columnType": 0, "sortOrder": 0, "isRequired": true },
{ "key": "written_exam", "label": "Written Exam", "labelArabic": "الامتحان الكتابي", "columnType": 1, "maxScore": 50, "sortOrder": 1, "isRequired": true },
{ "key": "oral_quiz", "label": "Oral Quiz", "labelArabic": "الامتحان الشفهي", "columnType": 1, "maxScore": 20, "sortOrder": 2, "isRequired": false },
{ "key": "comment", "label": "Comment", "labelArabic": "ملاحظة", "columnType": 2, "sortOrder": 3, "isRequired": false }
]
}
{
"data": {
"id": "e7f8a9b0-1234-5678-9abc-def012345678",
"sessionDate": "2026-09-12T14:00:00",
"sessionType": 1,
"topic": "Cell Division - Mitosis",
"description": "Chapter 3 review",
"createdAt": "2026-09-12T10:30:00Z",
"headers": [
{ "id": "...", "key": "attendance", "label": "Attendance", "labelArabic": "الحضور", "columnType": 0, "maxScore": null, "sortOrder": 0, "isRequired": true },
{ "id": "...", "key": "written_exam", "label": "Written Exam", "labelArabic": "الامتحان الكتابي", "columnType": 1, "maxScore": 50, "sortOrder": 1, "isRequired": true },
{ "id": "...", "key": "oral_quiz", "label": "Oral Quiz", "labelArabic": "الامتحان الشفهي", "columnType": 1, "maxScore": 20, "sortOrder": 2, "isRequired": false },
{ "id": "...", "key": "comment", "label": "Comment", "labelArabic": "ملاحظة", "columnType": 2, "maxScore": null, "sortOrder": 3, "isRequired": false }
]
},
"meta": { "timestamp": "...", "requestId": "..." }
}
Rate Limit: 30 req/min.
Enums:
·
sessionType: 0=Exam, 1=Quiz, 2=Homework, 3=Oral, 4=Attendance. Integer, not string.·
columnType (in headers): 0=Attendance, 1=Score, 2=Comment. Integer, not string.Validation: sessionDate (required), topic (required), description (optional). Headers array items: key (required, unique per session), label (required), columnType (required), sortOrder (required, 0-based), isRequired (default false). maxScore required when columnType=1 (Score).
Behavior: If headers omitted, defaults to Attendance + Written 60 + Assignment 20 + Comment. Use copyHeadersFromSessionId to reuse headers from a previous session (headers param is ignored if this is set).
Errors: 400 (validation), 401 (no token), 403 (wrong role), 404 (group not found).
Get Session Detail
Full session with all student sessions and assessment breakdowns.
Request / Response
{
"data": {
"sessionId": "e7f8a9b0-...",
"sessionDate": "2026-09-12T14:00:00",
"sessionType": "Quiz",
"topic": "Cell Division - Mitosis",
"description": "Chapter 3 review",
"groupName": "Biology - Secondary 2",
"subjectName": "Biology",
"teacherName": "Ahmed Hassan",
"studentSessions": [
{
"studentSessionId": "...",
"studentId": "d4e5f6a7-...",
"studentName": "Sara Mohamed",
"studentCode": "STU-001",
"attended": true,
"assessments": [
{ "assessmentId": "...", "examName": "written_exam", "topic": "", "maxScore": 50, "scorePercentage": 45, "teacherComment": "Good work" },
{ "assessmentId": "...", "examName": "oral_quiz", "topic": "", "maxScore": 20, "scorePercentage": 18, "teacherComment": "Excellent" }
]
}
]
},
"meta": { "timestamp": "...", "requestId": "..." }
}
Rate Limit: 30 req/min.
Response: Full session with all StudentSessions and their Assessments. Includes group name, subject name, teacher name.
Errors: 401 (no token), 403 (not your session), 404 (session not found).
Update Session
Change session date, topic, description, or type.
Request / Response
{
"sessionDate": "2026-09-13T14:00:00",
"topic": "Cell Division - Meiosis",
"description": "Updated topic",
"sessionType": 0
}
{
"data": {
"id": "e7f8a9b0-...",
"sessionDate": "2026-09-13T14:00:00",
"sessionType": 0,
"topic": "Cell Division - Meiosis",
"description": "Updated topic",
"createdAt": "2026-09-12T10:30:00Z"
},
"meta": { "timestamp": "...", "requestId": "..." }
}
Rate Limit: 30 req/min.
Validation: All fields optional. Only provided fields are updated (partial update). sessionType uses same enum as create (0-4).
Errors: 400 (validation), 401 (no token), 403 (not your session), 404 (session not found).
Delete Session
Permanently delete a session and all its assessments.
Request / Response
Mark Attendance
Bulk mark attendance for all students in a session.
Request / Response
{
"attendances": [
{ "studentId": "d4e5f6a7-...", "attended": true },
{ "studentId": "a1b2c3d4-...", "attended": false },
{ "studentId": "e5f6a7b8-...", "attended": true }
]
}
Rate Limit: 30 req/min.
Behavior: Bulk operation. Creates or updates StudentSession records with the attended flag. Idempotent — calling again overwrites previous attendance.
Errors: 400 (validation), 401 (no token), 403 (not your session), 404 (session/student not found).
Get Session Headers
View the Excel column configuration for a session.
Request / Response
{
"data": [
{ "id": "...", "key": "attendance", "label": "Attendance", "labelArabic": "الحضور", "columnType": 0, "maxScore": null, "sortOrder": 0, "isRequired": true },
{ "id": "...", "key": "written_exam", "label": "Written Exam", "labelArabic": "الامتحان الكتابي", "columnType": 1, "maxScore": 50, "sortOrder": 1, "isRequired": true },
{ "id": "...", "key": "oral_quiz", "label": "Oral Quiz", "labelArabic": "الامتحان الشفهي", "columnType": 1, "maxScore": 20, "sortOrder": 2, "isRequired": false },
{ "id": "...", "key": "comment", "label": "Comment", "labelArabic": "ملاحظة", "columnType": 2, "maxScore": null, "sortOrder": 3, "isRequired": false }
],
"meta": { "timestamp": "...", "requestId": "..." }
}
Rate Limit: 30 req/min.
Response: Array of SessionHeaderDefinitionDto (not wrapped in pagination). Sorted by sortOrder ascending.
Errors: 401 (no token), 403 (not your session), 404 (session not found).
Update Session Headers
Change the Excel column configuration. Locked (409) once scores are uploaded.
Request / Response
{
"headers": [
{ "key": "attendance", "label": "Attendance", "columnType": 0, "sortOrder": 0, "isRequired": true },
{ "key": "written_exam", "label": "Written Exam", "columnType": 1, "maxScore": 50, "sortOrder": 1, "isRequired": true },
{ "key": "bonus", "label": "Bonus Points", "columnType": 1, "maxScore": 10, "sortOrder": 2, "isRequired": false }
],
"copyHeadersFromSessionId": null
}
{
"data": {
"sessionId": "e7f8a9b0-...",
"headers": [
{ "id": "...", "key": "attendance", "label": "Attendance", "columnType": 0, "maxScore": null, "sortOrder": 0, "isRequired": true },
{ "id": "...", "key": "written_exam", "label": "Written Exam", "columnType": 1, "maxScore": 50, "sortOrder": 1, "isRequired": true },
{ "id": "...", "key": "bonus", "label": "Bonus Points", "columnType": 1, "maxScore": 10, "sortOrder": 2, "isRequired": false }
]
},
"meta": { "timestamp": "...", "requestId": "..." }
}
Rate Limit: 30 req/min.
Lock Condition: Returns 409 Conflict if scores already exist for this session. Delete existing scores first or create a new session.
Alternative: Use copyHeadersFromSessionId in the request body to copy headers from another session instead of defining them manually.
Errors: 400 (validation), 401 (no token), 403 (wrong role), 404 (session not found), 409 (scores exist — headers locked).
Add Assessment
Add a score record for a student in a session. Creates or updates the assessment.
Request / Response
{
"studentId": "d4e5f6a7-89ab-cdef-0123-456789abcdef",
"examName": "written_exam",
"scorePercentage": 45,
"maxScore": 50,
"teacherComment": "Good understanding of mitosis phases"
}
{
"data": {
"id": "...",
"examName": "written_exam",
"topic": "",
"scorePercentage": 45,
"maxScore": 50,
"teacherComment": "Good understanding of mitosis phases"
},
"meta": { "timestamp": "...", "requestId": "..." }
}
Rate Limit: 30 req/min.
Validation: studentId (required, valid GUID), examName (required, must match a header key), scorePercentage (required, 0-100), maxScore (required, > 0).
Behavior: scorePercentage is stored as percentage (0-100), not raw score. The system calculates: rawScore = (scorePercentage / 100) * maxScore. Creates a new Assessment record linked to the StudentSession.
Errors: 400 (validation), 401 (no token), 403 (wrong role), 404 (session/student not found).
Update Assessment
Change score or comment on an existing assessment.
Request / Response
{ "scorePercentage": 48, "teacherComment": "Updated: excellent progress" }
{
"data": {
"id": "...", "examName": "written_exam", "topic": "",
"scorePercentage": 48, "teacherComment": "Updated: excellent progress"
},
"meta": { "timestamp": "...", "requestId": "..." }
}
Rate Limit: 30 req/min.
Validation: Both fields optional (partial update). scorePercentage: 0-100.
Errors: 400 (validation), 401 (no token), 403 (not your assessment), 404 (assessment not found).
Get Average Score
Quick summary: average, min, max scores and attendance for a session.
Request / Response
{
"data": {
"sessionId": "e7f8a9b0-...",
"sessionType": "Quiz",
"sessionDate": "2026-09-12T14:00:00",
"groupName": "Biology - Secondary 2",
"totalStudents": 50,
"attendedStudents": 46,
"attendanceRate": 92.0,
"averageScore": 82.8,
"minScore": 35,
"maxScore": 100,
"totalAssessments": 138
},
"meta": { "timestamp": "...", "requestId": "..." }
}
Rate Limit: 30 req/min.
Response: Lightweight summary — faster than /report. TotalAssessments = total assessment records (one per score column per student).
Errors: 401 (no token), 403 (not your session), 404 (session not found).
Download Template
Get an Excel template pre-filled with enrolled student names/codes AND your custom header columns.
Request / Response
groupId: uuid? (legacy mode: populates student names) sessionId: uuid? (session-bound mode: headers match session config)
Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet Content-Disposition: attachment; filename="template_20260912.xlsx"
Rate Limit: 5 req/min (stricter — file generation is expensive).
Modes:
· Session-bound (recommended): Pass sessionId. Template columns match session headers exactly. Student names/codes auto-filled from enrolled students.
· Legacy: Pass groupId only. Uses default headers (Attendance, Written 60, Assignment 20, Comment).
Errors: 401 (no token), 403 (wrong role), 404 (session/group not found).
Upload Excel
Fill the template with attendance + scores, then upload. Hangfire processes it async. Strategy: replace (overwrite), skip (keep existing), or error (reject if scores exist).
Request / Response
file: (binary .xlsx, max 10MB) groupId: c5d6e7f8-9abc-def0-1234-567890abcdef sessionId: e7f8a9b0-1234-5678-9abc-def012345678 (optional) assessmentDate: 2026-09-12T14:00:00 (optional) strategy: replace (error | replace | skip)
{
"data": {
"uploadId": "f9a0b1c2-3456-789a-bcde-f01234567890",
"fileName": "template_20260912.xlsx",
"status": "Queued",
"uploadedAt": "2026-09-12T14:30:00Z"
},
"meta": { "timestamp": "...", "requestId": "..." }
}
Rate Limit: 5 req/min (stricter — file upload is expensive).
Enums:
·
strategy: "error" | "replace" | "skip". Query parameter, not body.-
"error": Returns 409 if scores already exist for this session.-
"replace": Deletes all existing StudentSessions + Assessments, creates fresh from Excel.-
"skip": Keeps students who already have scores, only adds new students.Behavior: File is saved to disk at
uploads/excel/{yyyy}/{MM}/{dd}/{guid}_{filename}. Processing is async via Hangfire (ProcessExcelJob on "uploads" queue). Always returns status="Queued". Poll GET /upload-logs to check completion.Errors: 400 (invalid file), 401 (no token), 403 (wrong role), 409 (strategy=error and scores exist), 413 (file too large).
List Upload Logs
Paginated upload history with status, row counts, and group info.
Request / Response
{
"data": {
"items": [
{
"uploadId": "f9a0b1c2-...",
"fileName": "template_20260912.xlsx",
"fileSize": 24576,
"status": 2,
"uploadedAt": "2026-09-12T14:30:00Z",
"completedAt": "2026-09-12T14:30:15Z",
"rawProcessed": 50,
"rawFailed": 0,
"groupId": "c5d6e7f8-...",
"groupName": "Biology - Secondary 2"
}
],
"page": 1, "pageSize": 10, "totalCount": 8
},
"meta": { "timestamp": "...", "requestId": "..." }
}
Rate Limit: 30 req/min.
Enums:
·
status: 0=Queued, 1=Processing, 2=Completed, 3=Failed. Integer (UploadStatus enum).Polling: After upload, poll this endpoint every 2-3 seconds. Check if status changed from 0/1 to 2 or 3.
Errors: 401 (no token), 403 (wrong role).
Get Upload Log Detail
Single upload log with full details.
Request / Response
Retry Failed Upload
Re-queue a failed upload for processing.
Request / Response
Rate Limit: 30 req/min.
Behavior: Re-queues the same file for processing with the same strategy. Creates a new Hangfire job. The upload status resets to Queued (0).
Errors: 401 (no token), 403 (wrong role), 404 (upload log not found), 409 (status is not Failed).
Session Report
Professional report: column breakdown, highlights, chart series, needs-attention, comparison with previous session. Cached 1 minute.
Request / Response
{
"data": {
"session": {
"sessionId": "e7f8a9b0-...", "sessionDate": "2026-09-12T14:00:00",
"sessionType": "Quiz", "topic": "Cell Division - Mitosis",
"groupName": "Biology - Secondary 2", "subjectName": "Biology"
},
"analytics": {
"totalStudents": 50, "attendedStudents": 46, "attendanceRate": 92.0,
"averageScore": 82.8, "minScore": 35, "maxScore": 100, "medianScore": 85.0,
"passCount": 42, "failCount": 4, "passRate": 87.5,
"gradeDistribution": { "A": 31, "B": 33, "C": 21, "D": 7 }
},
"columnBreakdown": [
{ "key": "written_exam", "label": "Written Exam", "maxScore": 50, "average": 38.5, "min": 15, "max": 50, "passRate": 90.0, "assessedCount": 46 },
{ "key": "oral_quiz", "label": "Oral Quiz", "maxScore": 20, "average": 16.2, "min": 8, "max": 20, "passRate": 85.0, "assessedCount": 46 }
],
"highlights": {
"topStudents": [{ "studentId": "...", "studentName": "Ali Hassan", "studentCode": "STU-003", "average": 98.5, "delta": 3.2 }],
"atRisk": [{ "studentId": "...", "studentName": "Omar Ali", "studentCode": "STU-012", "average": 35.0, "delta": -5.0 }],
"mostImproved": [{ "studentId": "...", "studentName": "Fatma SA", "studentCode": "STU-008", "average": 88.0, "delta": 12.5 }],
"biggestDrop": []
},
"chartSeries": {
"histogram": [{ "bucket": "90-100", "count": 18 }, { "bucket": "80-89", "count": 15 }, { "bucket": "70-79", "count": 8 }, { "bucket": "60-69", "count": 5 }, { "bucket": "0-59", "count": 4 }],
"byColumn": [{ "label": "Written Exam", "average": 82.8 }, { "label": "Oral Quiz", "average": 81.0 }]
},
"needsAttention": [
{ "type": "low_attendance", "message": "4 students absent", "count": 4 },
{ "type": "failing_scores", "message": "4 students below 60%", "count": 4 }
],
"comparison": {
"previousAvgScore": 78.2, "currentAvgScore": 82.8, "scoreChange": 4.6,
"previousAttendanceRate": 88.0, "currentAttendanceRate": 92.0, "attendanceChange": 4.0,
"perColumnDelta": [
{ "key": "written_exam", "label": "Written Exam", "previousAverage": 35.0, "currentAverage": 38.5, "delta": 3.5 }
]
}
},
"meta": { "timestamp": "...", "requestId": "..." }
}
Rate Limit: 30 req/min.
Caching: IMemoryCache, 1 minute TTL. Cache key:
session_report_{sessionId}. Data is stale for up to 60s after new uploads.Response Fields: session (info), analytics (11 stats + gradeDistribution), columnBreakdown[] (per-column avg/min/max/passRate), highlights (topStudents/atRisk/mostImproved/biggestDrop — max 5 each), chartSeries (histogram 5 buckets + byColumn), needsAttention[] (max 5 items), comparison (nullable — null if no previous session for this group).
GradeDistribution Keys: "A" (90-100), "B" (80-89), "C" (70-79), "D" (60-69), "F" (below 60). Based on average score percentage.
Errors: 401 (no token), 403 (wrong role), 404 (session not found).
Export Session Report
Download session report as an Excel file.
Request / Response
Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet Content-Disposition: attachment; filename="report_e7f8a9b0.xlsx"
Rate Limit: 30 req/min.
Behavior: Generates Excel with attendance, scores per column, grade distribution. File is generated on-the-fly (not cached).
Errors: 401 (no token), 403 (not your session), 404 (session not found).
Export Students
Download all students in a group as Excel with their stats.
Request / Response
Rate Limit: 30 req/min.
Behavior: Generates Excel with student names, codes, enrollment dates, attendance rates, average scores. One row per student.
Errors: 401 (no token), 403 (not your group), 404 (group not found).
Group Leaderboard
Rankings for a specific group. Recomputed hourly by Hangfire.
Request / Response
{
"data": [
{
"rank": 1, "studentId": "d4e5f6a7-...",
"studentName": "Ali Hassan", "studentCode": "STU-003",
"weightedScore": 95.5, "avgScore": 92.0, "attendanceRate": 100.0, "participationScore": 10.0,
"context": {
"totalStudents": 50, "scoreDifferenceToTop": 0,
"averageScore": 82.8, "topScore": 95.5,
"isTopPerformer": true, "rankLabel": "#1"
}
},
{
"rank": 2, "studentId": "...",
"studentName": "Sara Mohamed", "studentCode": "STU-001",
"weightedScore": 91.2, "avgScore": 88.0, "attendanceRate": 95.0, "participationScore": 9.0,
"context": { "totalStudents": 50, "scoreDifferenceToTop": 4.3, "averageScore": 82.8, "topScore": 95.5, "isTopPerformer": false, "rankLabel": "#2" }
}
],
"meta": { "timestamp": "...", "requestId": "..." }
}
Rate Limit: 30 req/min.
Enums:
·
period: "daily" | "weekly" | "monthly" | "alltime". Default: "weekly".Response: Array (not paginated). Sorted by rank ascending. WeightedScore = avgScore * 0.6 + attendanceRate * 0.3 + participationScore * 0.1.
Errors: 401 (no token), 403 (not your group), 404 (group not found).
Teacher-Wide Leaderboard
Combined rankings across all your groups. Paginated.
Request / Response
Rate Limit: 30 req/min.
Query: period (default "alltime"), gradeLevel (optional filter), page (default 1), pageSize (default 20, max 100).
Behavior: Combines rankings across ALL groups owned by this teacher. A student appears once with their best score across groups.
Errors: 401 (no token), 403 (wrong role).
List Notifications
Your notifications: badge awards, payment confirmations, system alerts.
Request / Response
{
"data": {
"items": [
{
"id": "...", "userId": "b0000000-...",
"title": "Payment Confirmed", "body": "Sara Mohamed's payment of 1500 EGP confirmed",
"type": "PaymentConfirmed", "isRead": false,
"payload": null, "sentAt": "2026-09-12T10:00:00Z"
}
],
"page": 1, "pageSize": 20, "totalCount": 12
},
"meta": { "timestamp": "...", "requestId": "..." }
}
Rate Limit: 30 req/min.
Query: page (default 1), pageSize (default 20, max 100).
Response Fields: id, userId, title, body, type (string like "PaymentConfirmed"), isRead (bool), payload (nullable JSON string), sentAt (nullable DateTime).
Errors: 401 (no token), 403 (wrong role).
Unread Count
Quick badge count for the notification bell.
Request / Response
{ "data": 3, "meta": { "timestamp": "...", "requestId": "..." } }
Mark Read / Read All
Mark a single notification as read, or mark all as read.
Request / Response
{ "data": {}, "meta": { "timestamp": "...", "requestId": "..." } }
Rate Limit: 30 req/min.
Behavior: mark-read sets isRead=true on one notification. read-all sets isRead=true on ALL unread notifications for this user.
Errors: 401 (no token), 403 (wrong role), 404 (notification not found for mark-read).
Send Notification to Parent
Send a notification to a specific student's parent.
Request / Response
{
"studentId": "d4e5f6a7-89ab-cdef-0123-456789abcdef",
"title": "Exam Results",
"body": "Sara scored 90% in the Cell Division quiz",
"type": 0
}
Rate Limit: 30 req/min.
Enums:
·
type: 0=NewGrade, 1=RankChange, 2=BadgeEarned, 3=PaymentConfirmed, 4=SystemAlert. Integer (NotificationType enum).Behavior: Creates a single Notification record for the student's parent. Bilingual EN/AR — title and body are stored as-is (caller provides translated version if needed).
Errors: 400 (validation), 401 (no token), 403 (wrong role), 404 (student not found or student has no parent).
Bulk Notify Parents
Send a notification to all parents in a group. Returns count of notifications sent.
Request / Response
{
"title": "Exam Results Available",
"body": "The results for the Cell Division quiz are now available. Class average: 82.8%",
"type": 0
}
{ "data": 15, "meta": { "timestamp": "...", "requestId": "..." } }
Rate Limit: 30 req/min.
Enums:
·
type: 0=NewGrade, 1=RankChange, 2=BadgeEarned, 3=PaymentConfirmed, 4=SystemAlert.Behavior: Creates one Notification per parent (not per student). If a parent has multiple children in the group, they still get only one notification. Response data = count of notifications sent.
Errors: 400 (validation), 401 (no token), 403 (wrong role), 404 (group not found).
Look Up Subject by Name
Find a subject's ID by its English or Arabic name. Useful for creating groups.
Request / Response
{
"data": {
"id": "a1b2c3d4-5678-9abc-def0-1234567890ab",
"name": "Biology",
"nameArabic": "أحياء",
"isActive": true,
"createdAt": "2026-01-01T00:00:00Z",
"updatedAt": null
},
"meta": { "timestamp": "...", "requestId": "..." }
}
Rate Limit: 30 req/min.
Behavior: Case-insensitive name search. Use the returned
id as subjectId when creating groups (POST /teachers/groups).Related Endpoints: GET /subjects (list all, paginated), POST /subjects (admin only, create new).
Errors: 401 (no token), 403 (wrong role), 404 (subject not found by name).
Codebase Guide
✗ Application NEVER depends on Infrastructure
✗ API NEVER depends on Infrastructure directly (only via Application interfaces)
All wiring, middleware pipeline, DI
MediatR + validator registration
DbContext + repos + services registration
17 DbSets, TPT config
SaveChanges with error handling
25+ exception → HTTP mappings
Guid.CreateVersion7() — time-ordered UUIDs. Generated in handlers, NOT in entities. Entities have { get; set; } only._context.AddAsync() or LINQ queries. Handlers call _unitOfWork.SaveChangesAsync() explicitly..AsNoTracking() — faster, no change tracking overhead. Only use tracking for update scenarios.TrackToImprove.Infrastructure. Startup: TrackToImprove.API.Static factory methods for creating valid test entities. Thread-safe counter for unique values.
Generates JWT tokens for test users with known secret. Used in integration tests.
Seeds deterministic test data with fixed GUIDs. Same data every test run.
InMemory EF Core + FakeEmailService + FakeJobService + FakeFileStorage. Sets ASPNETCORE_ENVIRONMENT=Testing.
FakePasswordHasher (no BCrypt), FakeEmailService (logs to console), FakeJobService (no Hangfire), FakeFileStorage (no disk).
Wrapper for testing LoggingBehavior with non-seekable streams (prevents Stream.ReadTimeout).