DocsAPI Reference

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

StatusCodeReason
403FORBIDDENCaller is not a parent Contact
400VALIDATION_ERRORSession is missing contactId
500INTERNAL_ERROROptiCRM 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

  • recentGrades returns the last 10 graded submissions. score is finalScore ?? score. percentage and passed are derived server-side.
  • upcomingAssignments is capped at 5, scoped to the child's currently-active enrollments, excluding ones the child already submitted.
  • liveClasses.upcoming is capped at 5; attendedThisMonth / missedThisMonth count only COMPLETED sessions in the current calendar month.
  • If the child has no portal Contact row yet (legacy data), the response returns the child block plus zeroed-out arrays so the frontend can render a "no data yet" state.

Errors

StatusCodeReason
403FORBIDDENCaller is not a parent / child not linked
400VALIDATION_ERRORSession missing contactId
500INTERNAL_ERRORDB or OptiCRM error