/v1/trainee, router traineeRouter in src/modules/trainee/trainee.routes.ts. The two OTP routes are public. switch-studio requires a trainee token. All three sit behind the global /v1 rate limiter (120 requests per 60 seconds per IP by default).
Flow
Phone matching
A trainee is aClient row with a phone. Matching is done on the national core of the number, computed by nationalCore in trainee.service.ts:
- Keep digits only.
- Drop a leading
972or234country code. - Drop leading zeros.
phoneMatchVariants), limited to clients that are not deleted and whose studio is not archived, newest first. The service then keeps only candidates whose own national core is exactly equal. That second step stops a short number from signing in as the holder of a longer number that contains it.
One phone can belong to trainees in several studios. The newest account signs in, and the response lists all of them in studios so the app can offer a picker and call switch-studio.
The trainee token
trainee.token.ts builds the token without a library:
Regular sessions never expire. There is no refresh endpoint and no server-side session table. A token stops working when:
- the signature no longer verifies, for example after
BETTER_AUTH_SECRETis rotated, - the studio is archived (
studio no longer exists, checked on every request with a 30 second cache), - the client or studio is deleted, or the app is locked, on routes that use the
accessguard.
DELETE /v1/trainee/push-token. See Profile and home.
Preview tokens
A coach can open the trainee app as one of their trainees from the web dashboard.POST /v1/web/clients/:id/preview-token (clients module, web gateway lane) returns { token, expiresInSeconds } with preview: true and a 15 minute expiry (PREVIEW_TTL_SECONDS = 900).
A preview token behaves like the trainee with three differences:
- Any request that is not a
GETreturns 403FORBIDDENwithdetails.reason = "PREVIEW_READ_ONLY". - It never updates
Client.timezone. GET /v1/trainee/meandGET /v1/trainee/homedo not stamplastCheckInAt, so a coach looking at the app does not count as trainee activity.
App lock
Routes guarded byrequireTraineeAppAccess return 403 FORBIDDEN with details.reason = "APP_LOCKED" when the trainee’s subscription is frozen (planFrozenOn set) or its coverage has ended, unless the coach set appAccessWhileFrozen to true. The sign-in routes and GET /v1/trainee/me are not guarded, so a locked trainee can still sign in. The client.appLocked flag in the sign-in and me responses tells the app to show the lock screen instead of calling guarded routes.
Endpoints
POST /v1/trainee/auth/request-otp
Sends a 6-digit code to the phone over WhatsApp.
Auth: public.
string
required
At least 6 characters. Any formatting is accepted. Only the digits are used.
- Rejects a number whose national core is shorter than 6 digits.
- Looks up trainee accounts for the number. No match returns 404, so the endpoint does reveal whether a number is registered.
- Increments
perform:trainee-otp-throttle:phone:<core>in Redis. The first hit sets a 600 second expiry. More than 3 sends in the window returns 429. - Generates the code with
crypto.randomInt(100000, 1000000)and stores it atperform:trainee-otp:<core>for 300 seconds. A new request overwrites the previous code. - Sends the WhatsApp template
registercodein Hebrew through SmartSend, with the code as both the body parameter and the URL button parameter. The sending organization isSMARTSEND_OTP_API_KEY, falling back toSMARTSEND_API_KEY, falling back to the studio’s ownsettings.smartsend.apiKey.
TRAINEE_OTP_DEV_MODE is true, no WhatsApp message is sent. The code is logged and returned in the response as devCode. Never enable this in production.
Response: 200.
data is { "sent": true, "devCode": "482913" }.
Errors:
POST /v1/trainee/auth/verify-otp
Exchanges the code for a trainee token.
Auth: public.
string
required
The same number used to request the code. At least 6 characters.
string
required
4 to 8 characters. Codes issued by this API are 6 digits.
string
ios or android. Stored as Client.lastCheckInPlatform.- Compares the code with the value stored in Redis. A missing or different value returns 400. The status is 400 and not 401 on purpose: the mobile client signs the user out on any 401, and a mistyped code must not do that.
- Loads the trainee accounts for the phone and picks the newest.
- Deletes the stored code so it cannot be used twice.
- Stamps
lastCheckInAtand the platform, closes any open automaticINACTIVEinbox item for the trainee, and records aClientAppDayrow for today. - Signs a token for the chosen account.
TRAINEE_REVIEW_PHONE and TRAINEE_REVIEW_CODE are both set and the request matches both, the Redis check is skipped. The values are environment configuration and are not documented here.
The service keeps no counter of wrong codes. A wrong code leaves the stored one in place until it expires, so the only brake on guessing is the global per-IP limiter.
Response: 200.
studios carries the full studio view. It is shortened here. The values of branding, mbpAnchors and mbpUnitLabel are examples. branding is the studio’s stored JSON passed through projectBranding, and the anchor numbers come from the studio settings.
string
active, paused (frozen), scheduled, waiting or none.string | null
When coverage ends, including plans queued behind the current one.
number | null
Whole days until
endsOn, only for active and paused.object | null
The next scheduled plan as
{ planName, startedOn, endsOn }.string | null
For a
waiting subscription: intake_form, first_plan or first_workout.string | null
The studio’s
settings.contactWhatsapp, or the assigned coach’s phone.boolean
false only when settings.calendarEnabled is explicitly false.POST /v1/trainee/auth/switch-studio
Issues a token for another studio the same phone number trains at, without a new code. The current token already proves the phone.
Auth: trainee token. No app access guard, so it works for a locked account. Preview tokens are refused because the method is not GET.
string
required
The
clientId of the target account, taken from the studios list.string
ios or android.verify-otp: token, client, studio and studios for the target account.
Errors:
Related
- Authentication for the lane-level errors every trainee route can return.
- Profile and home for
GET /v1/trainee/me, account deletion and push tokens.