Parent API
Children list and per-child read-only dashboard
Parent endpoints are gated to authenticated parent Contacts (isContact: true && contactType: "CONTACT_PARENT"). Staff users and student Contacts get 403.
OptiLearn never owns the parent↔student graph — that's ContactStudentLink in OptiCRM. These endpoints proxy to OptiCRM for the linkage and then read the LMS database for course/grade/attendance data scoped to the linked child.
GET /api/parent/children
Children linked to the authenticated parent. Backing endpoint for the parent dashboard header + child selector.
Response
{
"data": {
"children": [
{
"contactId": "ct_456",
"studentId": "st_789",
"name": "Aarav Sharma",
"email": "aarav@example.com",
"studentCode": "SP-2026-014",
"class": "Class 8",
"section": "B",
"photoUrl": "https://...",
"status": "ACTIVE",
"relation": "FATHER",
"isPrimary": true,
"institutionId": "inst_..."
}
]
},
"error": null
}
Pulled fresh from OptiCRM (cache: no-store) so newly added children appear immediately.
Errors
| Status | Code | Reason |
|---|---|---|
| 403 | FORBIDDEN | Caller is not a parent Contact |
| 400 | VALIDATION_ERROR | Session is missing contactId |
| 500 | INTERNAL_ERROR | OptiCRM bridge failure |
GET /api/parent/children/[childId]/overview
Read-only dashboard payload for one child. childId in the URL is the child's OptiCRM Contact id (the same value OptiLearn stores on Enrollment.studentId).
Security
The handler always re-resolves the parent's children list from OptiCRM and verifies childId is in the list. A parent cannot enumerate other parents' children by id even if they guess one.
Every Prisma query below is also filtered by institutionId = session.institutionId.
Response shape
{
"data": {
"child": { "contactId", "studentId", "name", "email", "studentCode", "class", "section", "photoUrl", "relation" },
"courses": [
{
"enrollmentId", "courseId", "courseTitle", "thumbnailUrl",
"progress": 64,
"status": "ACTIVE",
"finalScore": null,
"passed": null,
"lastAccessedAt": "...",
"completedAt": null,
"totalLessons": 24,
"estimatedDuration": 360
}
],
"recentGrades": [
{
"submissionId", "assignmentId", "assignmentName",
"courseId", "courseTitle",
"score": 18, "maxScore": 20, "passingScore": 12,
"percentage": 90, "passed": true,
"gradedAt": "...", "status": "GRADED"
}
],
"upcomingAssignments": [
{ "assignmentId", "title", "courseId", "courseTitle", "dueDate", "maxScore" }
],
"liveClasses": {
"attendedThisMonth": 6,
"missedThisMonth": 1,
"upcoming": [
{ "sessionId", "title", "courseId", "courseTitle", "scheduledAt", "durationMinutes" }
]
},
"gamification": {
"currentStreak": 12,
"longestStreak": 21,
"totalPoints": 1450,
"totalBadges": 5
},
"certificates": [
{ "id", "certificateNumber", "verificationCode", "courseTitle", "issuedAt", "expiresAt", "pdfUrl" }
]
}
}
Notes
recentGradesreturns the last 10 graded submissions.scoreisfinalScore ?? score.percentageandpassedare derived server-side.upcomingAssignmentsis capped at 5, scoped to the child's currently-active enrollments, excluding ones the child already submitted.liveClasses.upcomingis capped at 5;attendedThisMonth/missedThisMonthcount onlyCOMPLETEDsessions in the current calendar month.- If the child has no portal Contact row yet (legacy data), the response returns the
childblock plus zeroed-out arrays so the frontend can render a "no data yet" state.
Errors
| Status | Code | Reason |
|---|---|---|
| 403 | FORBIDDEN | Caller is not a parent / child not linked |
| 400 | VALIDATION_ERROR | Session missing contactId |
| 500 | INTERNAL_ERROR | DB or OptiCRM error |
Related
- Parent Portal — admin setup
- OptiCRM Integration —
/api/lms/parent/[id]/childrenand/api/lms/student/[id]/parents