/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 (403 APP_LOCKED for a frozen or ended subscription). Preview tokens can only call the GET endpoints. See Authentication.
How days are resolved
The trainee’s day follows their device, not the server and not the studio.- The app sends its IANA zone in the
x-timezoneheader on every request.authenticateTraineestores a valid value onClient.timezone. - Services read the zone with
repo.traineeTimezoneorresolveTimeZone(client.timezone, client.studio.timezone). The order is the client’s zone, then the studio’s, thenAsia/Jerusalem. - “Today” is
isoDateInTz(new Date(), timeZone), aYYYY-MM-DDkey. - Per-day rows (
DailyMetric,CardioLog.performedOn) are keyed by that day key, stored as UTC midnight of the key in a@db.Datecolumn. - Instant rows (
WeightEntry.recordedAt,ClientPhoto.takenAt) keep the real timestamp and are bucketed into days at read time.
Models written
Progress
GET /v1/trainee/progress
The progress tab summary: weight trend, latest measurements, recent photos, workout streak, weekly compliance and personal records.
Auth: trainee token and app access.
No parameters.
Response: 200.
number | null
The latest and the earliest weigh-in by
recordedAt. Both fall back to Client.weightKg when there are no weigh-ins.null
Always
null on this endpoint. The goal weight is served by GET /v1/trainee/measurements/summary.array
Every weigh-in in chronological order.
date is the full recordedAt instant.object | null
The most recent
DailyMetric that has measurements. values keys are chest, waist, hips, arm, thigh and neck, in centimetres.array
The 12 most recent progress photos by
takenAt.object
Consecutive local days with a completed workout, over the last 180 days.
current counts back from today, or from yesterday when today has no workout yet. best is the longest run in the window.number | null
Distinct days with a completed workout in the last 7 days divided by the number of days in the active training plan, as a rounded percent.
0 when there is no plan, null for a file plan.array
The 5 most recently achieved records across the latest 200 completed logs.
value is a display string: weight and reps, or reps only for bodyweight. exercise is the shared library name.NOT_FOUND (404) trainee not found.
Weight
POST /v1/trainee/weight
Logs a weigh-in.
Auth: trainee token and app access.
number
required
Greater than 0, at most 500.
string
ISO date-time. Defaults to now. Must not be in the future.
string
Trimmed, at most 200 characters.
WeightEntry with source: "APP", then sets Client.weightKg to the weight of the latest weigh-in by recordedAt.
Response: 201. The created row.
PATCH /v1/trainee/weight/:id
Edits a weigh-in the trainee created in the app.
Auth: trainee token and app access.
string
required
The weigh-in id.
number
Greater than 0, at most 500.
string | null
Trimmed, at most 200 characters.
null clears the note.source equal to APP can be changed. Weigh-ins that came from a form, a coach, the automation API or an import are read-only for the trainee. recordedAt cannot be changed. After the update the client’s headline weight is re-synced.
Response: 200. The updated WeightEntry row, same shape as the create.
Errors:
DELETE /v1/trainee/weight/:id
Deletes a weigh-in the trainee created in the app.
Auth: trainee token and app access.
string
required
The weigh-in id.
Client.weightKg keeps its last value.
Response: 204, no body.
Errors:
Measurements
GET /v1/trainee/measurements/summary
The measurements screen: headline weights, goal progress, a chart series, circumference tiles and a merged day log.
Auth: trainee token and app access.
No parameters.
Response: 200.
number
Number of weigh-ins plus number of days with measurements.
object
pct is progress from the start weight to Client.goalWeightKg, clamped to 0 to 100. direction is lose or gain. pct and direction are null when any of the three weights is missing or the start equals the goal.array
Weigh-ins of the last 90 days by local date, or all weigh-ins when fewer than two fall in that window, downsampled to at most 12 evenly spread points with both ends kept.
array
Always five tiles:
chest, waist, hips, arm, thigh. delta is current minus first, null with fewer than two values. neck is stored but has no tile.array
Newest first, at most 120 rows. Each weigh-in is a row. A day’s measurements ride on that day’s last weigh-in. A day with measurements and no weigh-in gets its own row with
weightEntryId: null. time is HH:mm in the trainee’s zone. source is app, form, coach, automation or import. editable is true only for weigh-ins created in the app.NOT_FOUND (404) trainee not found.
POST /v1/trainee/measurements
Logs body circumferences for a day.
Auth: trainee token and app access.
number
Centimetres. Greater than 0, at most 500.
number
Same limits.
number
Same limits.
number
Same limits.
number
Same limits.
number
Same limits.
string
At most 1000 characters.
string
YYYY-MM-DD. Defaults to today in the trainee’s zone. Must not be in the future.DailyMetric, merges the sent keys over the stored ones, and upserts the row. Keys that are not sent keep their previous value, so a second call on the same day adds to the first. A body measurement field on a submitted form writes to the same column. See Trainee forms.
Response: 201. The DailyMetric row.
Photos
POST /v1/trainee/photos
Adds a progress photo. Upload the image first with Trainee uploads.
Auth: trainee token and app access.
string
required
A valid URL, at most 1000 characters.
string
ISO date-time. Defaults to now.
string
FRONT, SIDE or BACK. Used for before and after pairing in coach and partner views.ClientPhoto row.
VALIDATION (422) when the body fails the schema.
Water
Water is stored as a running total (waterMl) plus an entry list (waterLogs) on the day’s DailyMetric. The entry list is what makes retries and undo safe.
GET /v1/trainee/water
Returns the day’s total, goal and entries.
Auth: trainee token and app access.
string
YYYY-MM-DD. Defaults to today. Must not be in the future.Client.waterGoalMl is used, else 0.
Response: 200.
deductEntryId.
Errors:
POST /v1/trainee/water
Adds or deducts water for a day.
Auth: trainee token and app access.
number
required
Integer from -5000 to 5000, not 0. Negative values deduct.
string
YYYY-MM-DD. Defaults to today. Must not be in the future.string
1 to 100 characters. A client-generated id for this entry. It becomes the entry’s
id. If an entry with this id already exists for the day, the call changes nothing and returns the current totals. Use it to make retries safe.string
1 to 100 characters. Undo a specific earlier entry. The amount applied is the negative of that entry’s amount, and
amountMl in the body is ignored. A second undo of the same entry changes nothing.repo.incrementWater with applyWaterLog). The total can never go below zero, and a day holds at most 1000 entries. Without requestId the server generates a UUID for the entry.
Response: 201.
steps is the same day’s step count, returned so the app can refresh both rings from one response.
Errors:
DELETE /v1/trainee/water/:entryId
Removes one water entry, and any undo entries that pointed at it.
Auth: trainee token and app access.
string
required
The entry id, 1 to 100 characters.
string
YYYY-MM-DD. The day the entry belongs to. Defaults to today.{ waterMl, steps }, the same shape as the POST.
Errors:
Steps
POST /v1/trainee/steps
Sets today’s step count.
Auth: trainee token and app access.
number
required
Non-negative integer, at most 100,000 (
MAX_DAILY_STEPS).DailyMetric.steps. It is not added to it. There is no date parameter: this endpoint only writes today in the trainee’s zone. Use POST /v1/trainee/health/days to sync other days.
Response: 201 with { waterMl, steps } for today.
VALIDATION (422) when steps is missing, negative, not an integer or above 100,000.
Cardio
POST /v1/trainee/cardio
Logs one cardio session, either timed in the app or added by hand.
Auth: trainee token and app access.
string
required
walking, running, stairs, cycling or other.number
required
Positive integer, at most 600 (
MAX_CARDIO_MINUTES).string
YYYY-MM-DD. Defaults to today. A day that has not started yet in the trainee’s zone is refused.string
1 to 100 characters. The training plan the session counts toward. Stored as a plain id, with no foreign key, so deleting a program never removes activity history.
string
1 to 100 characters. The plan day.
string
default:"MANUAL"
WORKOUT for a session timed as part of a workout, MANUAL otherwise.performedOn is the day key. startedAt is the current time for a session logged today, or the start of the day for a past date. Sessions feed the movement block on Plans and workouts and the home screen.
Response: 201.
GET /v1/trainee/cardio
Lists recent cardio sessions.
Auth: trainee token and app access.
number
default:"7"
Positive integer, at most 90 (
CARDIO_HISTORY_MAX_DAYS). The range ends today and covers this many local days.VALIDATION (422) when days is out of range.
DELETE /v1/trainee/cardio/:entryId
Deletes one cardio session of the trainee.
Auth: trainee token and app access.
string
required
The cardio log id, 1 to 100 characters.
NOT_FOUND (404) cardio entry not found.
Health data sync
The app reads steps from HealthKit on iOS and Health Connect on Android and sends per-day totals. Heart rate was removed from the product. Two endpoints keep legacy shapes so app builds already on devices keep working.POST /v1/trainee/health/days
Syncs per-day step totals.
Auth: trainee token and app access.
object[]
required
1 to 90 items (
HEALTH_MAX_DAYS_PER_INGEST).string
required
YYYY-MM-DD, the trainee’s local day.number
required
Non-negative integer, at most 100,000.
DailyMetric.steps for that day. The app must send the platform’s own daily total, not a sum of raw samples. Days whose local start is more than 90 days in the past or more than one day in the future are skipped without an error. When the same date appears twice, the last one wins.
Response: 201. days is the number of days written.
VALIDATION (422) when the array is empty, longer than 90, or an item fails the schema.
GET /v1/trainee/health/summary
Returns per-day steps for a range.
Auth: trainee token and app access.
string
ISO date or date-time. Defaults to 7 days ago.
string
ISO date or date-time. Defaults to now.
DailyMetric row are returned.
Response: 200.
restingHeartRate, avgHeartRate and latestHeartRate are always null and are never read from the database. They stay on the wire because older app builds test latestHeartRate !== null to decide whether health data exists. A missing key would read as undefined and pass that test.
Errors: VALIDATION (422) when a bound is not a date.
POST /v1/trainee/health/samples
Retired. Kept so older app builds that still post raw samples do not fail.
Auth: trainee token and app access.
object[]
required
1 to 2000 items. Each has
type (STEPS or HEART_RATE), value (0 to 1,000,000), unit (1 to 32 characters), source (HEALTHKIT or HEALTH_CONNECT), externalId (1 to 200 characters), startedAt and endedAt (dates, with endedAt on or after startedAt).HEART_RATE stays in the enum because older builds batch both types in one request and a narrower enum would reject the whole batch.
Response: 201.
VALIDATION (422) when the body fails the schema.