Client in Prisma). It lists and filters trainees, creates and edits them, keeps the coach assignments, stores each staff member’s saved filters, and serves the trainee board that the coach web app opens for one trainee.
Source: backend/apps/core-api/src/modules/clients/.
Mounting and auth
clientsRouter is mounted twice in modules/index.ts. Both mounts run the same router, so every path below exists under both prefixes.
The headings on this page use the web prefix. Replace
/v1/web/clients with /v1/clients for the bearer lane.
Both lanes set req.auth to { userId, studioId, role }. On the web lane the values come from the x-studio-id, x-user-id and x-user-role headers. An unknown or missing x-user-role is read as SUB_COACH. See API overview for the lanes and the response envelope.
Roles
The routes file uses norequireRole guard. Any authenticated caller of either lane reaches every route. What a caller can see is narrowed by coach access instead.
Coach access scoping
router.use(withCoachAccess(ctx.prisma)) runs first. It loads the caller’s Coach row by studioId and externalUserId and resolves a CoachAccess value (middleware/coach-access.ts):
- Role
OWNER: unrestricted. - A
Coachrow withactive: false: the request fails withFORBIDDEN(“this team member has been removed”). - No
Coachrow: unrestricted. This keeps callers who predate the permission model working. - A
HEAD_COACHrow, or aSUB_COACHrow with no storedpermissionsblob: unrestricted. - A
SUB_COACHrow whosepermissions.traineesisassigned: restricted to trainees that have aClientCoachrow for this coach.
{ assignedCoachId }, which is null for unrestricted callers. The scope never comes from the request.
router.use('/:id', requireAssignedClient(ctx.prisma)) guards every route that addresses one trainee, including the mounted tracking and subscriptions routers. A restricted coach who asks for a trainee they are not assigned to gets NOT_FOUND (“client not found”), the same answer as for a trainee of another studio.
Mounted sub-routers
Two routers hang off this one and are documented on their own pages:/:id/trackingmountsclientTrackingRouter. See Client tracking./:clientId/subscriptionsmountsclientSubscriptionsRouter. See Client subscriptions.
The client object
Most endpoints return the client shaped byshapeClient in clients.repository.ts. It is every scalar column of the Client model plus three derived fields:
coaches: array of{ id, name }from theClientCoachjoin rows, oldest assignment first, with the primary coach (coachId) moved to the front.coverageEndsOn: the latestendsOnacross the client’s ownendsOnand itsSCHEDULED,ACTIVEandFROZENsubscriptions. This is the date to show as “covered until”.awaitingStart: thestartTriggerof the client’sAWAITING_STARTsubscription (INTAKE_FORM,FIRST_PLANorFIRST_WORKOUT), ornull.
productId, planName, priceAgorot, startedOn, endsOn, planFrozenOn, planRemainingDays and status on the client are a mirror of its primary subscription. syncMirror in the client-subscriptions service rewrites them after every subscription change. ClientSubscription is the source of truth.List and filters
GET /v1/web/clients
Returns one page of the studio’s trainees, newest first (createdAt descending), with the filtered total and three status bucket counts.
Auth: web lane or bearer lane, any role. A restricted coach only sees assigned trainees.
string
Exact match on the stored
Client.status. One of ACTIVE, PAUSED, CHURN_RISK, CHURNED, ARCHIVED.string
Only trainees with a
ClientCoach assignment to this coach id.string
Free text. Trimmed, then matched as described under “Search” below.
string
A JSON-encoded array of filter rules. See “Filter rules” below. A value that is not valid JSON, or does not match the rule schema, is ignored and filters nothing.
string
One of
active, inactive, inactiveWithAccess. See “Lifecycle” below.string
One of
onTrack, undocumented. See “Derived filters” below.string
One of
start, middle, ending, ended, expired. See “Derived filters” below.string
One of
today, week, none. See “Derived filters” below.number
default:"1"
Positive integer.
number
default:"20"
Positive integer, maximum 500.
coachId nor a rule can widen it.
Response: data holds items (client objects), total (rows matching the filters), statusCounts, page and pageSize. statusCounts splits the filtered set into active, inactive and inactiveWithAccess using the lifecycle conditions. active is computed as total minus the other two.
VALIDATION for a query that fails the Zod schema (for example pageSize over 500). UNAUTHORIZED when req.auth is missing. FORBIDDEN when the caller is a dismissed team member.
Search
searchConditions builds an OR of:
namecontains the term, case insensitive.emailcontains the term, case insensitive.israeliIdcontains the term.idequals the term.phonecontains the term. When the term has three or more digits, the digits are extracted, a leading972or234country code and leading zeros are stripped, and the phone is matched against each of: the raw digits, the core number, the core with a leading0, and the core with972or234in front.
Lifecycle
traineeStatusConditions derives these from the plan fields on the client, not from Client.status:
A trainee with no
endsOn and no freeze counts as active.
Derived filters
Defined inclients.derived-filters.ts. “Coverage end” means the latest end date across Client.endsOn and the live (SCHEDULED, ACTIVE, FROZEN) subscriptions.
Filter rules
rules is a JSON array. Each rule matches the filterRule schema in clients.schema.ts:
Rules are ANDed. A rule that is incomplete (for example an empty
values array or a blank number) produces no condition and filters nothing.
status
status
value is active, inactive or inactiveWithAccess and defaults to active. The retired values churned and frozen are read as inactive. The condition is the same lifecycle condition as the lifecycle query parameter. Operator isNot negates it.coach
coach
values is a list of coach ids. __none__ selects trainees with no coach assignment and can be mixed with real ids (OR). Operator notOneOf negates the condition.age
age
Computed from
birthDate at query time. Operators gt and lt read value and are strict: older than 40 starts at 41. Any other operator reads min and max as an inclusive range, and either bound alone is accepted. A trainee with no birth date never matches.formSubmitted and lastActivity
formSubmitted and lastActivity
Both read
n and unit as a look-back window. formSubmitted matches a trainee with a FormResponse created inside the window. lastActivity matches lastCheckInAt inside the window. Operator notWithin negates the condition, which also matches trainees with no data at all.platform
platform
values holds ios, android or __none__. A platform matches when lastCheckInPlatform equals it, or when lastCheckInPlatform is unknown and the trainee has a push token for that platform. __none__ matches trainees with no known check-in platform and no iOS or Android push token. Operator notOneOf negates.nutritionPlan, workoutPlan, subscriptionPlan
nutritionPlan, workoutPlan, subscriptionPlan
Defined in
clients.plan-filters.ts.nutritionPlanlooks at the trainee’s programs with statusACTIVEorPAUSEDand typeNUTRITIONorCOMBINED.workoutPlanlooks at programs with statusACTIVEorPAUSEDand typeTRAININGorCOMBINED.subscriptionPlanlooks at subscriptions with statusSCHEDULED,ACTIVEorFROZEN.
planOneOfandplanNotOneOfreadvalues. For programs the ids are matched againstsourceTemplateId. For subscriptions they are matched againstproductId.__none__selects trainees with no such plan.planCountIs,planCountAtLeastandplanCountAtMostreadnas a non-negative integer. Counts of 0 and 1 fold into the main query. Other counts run agroupByover the plan rows first, narrowed by the rest of the request, and feed the matching ids back into the list query.
subscriptionStart and subscriptionEnd
subscriptionStart and subscriptionEnd
min and max are YYYY-MM-DD dates read as whole UTC days. One bound alone reads as “from” or “until”. subscriptionStart compares Client.startedOn. subscriptionEnd compares the coverage end: the lower bound matches when the client endsOn or any live subscription reaches it, and the upper bound matches only when no live subscription runs past it. A trainee with no end date at all never matches an upper bound.gender
gender
value is MALE, FEMALE or __none__ (no gender set). Operator isNot negates.weight and goalWeight
weight and goalWeight
Compare
weightKg and goalWeightKg. Operators gt and lt read value. Any other operator reads min and max as an inclusive range, and either bound alone is accepted.joinDate
joinDate
Compares
createdAt. Operators within and notWithin read n and unit as a look-back window. Any other operator reads min and max as YYYY-MM-DD dates.daysToEnd
daysToEnd
value is a number of days, zero or more. Operator lt matches a trainee whose coverage is still running and ends within that many days. Operator gt matches a trainee whose coverage runs past them. A trainee with no end date matches neither.frozen
frozen
value is yes (planFrozenOn set) or no (planFrozenOn null).GET /v1/web/clients/selection
Returns the id and name of every trainee matching the same filters as the list, with no paging. The trainee board uses it for “select all” across the whole filtered set.
Auth: web lane or bearer lane, any role. Restricted coaches only get assigned trainees.
The query is parsed with the same clientListQuery schema as the list, so it accepts status, coachId, search, rules, lifecycle, health, stage and activity. page and pageSize are parsed but not used.
Response:
VALIDATION, UNAUTHORIZED, FORBIDDEN (dismissed team member).
GET /v1/web/clients/tag-options
Returns every distinct tag used on the studio’s trainees, sorted, for the tags filter. The query reads rows where deletedAt is null and drops blank tags.
Auth: web lane or bearer lane, any role. This endpoint is not narrowed by coach scope.
Response:
UNAUTHORIZED, FORBIDDEN (dismissed team member).
Saved filters and filter state
Saved filters (SavedTraineeFilter) are named presets. They belong to one staff member (userId) inside one studio. The filter state (TraineeFilterState) is the filter bar as that staff member last left it, one row per studio and user.
GET /v1/web/clients/saved-filters
Lists the caller’s saved filters, favorites first, then newest first.
Auth: web lane or bearer lane, any role.
Response: an array.
UNAUTHORIZED, FORBIDDEN (dismissed team member).
POST /v1/web/clients/saved-filters
Creates a saved filter for the caller. Responds with status 201.
Auth: web lane or bearer lane, any role.
string
required
Trimmed, 1 to 80 characters.
object[]
required
Array of objects. Stored as given. The rule shape is not validated here, only that each entry is an object.
id, name, rules, isFavorite, createdAt).
Errors: VALIDATION, UNAUTHORIZED.
PATCH /v1/web/clients/saved-filters/:id
Renames a saved filter or toggles its favorite star.
Auth: web lane or bearer lane, any role. Only the caller’s own filters in the caller’s studio match.
string
required
Saved filter id.
string
Trimmed, 1 to 80 characters.
boolean
Favorite flag.
id, name, rules, isFavorite, createdAt).
Errors: NOT_FOUND (“saved filter not found”) when no filter with this id belongs to the caller. VALIDATION for an empty patch (“no fields to update”).
DELETE /v1/web/clients/saved-filters/:id
Deletes one of the caller’s saved filters.
Auth: web lane or bearer lane, any role.
string
required
Saved filter id.
{ "deleted": true }. The service uses deleteMany scoped to the studio and user and does not check the count, so the response is the same when nothing matched.
Errors: VALIDATION, UNAUTHORIZED.
GET /v1/web/clients/filter-state
Returns the caller’s remembered filter bar. When no row exists, or the stored JSON no longer matches the schema, the service returns the empty state.
Auth: web lane or bearer lane, any role.
Response:
UNAUTHORIZED, FORBIDDEN (dismissed team member).
PUT /v1/web/clients/filter-state
Upserts the caller’s filter bar.
Auth: web lane or bearer lane, any role.
object[]
required
Array of objects, maximum 50 entries.
string
required
One of
all, active, inactive, inactiveWithAccess.string | null
required
onTrack, undocumented or null.string | null
required
start, middle, ending, ended, expired or null.string | null
required
today, week, none or null.VALIDATION, UNAUTHORIZED.
Create and messaging
POST /v1/web/clients
Creates a trainee. Responds with status 201.
Auth: web lane or bearer lane, any role.
string
required
At least 1 character.
string
required
At least 1 character.
string
required
At least 1 character. Stored in canonical form through
normalizePhoneOrNull.boolean
When true, sends the WhatsApp invite template after the trainee is created.
string
The subscription plan (product) to assign.
string[]
Coach ids to assign. Ids that are not coaches of this studio are dropped without an error. The first id becomes the primary coach (
coachId).string | null
Intake form to send. Must be a form of type
INTAKE.string | null
Check-in form to attach. Must be a form of type
CHECK_IN.string | null
WEEKLY, BIWEEKLY or MONTHLY.number | null
Integer 0 to 6.
number | null
Integer 1 to 31.
string
Valid email.
string
Free text.
string
Overrides the plan name copied from the product.
number
Non-negative integer, in agorot.
string
Valid URL.
string[]
default:"[]"
Tags.
date
Coerced to a date.
age is computed from it and stored.string
MALE, FEMALE or OTHER.number
Positive.
number
Positive.
number
Positive.
string
Free text.
number
Positive.
number
Non-negative integer.
number
Non-negative integer.
date
Subscription start. Coerced to a date.
date
Subscription end. Coerced to a date. When both dates are sent,
endsOn must be on or after startedOn.number
Positive integer. Only used when
endsOn is absent.string
DAYS or MONTHS.- Builds
namefrom first and last name and normalizes the phone. - When
productIdresolves to a product of the studio, it fills defaults from the product:planNamefrom the product name, unless sent.startedOnis the sent value. Without one it is now, except when only a pastendsOnwas sent. Then the start is back-dated fromendsOnby the duration.endsOnis the sent value, orstartedOnplusdurationValueanddurationUnit(body first, then product). If neither the body nor the product has a duration, the request fails.onboardingFormId,updateFormIdandpriceAgorotfrom the product, unless the body sets them.
- Validates the intake form type and the check-in form type. The check-in form’s
cadence,sendWeekdayandsendDayOfMonthbecome the defaults forupdateCadence,updateWeekdayandupdateDayOfMonthwhen the body does not set them. - Resolves
coachIds. A restricted coach who sends nocoachIdsis assigned automatically. A restricted coach must include themselves and cannot add any other coach. - Checks that the span runs forward, then inserts the client and its
ClientCoachrows. When billing plan limits apply, the count and the insert run inside the studio’s trainee lock (planLimits.withTraineeRoom). - If
sendWhatsappInviteis true, sends the SmartSend templatetrainee_invite_marketing(languagehe) with the first name and studio name. Failure is logged and does not fail the request. If SmartSend is not configured, a warning is logged and nothing is sent. - If the client has a
productId, callssubscriptions.createInitial. This creates the firstClientSubscriptionrow, syncs the mirror fields, fires the subscription-assigned automation hook and records aSUBSCRIPTION_PLAN/ASSIGNEDactivity. When neitherstartedOnnorendsOnwas sent, the studio’s start mode can hold the subscription asAWAITING_START. See Client subscriptions. - If the result has an
onboardingFormId, sends that form to the trainee through the forms service (assignForm). Failure is logged and does not fail the request. - If the result has an
updateFormId, records aFORM/ASSIGNEDactivity. The check-in form itself is sent later by the dispatcher on its cadence.
VALIDATION: body fails the schema, including “endsOn must be on or after startedOn”.BAD_REQUEST: the plan has no duration and none was sent. The form is the wrong type (“onboarding form must be intake”, “update form must be check-in”). The form is a document to sign (details.reasonisFORM_IS_DOCUMENT_TO_SIGN). The subscription would end before it starts.NOT_FOUND: “onboarding form not found” or “update form not found”.FORBIDDEN: the studio’s plan is full (details.reasonisPLAN_LIMIT_TRAINEES, withlimit,countandplanId). A restricted coach tried to assign another coach or leave themselves off.CONFLICT: a database constraint failed (PrismaP2002orP2003).
A
productId that does not belong to the studio is not rejected by the service. The product defaults are skipped and the id is still written, so the outcome depends on the database foreign key. This path was not confirmed further.POST /v1/web/clients/push-message
Sends a free-text push notification from the coach to a set of trainees.
Auth: web lane or bearer lane, any role. For a restricted coach, clientIds is first narrowed to the trainees assigned to them.
string[]
required
1 to 500 trainee ids.
string
required
1 to 1000 characters. Every occurrence of the token
[שם] is replaced with the trainee’s first name.TRAINER_MESSAGE, so the studio’s notification settings for that type apply. Each recipient gets the message as bodyOverride with the vars trainer (studio name, or “Perform”) and name.
For every trainee whose device accepted the message, it records a ClientActivity row with type MESSAGE, action SENT, entity type PUSH and the first 120 characters of the message as entityName. Trainees that were not reached get no activity row.
Response:
sent: trainees reached.selected: trainees found in the studio from the requested ids.unreachable:selectedminussent.disabled: true when the studio turned theTRAINER_MESSAGEtype off.
{ "sent": 0 } only.
Errors: VALIDATION. BAD_REQUEST (“push messaging is not available on this lane”) when no notifier is wired. The clients router wires one on both mounts.
Single trainee
Every route in this section runs behindrequireAssignedClient.
GET /v1/web/clients/:id
Returns one trainee.
Auth: web lane or bearer lane, any role, coach scope applied.
string
required
Client id.
NOT_FOUND (“client not found”).
PATCH /v1/web/clients/:id
Updates a trainee. Every field is optional. Sending null clears a nullable field.
Auth: web lane or bearer lane, any role, coach scope applied.
string
required
Client id.
string
At least 1 character.
name is recomposed when first or last name changes.string
At least 1 character.
string
At least 1 character. Normalized before saving.
string | null
Changes the plan.
null removes it. See the plan change steps below.string[]
Replaces the full set of assigned coaches. Unknown ids are dropped. The first id becomes the primary coach. An empty array removes all coaches.
string | null
Must be an
INTAKE form.string | null
Must be a
CHECK_IN form.string | null
WEEKLY, BIWEEKLY or MONTHLY.number | null
Integer 0 to 6.
number | null
Integer 1 to 31.
string | null
ISO date
YYYY-MM-DD. The date the biweekly cycle counts from. Stored as the start of that day in the studio timezone.string | null
ISO date
YYYY-MM-DD. A one-off date for the next check-in. Cannot be in the past (studio timezone) and cannot fall on a Saturday.string | null
ISO date
YYYY-MM-DD. A check-in date to skip.string | null
Valid email.
string | null
Free text.
string | null
Plan name override.
number | null
Non-negative integer.
string | null
Valid URL.
string[]
Replaces the tags.
string
ACTIVE, PAUSED, CHURN_RISK, CHURNED or ARCHIVED.date | null
age is recomputed, or cleared on null.string | null
MALE, FEMALE or OTHER.number | null
Positive.
number | null
Positive.
number | null
Positive, maximum 500.
number | null
Positive.
string | null
Free text.
number | null
Positive.
number | null
Non-negative integer.
number | null
Non-negative integer.
date | null
Subscription start.
date | null
Subscription end. When both dates are in the body,
endsOn must be on or after startedOn.number
Positive integer. Used to derive
endsOn on a plan change.string
DAYS or MONTHS.boolean
Lets a frozen or ended trainee keep app access.
- Un-archive check. Moving a trainee out of
ARCHIVEDcallsplanLimits.assertCanAddTrainees, because the trainee counts toward the plan again. - Plan change (
productIddiffers from the stored one):subscriptions.assertReplaceablerefuses when the trainee has aSCHEDULEDorAWAITING_STARTsubscription.planFrozenOnandplanRemainingDaysare cleared andappAccessWhileFrozenis set to false.- With
productId: null,planName,endsOnandpriceAgorotare cleared unless the body sets them. - With a new product, the same defaults as on create are applied: plan name, start date (body, then the stored one, then back-dated or now), end date from the duration, forms and price.
- After the client row is saved,
subscriptions.replaceActivecancels theACTIVEandFROZENsubscriptions, creates a new subscription from the client’s dates when it has a product and both dates, syncs the mirror and fires the subscription-assigned hook. - A
SUBSCRIPTION_PLANactivity is recorded with actionREMOVED,EDITEDorASSIGNED.
- Forms. A changed
onboardingFormIdorupdateFormIdrecords aFORMactivity (REMOVED,EDITEDorASSIGNED). WhenupdateFormIdchanges and the body sets no schedule, the cadence, weekday and day of month default to the form’s own. A cadence change with no form change records aFORM/EDITEDactivity. - Check-in dates (
checkInDatePatch). When the cadence, weekday or day of month changes, or the check-in form is removed, and the body sets neitherupdateNextOnnorupdateSkipOn, both are cleared. A storedupdateAnchorOnthat is still in the future is also cleared unless the body sets one. - Coaches. With
coachIds, theClientCoachrows are deleted and recreated in one transaction. A restricted coach must stay on the trainee, cannot add another coach and cannot remove another coach. When the set changed,syncClientCoachTasksmoves the trainee’s open automated tasks (InboxItemrows with sourceauto) to the new coaches. A failure there is logged and does not fail the request. - Date span. When the patch touches
startedOnorendsOn, the merged pair (patch plus stored row) must run forward. - Status. When
statusisACTIVE,PAUSEDorCHURNED, the service callssubscriptions.resyncafter saving. That runsreconcileandsyncMirror, andsyncMirrorderives the status from the subscriptions again (deriveClientStatus). The stored status can therefore differ from the one sent.CHURN_RISKandARCHIVEDare kept as sent.
NOT_FOUND: “client not found”, “onboarding form not found”, “update form not found”.VALIDATION: schema failure. “updateNextOn is in the past”. “updateNextOn cannot fall on a Saturday”. “cancel the scheduled subscription before changing the plan”, withdetails.conflicts.BAD_REQUEST: the new plan has no duration and none was sent. Wrong form type. The dates would end before they start.FORBIDDEN: plan limit on un-archive (PLAN_LIMIT_TRAINEES). Coach assignment rules (“you must stay assigned to this trainee”, “cannot assign a trainee to another coach”, “cannot unassign another coach from this trainee”).
DELETE /v1/web/clients/:id
Deletes the trainee. This is a hard delete (prisma.client.deleteMany). Every model that references the client cascades, so photos, logs, programs, form data, messages, subscriptions, activities and push tokens go with it.
Auth: web lane or bearer lane, any role, coach scope applied.
string
required
Client id.
{ "deleted": true }.
Errors: NOT_FOUND (“client not found”).
POST /v1/web/clients/:id/freeze
Freezes the trainee’s active subscription. No body.
Auth: web lane or bearer lane, any role, coach scope applied.
string
required
Client id.
subscriptions.freeze. Inside one transaction it runs reconcile, finds the ACTIVE subscription, computes the remaining days (rounded up), sets the subscription to FROZEN with planFrozenOn and planRemainingDays, and syncs the mirror. The client status becomes PAUSED unless it is CHURN_RISK or ARCHIVED.
Side effects: a SUBSCRIPTION_PLAN / EDITED activity row, and a SUBSCRIPTION_FROZEN push to the trainee with the studio name.
Response: the client object, now with planFrozenOn and planRemainingDays set.
Errors:
NOT_FOUND: “client not found”.VALIDATION: “plan is already frozen”. “client has no active plan”. “plan has already expired”. “cancel the scheduled subscription before freezing the plan”, withdetails.conflicts, when aSCHEDULEDorAWAITING_STARTsubscription exists.
POST /v1/web/clients/:id/reactivate
Unfreezes a frozen subscription. No body.
Auth: web lane or bearer lane, any role, coach scope applied.
string
required
Client id.
subscriptions.reactivate. The frozen subscription becomes ACTIVE with a new endsOn of now plus planRemainingDays. If a SCHEDULED subscription starts before that date, endsOn is cut to the scheduled start. planFrozenOn and planRemainingDays are cleared and the mirror is synced, which also resets appAccessWhileFrozen to false.
Side effect: a SUBSCRIPTION_PLAN / EDITED activity row. No push is sent.
Response: the client object.
Errors: NOT_FOUND (“client not found”). VALIDATION (“plan is not frozen”).
POST /v1/web/clients/:id/avatar
Uploads a profile picture as base64 and stores its URL on the trainee.
Auth: web lane or bearer lane, any role, coach scope applied.
string
required
Client id.
string
required
Base64 image data. The decoded image must be between 1 byte and 2,000,000 bytes.
string
required
image/png or image/jpeg.clients/<id>.png or clients/<id>.jpg, replacing any earlier file at that key. avatarUrl is saved as the public URL with a ?v=<timestamp> cache-busting suffix.
Response: the client object with the new avatarUrl.
Errors: NOT_FOUND. VALIDATION (“avatar image is empty”, “avatar image is too large”, or a schema failure).
POST /v1/web/clients/:id/preview-token
Mints a short-lived trainee token so the coach web app can load the trainee app as this trainee, in preview mode. No body.
Auth: web lane or bearer lane, any role, coach scope applied.
string
required
Client id.
createTraineeToken with the payload { clientId, studioId, preview: true }, signed with BETTER_AUTH_SECRET, and expires after PREVIEW_TTL_SECONDS (900 seconds).
Response:
NOT_FOUND.
GET /v1/web/clients/:id/photos
Lists the trainee’s progress photos (ClientPhoto), newest takenAt first.
Auth: web lane or bearer lane, any role, coach scope applied.
string
required
Client id.
pose is FRONT, SIDE, BACK or null. The values stored in source are not defined in this module.
Errors: NOT_FOUND.
GET /v1/web/clients/:id/weights
Lists the trainee’s weigh-ins (WeightEntry), oldest recordedAt first.
Auth: web lane or bearer lane, any role, coach scope applied.
string
required
Client id.
NOT_FOUND.
Board and activity
GET /v1/web/clients/:id/board
Returns everything the trainee card needs in one call: the client, plans, subscriptions, forms, meetings, open tasks and the activity feed.
Auth: web lane or bearer lane, any role, coach scope applied.
string
required
Client id.
data has these keys.
The activity feed is assembled by
assembleActivity from six sources, each capped at 500 rows:
Day boundaries use the trainee’s own timezone when set, otherwise the studio’s.
client is shortened in the example. Only recorded ClientActivity entries carry actorId.
Errors: NOT_FOUND.
POST /v1/web/clients/:id/activity/notes
Adds a free-text coach note to the trainee’s activity feed. Responds with status 201.
Auth: web lane or bearer lane, any role, coach scope applied.
string
required
Client id.
string
required
Trimmed, at least 1 character. No maximum length.
ClientActivity row with type COACH_NOTE, actor type TRAINER, the caller’s user id as actorId, the coach’s name as actorName (looked up from the Coach row) and the text in metadata.note.
Response:
NOT_FOUND (“client not found”). VALIDATION (empty text).
DELETE /v1/web/clients/:id/activity/notes/:activityId
Deletes a coach note. Only notes can be deleted, and only by their author.
Auth: web lane or bearer lane, any role, coach scope applied. The caller’s user id must equal the note’s actorId.
string
required
Client id.
string
required
The
ClientActivity row id.{ "deleted": true }.
Errors: NOT_FOUND (“note not found”) when the row does not exist for this trainee or is not a COACH_NOTE. FORBIDDEN (“only the author can delete a note”).
Food preferences
A trainee’s food preferences are the food categories and foods they do not eat. The effective value is the coach’s saved override when one exists. Otherwise it is the answer from the newest submitted form response that has afood_preferences field (the last 10 submissions of those forms are scanned).
GET /v1/web/clients/:id/food-preferences
Returns the effective food preferences and where they came from.
Auth: web lane or bearer lane, any role, coach scope applied.
string
required
Client id.
source:coach,formornone.origin: the form response the answer was read from, ornull.excludedCount: how many foods in the studio’s visible food library (global rows plus studio rows, with per-studio category overrides applied) are excluded. A food is excluded when its id is inexcludedFoodIds, or when its primary category is inexcludedCategoryIdsand its id is not inincludedExceptionIds.
NOT_FOUND (“client not found”).
PUT /v1/web/clients/:id/food-preferences
Saves a coach override. From then on the override wins over form answers.
Auth: web lane or bearer lane, any role, coach scope applied.
string
required
Client id.
string[]
default:"[]"
Up to 5000 ids, each 1 to 128 characters.
string[]
default:"[]"
Up to 5000 ids, each 1 to 128 characters.
string[]
default:"[]"
Foods kept even though their category is excluded. Up to 5000 ids, each 1 to 128 characters.
Client.foodPreferences and foodPreferencesUpdatedAt is set to now.
Response: same shape as the GET, with source set to coach, value set to the saved body and updatedAt set to the save time. origin still names the form response, if one exists.
Errors: NOT_FOUND (“client not found”). VALIDATION.