/v1/trainee, router traineeRouter in src/modules/trainee/trainee.routes.ts.
Lane: trainee. Every endpoint on this page uses both guards: the trainee token and the app access check. A frozen or ended subscription returns 403 with details.reason = "APP_LOCKED", and preview tokens can only call the GET endpoints. See Authentication.
Reading plans
GET /v1/trainee/workouts
Returns all active training plans with their days and exercises, the current week strip and the movement requirement.
Auth: trainee token and app access.
No parameters.
What the service does, in order:
- Loads every
Programof typeTRAININGwith statusACTIVEfor the trainee, newestupdatedAtfirst, plus the latest 200 completed workout logs (WORKOUT_LOG_SCAN), the trainee’s technique videos, the last performed date per day label, and the trainee’s stored substitutions. - Normalizes each plan’s content with
normalizeTrainingContent, which migrates older content shapes on read. A file plan (a program withpdfUrlset, either a PDF or a site link) is never normalized and is returned withkind: "file"and no days. - For plans with the automatic alternatives switch on (
meta.autoAlts), replaces each row’s alternatives with ranked automatic picks for the prescribed exercise. The coach’s own list stays stored and comes back when the switch is turned off. - Applies the trainee’s substitutions. A substitution only applies while the row still prescribes the exercise it was made against (
fromExerciseId). When it applies, the coach’s original is placed first inalternativesso the trainee can switch back. - Resolves each exercise id to a name, image, video, instructions and target muscle, applying the studio’s per-exercise overrides (
ExerciseStudioOverride) over the shared library row. - Attaches history to every row and alternative: the sets from the most recent log of that exercise, the personal record, the technique video prompt and any unseen coach feedback.
week always holds seven entries, Sunday to Saturday of the current week in the trainee’s time zone. It is shortened above. With no active training plan the response is { "plans": [], "week": [...], "movement": null }.
Plan fields
string
regular or file. A file plan has pdfUrl set, days: [], and workoutsPerWeek from content.meta.workoutsPerWeek or null.string
The plan’s display name when set, else its internal name (
traineePlanName).number
Completed logs this week that belong to the plan. A log with
programId counts for that plan only. A log without one counts when its label matches one of the plan’s day labels. For a file plan the label to match is the plan name.Day fields
string
The distinct muscles of the day’s rows joined with a middle dot.
number
(work seconds + rest seconds) x 1.1, plus the day’s cardio minutes when the plan requires cardio. Work is 3 seconds per rep using the top of a rep range, or the stated seconds or minutes for timed sets. A row with no set breakdown counts its sets value, defaulting to 3. The mobile app mirrors this formula.string | null
The latest completed log whose
label equals this day’s label.object | null
Present when the plan’s movement requirement includes cardio.
activities values are walking, running, stairs and cycling.boolean
Whether the coach enabled the RIR column (
meta.columns.rir). The app shows the RIR input only when this is true.Row fields
string
For
repType: "range" this is low-high, for example 8-12, so older app builds that only read reps still work. repType is fixed, range, seconds or minutes.array
The per-set breakdown.
type is warmup, working or drop. Empty per-set values inherit the row’s.string | null
The id of the row this one is super-setted with.
array
The sets marked done in the most recent completed log that contains this exercise. Used to prefill weight and reps.
object | null
The trainee’s record for the exercise across the scanned logs. A set counts only when it is marked
done: true, has reps above zero and, for a weighted set with a target, reached at least 80 percent of targetReps. The heaviest weight wins. Bodyweight exercises rank by reps. The rule lives in client-tracking.compute.ts and is shared with the coach views.object | null
null when the row’s mode is none and there is no open coach request. Otherwise { mode, required, reason, completedCount }. mode is free or coachDemand. reason is FIRST_TIME, EVERY_N, COACH_REQUEST or null.object | null
The newest reviewed video of this exercise with a coach note the trainee has not seen:
{ videoId, note, reviewedAt }.object | null
Set when the trainee’s own swap is applied:
{ exerciseId, name } of the coach’s original.Technique video prompts
evaluatePrompt in technique-prompts.ts decides whether a video is required:
- A coach’s one-shot request (
techniqueVideo.requestedAton the row) is open until the trainee uploads a video of that exercise after the request time. While open, the prompt is{ mode: "coachDemand", required: true, reason: "COACH_REQUEST" }. - In
coachDemandmode the quota is 1 whenfirstTimeis on, plus one for everyeveryNcompleted performances of the exercise. A video is required while the trainee has submitted fewer videos than the quota. - In
freemode a video is never required.
Next workout
The API does not return a “next” flag on days. The rule isnextTrainingDay: the day after the most recently performed one, wrapping around, or the first day when nothing was performed. The app applies the same rule, and the server uses it for GET /v1/trainee/home and for the movement block.
Movement requirement
movement is built from the newest active plan whose meta.requirement.on is true. It is null when no plan has a requirement.
mode is cardioOnly, stepsOnly, both or bothCompensated. With bothCompensated, meeting either cardio or steps is enough for todayMet. Cardio minutes come from CardioLog rows and steps from DailyMetric.steps, both for the current week. Logging them is covered on Trainee tracking.
Errors: none beyond the lane errors.
Logging
POST /v1/trainee/workouts/log
Records one workout session.
Auth: trainee token and app access.
string
The plan the workout belongs to. Send it. Logs without it are matched to plans by label only.
string
The plan day id. The coach’s workout history groups sessions by it, because coaches rename day labels.
string
The day label at the time of the workout, for example
A. For a file plan’s mark-done button the app sends the plan name.string
Trimmed, at most 1000 characters. An empty note is not stored.
number
Non-negative integer.
boolean
default:"true"
false records an unfinished session. Only completed logs count for history, streaks, records and weekly progress.object[]
default:"[]"
One object per exercise. The schema accepts any object. The fields the server reads are listed below.
entries is a contract between the app and every reader of WorkoutLog.entries. The schema does not enforce it, so follow it exactly:
programId is not checked against the trainee’s own programs on this endpoint. It only has to be an existing program id.
What the service does:
- Creates a
WorkoutLogwithperformedOnset to the server’s current time. The client cannot backdate a workout. - When
completedistrue, counts the trainee’s completed logs to getworkoutNumber. - When this is the trainee’s first completed workout, calls the subscription starter with
FIRST_WORKOUT. A subscription that was waiting for the first workout starts now. Deleting the log later does not undo that.
workoutNumber.
workoutNumber is the trainee’s lifetime count of completed workouts, used by the share screen. It is null for an unfinished log. entries is echoed as stored and shortened here.
Errors:
GET /v1/trainee/workouts/history
Lists the trainee’s completed workouts, newest first.
Auth: trainee token and app access.
No parameters. Returns at most 100 logs (WORKOUT_HISTORY_LIMIT), ordered by performedOn then createdAt, both descending. Unfinished logs are excluded.
Response: 200.
setCount, volumeKg and exerciseCount are computed by readWorkoutLog from entries. Only performed sets count. Volume is the sum of weight times reps for sets with both above zero, rounded to a whole kilogram. label is an empty string when the log has none.
Errors: none beyond the lane errors.
GET /v1/trainee/workouts/history/:id
Returns one completed workout with its exercises and sets.
Auth: trainee token and app access.
string
required
The workout log id.
workoutNumber is the ordinal of this workout among the trainee’s completed logs, counted up to this log’s performedOn and createdAt. Exercise names come from the name stored in each entry, not from the library, so they show what the trainee saw at the time. Exercises with no performed set are left out.
Errors:
DELETE /v1/trainee/workouts/log/:id
Deletes one of the trainee’s workout logs.
Auth: trainee token and app access.
string
required
The workout log id.
clientId, so a trainee can only remove their own logs. Records, last sets, streaks and weekly progress are all derived at read time, so they update on the next read. A subscription that the log started stays started.
Response: 200.
Substitutions
POST /v1/trainee/workouts/substitutions
Saves the trainee’s own swap of one prescribed exercise for an approved alternative, so it becomes their default in future workouts. Sending the original exercise as the target removes the swap.
Auth: trainee token and app access.
string
required
The training plan.
string
required
The plan day that holds the row.
string
required
The strength row id from
GET /v1/trainee/workouts.string
required
The exercise the coach currently prescribes on that row. Acts as a staleness guard.
string
required
The alternative to use. Equal to
fromExerciseId means undo.TraineeExerciseSubstitution (unique on programId and rowId), not inside the program content. The coach’s editor rewrites the whole content blob on save, so anything stored there would be lost. Writing the side table also avoids bumping the program version, which would push a “your coach updated your plan” notification to the trainee who just tapped swap.
Checks, in order:
- The program must be one of the trainee’s active training programs.
- The row must exist in the named day of the current content.
- The row must still prescribe
fromExerciseId. - For an undo, the stored substitution is deleted and the call ends.
toExerciseIdmust be offered for that row right now: one of the coach’salternatives, or, when automatic alternatives are on, one of the automatic picks computed the same way the workouts read computes them.
{ "saved": false }.
Errors:
Technique videos
POST /v1/trainee/technique-videos
Submits a technique video for coach review. Upload the file first with Trainee uploads and send the returned URL.
Auth: trainee token and app access.
string
required
The uploaded video URL. Any non-empty string is accepted.
string
The exercise the video shows. Send it. Prompts and coach requests are tracked per exercise, and videos without an exercise are ignored by them.
string
A poster image URL.
TechniqueVideo row with status PENDING. A new upload of an exercise closes any open coach request for it, because the request only stands until a newer video exists.
Response: 201. The created row.
VALIDATION (422) when url is missing or empty.
POST /v1/trainee/technique-videos/:id/feedback-seen
Marks the coach’s feedback on a video as seen, so it stops appearing as coachFeedback on the exercise.
Auth: trainee token and app access.
string
required
The technique video id, taken from
coachFeedback.videoId.TechniqueVideo.feedbackSeenAt to now. Calling it again just refreshes the timestamp.
Response: 200.