Everything the trainee records about their body and daily activity goes through these endpoints on the main trainee router. Mount: /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-timezone header on every request. authenticateTrainee stores a valid value on Client.timezone.
  • Services read the zone with repo.traineeTimezone or resolveTimeZone(client.timezone, client.studio.timezone). The order is the client’s zone, then the studio’s, then Asia/Jerusalem.
  • “Today” is isoDateInTz(new Date(), timeZone), a YYYY-MM-DD key.
  • Per-day rows (DailyMetric, CardioLog.performedOn) are keyed by that day key, stored as UTC midnight of the key in a @db.Date column.
  • Instant rows (WeightEntry.recordedAt, ClientPhoto.takenAt) keep the real timestamp and are bucketed into days at read time.
A date parameter is always the trainee’s local calendar day. Future days are refused wherever a date can be sent.

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.
Errors: 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.
Creates a WeightEntry with source: "APP", then sets Client.weightKg to the weight of the latest weigh-in by recordedAt. Response: 201. The created row.
Errors:

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.
Only entries with 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.
Same ownership rule as the edit. The headline weight is re-synced to the latest remaining weigh-in. When none remain, 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.
Errors: 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.
At least one of the six measurements is required. The service reads the day’s existing 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.
Errors:

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.
Response: 201. The created ClientPhoto row.
Errors: 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.
The goal comes from the active nutrition plan: the water target (in litres) of the plan day that maps to the requested date, converted to millilitres. When the plan has no water target, or the plan is a file plan, the trainee’s own Client.waterGoalMl is used, else 0. Response: 200.
An entry created as an undo also carries 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.
The whole read, check and write runs in one database transaction (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.
The total is reduced by the net amount of the removed entries and never drops below zero. Response: 200 with { 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).
The value replaces today’s 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.
Errors: 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.
Errors:

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.
Response: 200. Entries are ordered by day, then start time, both descending.
Errors: 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.
Response: 200.
Errors: 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.
Each day’s value replaces 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.
Errors: 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.
Both bounds are converted to local day keys in the trainee’s zone. Only days that have a 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).
The controller validates the body and returns a fixed result. It does not call the service and stores nothing. 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.
Errors: VALIDATION (422) when the body fails the schema.