TagMangoTagMango Docs
Course

Get Student Course Progress

GET
/api/v1/external/users/{userId}/course-progress

Read how far one student has progressed through the courses they are enrolled in with you. Use it to build a learner dashboard, sync progress into another system, or answer where a student stopped.

This endpoint is organised by student: you name one student and get every course they are in. Two siblings answer neighbouring questions and are the better choice when they fit:

  • Get Student Overview — the same student as headline counts (courses enrolled, courses completed, certificates available) plus per-course watch time, when you need totals rather than per-course status. This endpoint does not report watch time.
  • List Course Reporting Students — one course and every student in it, the transpose of this endpoint. Use it for a course dashboard rather than calling this endpoint per student.

Because those endpoints were built for different questions, each applies its own completion rule: this one treats a course as completed when its stored percentage reached 100, Get Student Overview counts only courses stored at exactly 100, and List Course Reporting Students also counts a course carrying a completion date. Expect their completion counts to differ for the same student, and pick one endpoint as your source of truth rather than reconciling them.

A student is enrolled in a course when they hold an active subscription to a product the course is sold through, and the course is published and belongs to you. A course the student has never opened is still included with a percentage of 0 — so total is a usable denominator for questions like "4 of 12 completed".

Only students on the authenticated host are readable, and only your own courses are returned, so this endpoint can never report another creator's course or another host's student. It reads without writing anything: retrying a request is always safe and changes no state.

Courses are returned newest first, ordered by course ID descending. The order is stable across requests, so paging through a large enrolment never repeats or skips a course.

  • level chooses the depth. course returns one entry per course; module nests each course's sections; chapter nests each section's chapters. Sections and chapters follow the order the creator arranged them, and are omitted entirely — rather than returned empty — when you did not ask for them.
  • courseIds narrows within the student's enrolment and can never widen it. An ID they are not enrolled in, or that belongs to another creator, is ignored rather than reported, so a filtered request that matches nothing returns an empty list instead of an error.
  • completed means the stored percentage reached 100, applied identically at every level. completedAt is reported alongside it for reference but does not decide the status, so a course whose percentage was later reduced — for example because you added a chapter — can carry a completion date while reporting in_progress.
  • clicked_only distinguishes a student who opened the course from the catalogue without starting any content. Both that and not_started report a percentage of 0, so read status rather than the number to tell them apart.
  • Progress is recorded as the student watches and is persisted in the background, so these values reflect the last completed sync and can lag live activity by roughly 30 seconds. A percentage of 0 means nothing has been recorded yet, which is not always the same as never opened — this applies to sections and chapters as much as to courses.
  • Section and chapter percentages are stored unrounded and are returned exactly as stored, so a section can read as 99.99999999999999 rather than 100. Such a section is correctly reported as in_progress, matching how the platform itself treats it.
AuthorizationBearer <token>

In: header

Path Parameters

userId*string

Unique identifier of the student (fan) whose progress is read, as a 24-character hex ObjectId. The student must belong to the authenticated host; one belonging to another host is reported as not found. If you hold the student's email or phone rather than their id, resolve it first with Get User By Email or Phone.

Query Parameters

limit?number

Maximum courses returned on this page. Sections and chapters are not paginated separately; they are bounded by each course's own structure. Omit to return 25.

Default25
Range1 <= value <= 50
page?number

One-based page number over the enrolled courses. A page beyond the available results returns an empty data array without changing total. Omit for page 1.

Default1
Range1 <= value
level?string

Depth of the returned tree. course returns one entry per enrolled course; module nests each course's sections; chapter nests each section's chapters. Omit for course.

Default"course"
Value in"course" | "module" | "chapter"
courseIds?array<>

Non-empty array of 24-character Course ObjectIds from List Courses by Creator, at most 50 per request. Omit to include every course the student is enrolled in. This filter only narrows within that enrolment: an id the student is not enrolled in, or that belongs to another creator, is ignored rather than reported, so an empty result never reveals whether a course exists.

Header Parameters

x-whitelabel-host*string

Host name of the dashboard, ex: mydomain.tagmango.com

Response Body

application/json

application/json

application/json

application/json

application/json

application/json

curl -X GET "https://api-prod-new.tagmango.com/api/v1/external/users/66c4964c11e7fef26751f3a7/course-progress" \  -H "x-whitelabel-host: string"
{
  "data": [
    {
      "courseId": "64a7b8c9d1e2f3a4b5c6d7e8",
      "courseTitle": "Mastering TypeScript",
      "progressPercent": 62.5,
      "status": "in_progress",
      "completedAt": "2026-08-26T11:20:45.000Z",
      "startedAt": "2026-07-02T09:14:22.000Z",
      "lastActivityAt": "2026-08-25T18:03:11.000Z",
      "modules": [
        {
          "moduleId": "64b1c2d3e4f5a6b7c8d9e0f2",
          "moduleTitle": "Generics in Depth",
          "progressPercent": 50,
          "status": "in_progress",
          "chapters": [
            {
              "chapterId": "64c9d0e1f2a3b4c5d6e7f8b2",
              "chapterTitle": "Conditional Types",
              "progressPercent": 100,
              "status": "completed"
            }
          ]
        }
      ]
    }
  ],
  "total": 2
}
{
  "code": 400,
  "type": "Bad Request",
  "statusMessage": "BAD_REQUEST",
  "success": false,
  "message": "Invalid request",
  "result": "Invalid request"
}
{
  "code": 401,
  "type": "Unauthorized",
  "statusMessage": "UNAUTHORIZED",
  "message": "Invalid token",
  "result": "Invalid token",
  "success": false
}
{
  "code": 403,
  "type": "Forbidden",
  "statusMessage": "FORBIDDEN",
  "errorCode": "WORKSHOP_MANGO_FORBIDDEN",
  "message": "You can not access this api with a TagMango account, only available for whitelabel hosts",
  "result": "You can not access this api with a TagMango account, only available for whitelabel hosts",
  "success": false
}
{
  "code": 404,
  "type": "Not Found",
  "statusMessage": "NOT_FOUND",
  "errorCode": "COURSE_LEARNER_NOT_FOUND",
  "message": "No student with this ID exists on the authenticated host. Nonexistent students and students belonging to another host are answered identically, so this response never reveals whether an ID exists elsewhere. A student who exists but is enrolled in nothing returns 200 with an empty list instead.",
  "result": "No student with this ID exists on the authenticated host. Nonexistent students and students belonging to another host are answered identically, so this response never reveals whether an ID exists elsewhere. A student who exists but is enrolled in nothing returns 200 with an empty list instead.",
  "success": false
}
{
  "code": 429,
  "type": "Too Many Requests",
  "statusMessage": "TOO_MANY_REQUESTS",
  "message": "Request limit exceeded. try after 10 seconds",
  "result": "Request limit exceeded. try after 10 seconds",
  "success": false
}