Lesson Plays API
Updated on 2025-11-25
GET /api/lesson-plays
List all lesson plays for the authenticated client/user, with support for pagination, filtering, and sorting.
Query Parameters
clientId: string (required) — The client identifier. If not found, a 500 error is returned.page: number (optional, default: 1) — The page number (1-based).pageSize: number (optional, default: 20, max: 100) — Number of results per page.sortBy: string (optional, default:createdAt) — Field to sort by (e.g.,createdAt,score).sortOrder: string (optional, default:desc) — Sort order:ascordesc.userId: string (optional) — Filter by user ID.lessonId: string (optional) — Filter by lesson ID.startAfter: any (optional) — Cursor value for pagination (use the value of thesortByfield from the last item of the previous page).
Headers:
Authorization: Bearer <JWT>(ID token)AdminToken: <JWT>(Admin token alternative)RevokableToken: <JWT>(Revokable token alternative)
Response
-
200 OK:Success Response{
"data": [LessonPlay, ...],
"page": number,
"pageSize": number,
"hasMore": boolean,
"nextPageToken": any
} -
401 Unauthorized:{ "error": string } -
500 Internal Server Error:{ "error": 'Client ID not found' | string } -
500 Firestore index required:{ "error": 'Firestore index required', details: string }
Use nextPageToken as the startAfter value for the next page request.
Filtering and sorting combinations may require Firestore composite indexes. If an index is missing, the error response will include a link to create it.
Each lesson play object includes a lesson property with the full lesson configuration when available.
Each LessonPlay object includes:
id: string — Unique ID of this lesson play recordclientId: string — ID of the user client (manages a group of users)userId: string (optional) — ID of the userlessonId: string — Lesson IDcreatedAt: string (ISO date or Firestore timestamp) — Creation datecompletedAt: string (ISO date or Firestore timestamp, optional, nullable) — Completion dateactivityTime: number (optional) — How long the user has spent in the activity, in secondslessonSessionGradeMap: object (optional) — Grade achieved in each session (map of session index to grade)averageGrade: string (optional, nullable) — Average grade of the workout (A+, A, B, C, D, E, F)lessonSessionEngagementLevelMap: object (optional) — Engagement level (0-1) of each session (map of session index to number)averageEngagementLevel: number (optional, nullable) — Average engagement level (0-1)lessonSessionRepCountMap: object (optional) — Rep count of each session (map of session index to number)totalRepCount: number (optional) — Total reps completedtotalTargetRepCount: number (optional) — Total target repslessonSessionSimilarityScoreMap: object (optional) — Similarity score (0-1) of each session (map of session index to number)averagePoseSimilarity: number (optional, nullable) — Average similarity score (0-1)caloriesBurned: number (optional) — Calories burnedsessions:LessonPlaySession[]— Details for each session (see below)email: string (optional)displayName: string (optional)photoURL: string (optional)skipCalibration: boolean (optional)skipWarmUp: boolean (optional)skipCoolDown: boolean (optional)locale: string (optional) — One of:en,zh-Hans,zh-Hantlesson:LessonConfig(optional) — Full lesson configuration (populated by API)playbackSpeed: number (optional) — Current playback speedreturnUrl: string (optional) — Custom URL to redirect to after lesson play completion
POST /api/lesson-plays
Create a new lesson play for the authenticated client/user.
Headers:
Authorization: Bearer <JWT>(ID token required)
Request Body:
{
"lessonId": "string (required)",
"userId": "string (optional)",
"email": "string (optional)",
"displayName": "string (optional)",
"photoURL": "string (optional)",
"skipWarmUp": "boolean (optional)",
"skipCoolDown": "boolean (optional)",
"skipCalibration": "boolean (optional)",
"locale": "string (optional)",
"playbackSpeed": number (optional) — Target playback speed,
"returnUrl": "string (optional)" — Custom URL to redirect to after lesson completion
}
Response:
-
200 OK:Create Lesson Play Response{
"lessonPlayId": "string",
"redirectUrl": "string",
"createdAt": "ISO date string"
} -
400 Bad Request:{ "error": "Invalid email" }— Email format is invalid{ "error": "Invalid photoURL" }— Photo URL format is invalid{ "error": "Invalid returnUrl" }— Return URL format is invalid{ "error": "User ID does not match auth token" }— User ID mismatch with auth token{ "error": "User does not exist" }— User ID not found in client users{ "error": "Lesson does not exist" }— Lesson ID not found{ "error": "Invalid locale" }— Locale is not supported
-
401 Unauthorized:{ "error": string } -
500 Internal Server Error:{ "error": "Client ID not found" | string }
- If
userIdis provided and an ID token is used, theuserIdmust match the token'suid. - The
userIdmust exist in the client's user list if provided. - The
lessonIdmust reference an existing lesson. - Valid locales are:
en,zh-Hans,zh-Hant. - Returns a
redirectUrlin the format/move/lesson/play/{lessonPlayId}. - If
returnUrlis provided, the user will be redirected to that URL after the lesson play experience is completed instead of the default result page.
GET /api/lesson-plays/[lessonPlayId]
Fetch a lesson play by its ID.
Headers:
Authorization: Bearer <JWT>(ID token)AdminToken: <JWT>(Admin token alternative)RevokableToken: <JWT>(Revokable token alternative)
Path Parameters:
lessonPlayId: string (required) — The lesson play's unique identifier
Response:
200 OK:{ "data": LessonPlay }404 Not Found:{ "error": "LessonPlay not found" }401 Unauthorized:{ "error": "Unauthorized" }— User ID mismatch with auth token500 Internal Server Error:{ "error": "Client ID not found" | "Authentication error" | "Internal Server Error" }
- If using an ID token, the lesson play's
userIdmust match the token'suid. - The response includes a
lessonproperty with the full lesson configuration when available. - Session titles are automatically populated from the lesson configuration.
PATCH /api/lesson-plays/[lessonPlayId]
Update a lesson play record. Currently supports updating the returnUrl field.
Headers:
Authorization: Bearer <JWT>(ID token required)
Path Parameters:
lessonPlayId: string (required) — The lesson play's unique identifier
Request Body:
{
"returnUrl": "string (optional)" — Custom URL to redirect to after lesson completion. Set to null to remove.
}
Response:
-
200 OK:Update Lesson Play Response{
"success": true,
"lessonPlayId": "string",
"updatedFields": ["returnUrl"]
} -
400 Bad Request:{ "error": "Invalid returnUrl" }— Return URL format is invalid{ "error": "No valid fields to update" }— Request body contains no valid fields
-
401 Unauthorized:{ "error": "Unauthorized" }— User ID mismatch with auth token -
404 Not Found:{ "error": "LessonPlay not found" } -
500 Internal Server Error:{ "error": "Client ID not found" | "Authentication error" | "Internal Server Error" }
- If using an ID token, the lesson play's
userIdmust match the token'suid. - The
returnUrlcan be set tonullto remove it. - Only the
returnUrlfield is currently supported for updates.
LessonPlaySession (in sessions array)
Each lesson play contains an array of LessonPlaySession objects. Fields:
index: number — Index of the lesson sessiongrade: string (optional) — Grade achieved (A+, A, B, C, D, E, F)engagementLevel: number (optional) — Engagement level (0-1)poseSimilarity: number (optional) — Pose similarity (0-1)repCount: number (optional) — Rep counttargetRepCount: number (optional) — Target rep counttitle: object (optional) — Multilingual title (populated by API, see below)skipped: boolean (optional) — Whether the session was skipped
If either repCount or targetRepCount is provided, both must be provided together.
Multilingual Title Example
{
"en": "Warmup",
"zh-Hant": "熱身",
"zh-Hans": "热身"
}
Sample LessonPlay Object
{
"id": "abc123",
"clientId": "gofa",
"userId": "user456",
"lessonId": "lesson789",
"createdAt": "2024-05-24T10:00:00.000Z",
"completedAt": null,
"activityTime": 1200,
"lessonSessionGradeMap": { "0": "A", "1": "B" },
"averageGrade": "A",
"lessonSessionEngagementLevelMap": { "0": 0.95, "1": 0.8 },
"averageEngagementLevel": 0.875,
"lessonSessionRepCountMap": { "0": 10, "1": 15 },
"totalRepCount": 25,
"totalTargetRepCount": 30,
"lessonSessionSimilarityScoreMap": { "0": 0.9, "1": 0.85 },
"averagePoseSimilarity": 0.875,
"caloriesBurned": 100,
"sessions": [
{
"index": 0,
"grade": "A",
"engagementLevel": 0.95,
"poseSimilarity": 0.9,
"repCount": 10,
"targetRepCount": 12,
"title": { "en": "Warmup", "zh-Hant": "熱身", "zh-Hans": "热身" },
"skipped": false
},
{
"index": 1,
"grade": "B",
"engagementLevel": 0.8,
"poseSimilarity": 0.85,
"repCount": 15,
"targetRepCount": 18,
"title": { "en": "Workout", "zh-Hant": "運動", "zh-Hans": "运动" },
"skipped": false
}
],
"email": "user@example.com",
"displayName": "User Name",
"photoURL": "https://example.com/photo.jpg",
"skipCalibration": false,
"skipWarmUp": false,
"skipCoolDown": false,
"locale": "en",
"returnUrl": "https://example.com/custom-result-page",
"lesson": {
"id": "lesson789",
"title": { "en": "Pure Yoga", "zh-Hant": "純瑜伽", "zh-Hans": "纯瑜伽" },
"workoutCategory": "ENGAGEMENT"
}
}
- All fields may not be present on every lesson play. Optional and nullable fields are indicated above.
- For the most up-to-date schema, refer to the lesson play and session models/interfaces in your codebase (
src/models/lesson-play.tsandsrc/models/lesson-play-session.ts). - Authentication can be done using ID tokens (Bearer), Admin tokens, or Revokable tokens depending on the endpoint.
- The API automatically populates session titles and lesson configuration data when fetching lesson plays.