/v1/partner, router partnerRouter in src/modules/partner/partner.routes.ts.
Lane: partner. Every route runs requirePartnerKey. The caller sends a studio API key and every query is scoped to that key’s studio.
For Make.com and other automation tools use the Automation API. It accepts the same keys but has its own envelope, input coercion and write endpoints. Do not send automation traffic here.
Conventions on this lane
- No envelope on success. Controllers call
res.json(result)directly. The body is the object shown in each example. - Standard envelope on errors. Failures pass through the global error middleware:
{ "ok": false, "code": "...", "message": "...", "requestId": "..." }. Validation is 422VALIDATION. - One exception. Freeze and reactivate rejections return 409 with
{ "error": { "code": "FREEZE_REJECTED", "message": "..." } }. - Auth errors. 401
UNAUTHORIZEDwithmissing partner api keyorinvalid partner api key. See Authentication. - Rate limit. Only the global
/v1limiter: 120 requests per 60 seconds per IP by default. The module adds none. - Trainee ids.
:idis always aClient.idof the key’s studio. Any other id, and a soft-deleted trainee, returns 404trainee not found. - Dates. Instants are ISO strings. Day keys are
YYYY-MM-DDin the trainee’s time zone (client zone, then studio zone, thenAsia/Jerusalem).
API keys
Keys are managed from the coach web app on the web gateway lane. Mount:/v1/web/studios/current/api-keys, router studioApiKeysRouter in src/modules/partner/api-keys.routes.ts.
Auth: web gateway lane (HMAC service signature plus user context), then requireRole(OWNER, HEAD_COACH). Responses use the standard envelope.
How keys work (api-keys.service.ts, partner-auth.ts):
- A key is
pf_live_followed by 40 hex characters from 20 random bytes. - The database stores
keyHash, the SHA-256 hex of the full key, andprefix, its first 15 characters, for display. The plain key is never stored and cannot be read back. - A request is authenticated by hashing the presented key and looking the hash up. A key with
revokedAtset, or whose studio is archived, is refused. lastUsedAtis stamped at most once every 5 minutes per key, in the background.- Keys have no scopes and no expiry. Any key of a studio can call every partner and automation endpoint for that studio.
GET /v1/web/studios/current/api-keys
Lists the studio’s keys, newest first, including revoked ones.
Auth: web gateway, role OWNER or HEAD_COACH.
Response: 200.
FORBIDDEN insufficient role.
POST /v1/web/studios/current/api-keys
Creates a key and returns its secret once.
Auth: web gateway, role OWNER or HEAD_COACH.
string
required
A label for the key. Trimmed, 1 to 100 characters.
secret is the only time the full key is available.
VALIDATION (422) for a missing or too long name. 403 insufficient role.
POST /v1/web/studios/current/api-keys/:id/revoke
Revokes a key. Requests with it start failing with 401 immediately.
Auth: web gateway, role OWNER or HEAD_COACH.
string
required
The key id.
revokedAt. Webhooks registered with the key through the automation lane are not deleted by this call.
Response: 200.
NOT_FOUND (404) api key not found. 403 insufficient role.
Connection
GET /v1/partner/ping
Checks the key and returns the studio it belongs to.
Auth: studio API key.
Response: 200.
NOT_FOUND (404) studio not found.
Finding a trainee
GET /v1/partner/trainees/by-phone/:phone
Finds the trainee with a given phone number in the key’s studio.
Auth: studio API key.
string
required
At least 6 characters. Any format.
normalizePartnerPhone: digits only, a leading 972 becomes a leading 0, and a bare 9-digit number gets a leading 0. The lookup then matches on the same phone variants as trainee sign-in.
Response: 200 in both cases. This endpoint never returns 404 for a miss.
VALIDATION (422) when phone is shorter than 6 characters.
Reading a trainee
GET /v1/partner/trainees/:id/overview
One call for the trainee card: profile, deep links into the coach app, subscriptions, today’s nutrition and steps, training summary, check-in counters, weight change, before and after photos and the latest check-in answers.
Auth: studio API key.
string
required
The trainee id.
number | null
The latest weigh-in, falling back to the profile value.
string | null
The latest of
lastCheckInAt, the newest form response and the client’s updatedAt.object
Built from
APP_WEB_URL, the studio slug and the trainee id.array
Every subscription of the trainee, newest
startedOn first, in any status.number | null
Weeks since the running (
ACTIVE or FROZEN) subscription started, to one decimal.string
grams or mbp. In mbp mode (studio portion system on) the protein, carbs and fat numbers and targets are in portion units and targets.kcal is the sum of the three. In grams mode values are rounded to whole numbers.number | null
The trainee’s own
stepsGoal. stepsAvg7d averages the days of the last 7 that have a step value.object | null
Today’s plan-day targets.
null with no regular nutrition plan.object
Count and latest date of submitted check-in forms.
object
The first numeric
rating answer of the latest check-in, with its star count. All null when there is none.object
intake comes from posed photos of the first intake response, latest from the newest check-in that has posed photos. When either is missing, posed photos that are not tied to a form are used: the earliest day for intake, the latest for latest, with the nearest weigh-in within 60 days as the weight. Either can be null.array
Only
text, number and rating answers of the latest check-in, formatted as on the web response page.NOT_FOUND (404) trainee not found or studio not found.
GET /v1/partner/trainees/:id/workouts
Lists the trainee’s workout logs, completed or not, with exercises and sets.
Auth: studio API key.
string
required
The trainee id.
number
default:"1"
Positive integer.
number
default:"20"
Positive integer, at most 100.
exerciseId, falling back to the name stored in the log entry, then to a generic Hebrew placeholder. Sets explicitly marked done: false are left out.
Response: 200.
VALIDATION (422) for bad paging values. NOT_FOUND (404) trainee not found.
GET /v1/partner/trainees/:id/nutrition-days
Returns consumed and target macros per day for a date range.
Auth: studio API key.
string
required
The trainee id.
string
required
YYYY-MM-DD.string
required
YYYY-MM-DD. Not before from. The range may span at most 62 days.target is null with no regular nutrition plan. In mbp mode all four numbers are portion units, as on the overview.
Errors:
GET /v1/partner/trainees/:id/forms
Lists the trainee’s form responses.
Auth: studio API key.
string
required
The trainee id.
formName is the coach-facing template name.
Errors: NOT_FOUND (404) trainee not found.
GET /v1/partner/trainees/:id/forms/:responseId
Returns one form response with every answer formatted for display.
Auth: studio API key.
string
required
The trainee id.
string
required
The form response id.
formatAnswerValue):
Response: 200.
NOT_FOUND (404) trainee not found or form response not found.
GET /v1/partner/trainees/:id/weights
Lists every weigh-in of the trainee.
Auth: studio API key.
string
required
The trainee id.
source is the stored value: APP, form, COACH, automation, an autofit marker or null for old rows.
Errors: NOT_FOUND (404) trainee not found.
GET /v1/partner/trainees/:id/programs
Returns the trainee’s newest active program of one type, rendered for display.
Auth: studio API key.
string
required
The trainee id.
string
required
TRAINING or NUTRITION.{ "program": null }.
A training program:
pdfUrl and days: []. sets is the number of set rows, or the row’s sets value. display is the ready-made quantity text, or null for foods without display unit data. Unknown exercises and foods get generic Hebrew placeholder names.
Errors: VALIDATION (422) when type is missing or invalid. NOT_FOUND (404) trainee not found or studio not found.
GET /v1/partner/trainees/:id/tasks
Lists the open coach tasks about this trainee.
Auth: studio API key.
string
required
The trainee id.
InboxItem rows with status OPEN or SNOOZED, newest first.
Response: 200.
NOT_FOUND (404) trainee not found.
Subscription actions
POST /v1/partner/trainees/:id/freeze
Freezes the trainee’s running plan.
Auth: studio API key.
string
required
The trainee id.
ClientSubscriptionsService.freeze the coach web app uses, with the API key id as the actor and the same notifier wiring. An AuditLog row with action partner.freeze is written.
Response: 200.
subscription is the trainee’s FROZEN subscription after the call, or null if none is found.
Errors:
POST /v1/partner/trainees/:id/reactivate
Reactivates a frozen plan.
Auth: studio API key.
string
required
The trainee id.
ClientSubscriptionsService.reactivate and writes an AuditLog row with action partner.reactivate.
Response: 200 with { "ok": true, "subscription": { ... } }, where subscription is the trainee’s ACTIVE subscription after the call.
Errors: 409 FREEZE_REJECTED, for example plan is not frozen. The error code is FREEZE_REJECTED for both actions. 404 trainee not found.
Activity and notes
GET /v1/partner/trainees/:id/activity
Returns the trainee’s activity feed, the same one the coach sees on the trainee card.
Auth: studio API key.
string
required
The trainee id.
string
The partner-side user id of the person viewing, 1 to 80 characters. When given, the response includes the actor id that user maps to, so the client can tell which notes are theirs to delete.
assembleActivity in the clients service from recorded coach actions plus the trainee’s own meals, completed workouts, form responses and technique videos.
Response: 200.
metadata.note. The type and actorType strings shown for it are illustrative: the stored values come from COACH_NOTE_ACTIVITY_TYPE and recordActivity in the clients module. Derived entries use prefixed ids: workout:, form: and video:. For those, entityId is the program or form template id, not the log id. viewerActorId is null without viewer.
Errors: VALIDATION (422) when viewer is empty or too long. NOT_FOUND (404) trainee not found.
POST /v1/partner/trainees/:id/activity/notes
Adds a note to the trainee’s timeline on behalf of a partner-side user.
Auth: studio API key.
string
required
The trainee id.
string
required
At most 10,000 characters. Must not be blank after trimming.
object
required
Who is writing.
string
required
The partner-side user id. Trimmed, 1 to 80 characters.
string
Display name. Trimmed, at most 120 characters.
resolvePartnerActor):
- The studio’s
settings.smartsend.userMapmaps Perform coach ids to SmartSend users. Ifauthor.idmatches a mapped user and that coach has anexternalUserId, the note is written as that coach. The coach can then see and delete it inside Perform too. - Otherwise the note is written under the actor id
smartsend:<author.id>and labelledSmartSend · <author.name>, or justSmartSendwithout a name.
AuditLog row with action partner.activity_note.create records the API key.
Response: 201.
VALIDATION (422) when the body fails the schema or the text is blank (note text is required). NOT_FOUND (404) trainee not found or studio not found.
DELETE /v1/partner/trainees/:id/activity/notes/:activityId
Deletes a note. Only its author can delete it.
Auth: studio API key.
string
required
The trainee id.
string
required
The note’s activity id.
string
required
The partner-side user id of the person deleting, 1 to 80 characters. Resolved to an actor the same way as on create.
AuditLog row with action partner.activity_note.delete is written.
Response: 200.
Audience reports
Reports return lists of trainees for SmartSend’s campaign wizard. A report’s clients become a mailing list, and everycontext key becomes a per-recipient field usable as a template variable.
Rules for every report (reports.service.ts, reports.repository.ts):
- Only clients with
statusACTIVEand nodeletedAtare considered. Frozen, paused and archived trainees are never part of a messaging audience. - Clients with no digits in their phone are skipped and counted in
skippedNoPhone. phoneNumberis international digits without a plus sign. A local number starting with0becomes972plus the rest.- Names:
firstNameandlastNamewhen stored, else the full name split on the first space. contextvalues are flat strings or numbers. A key is omitted when it has no value.- A report whose base filter matches more than 20,000 clients returns 400
report too large. - Rows are ordered by name.
GET /v1/partner/reports
Returns the static report catalog. SmartSend renders its picker from it, so a new report added in reports.schema.ts appears there without a SmartSend deploy.
Auth: studio API key.
Response: 200.
workout-goal, inactive, plan-expiry, birthdays and open-forms. Descriptions are in Hebrew. params is omitted for reports without parameters.
Errors: none beyond the lane errors.
GET /v1/partner/reports/:key
Runs one report.
Auth: studio API key.
string
required
workout-goal, inactive, plan-expiry, birthdays or open-forms.
Notes:
workout-goalcounts every workout log in the window, whether or not it is marked completed. The goal is the constant 3, not the trainee’s plan frequency.plan-expiryreportsendsOnDateanddaysRemainingfrom the coverage end, which includes queued plans.- The
daysparameter ofplan-expiryis validated forexpiredtoo, but only used forexpiring.
total is the number of clients returned, after skipping those without a phone.
Errors:
Differences from the older notes
backend/docs/PARTNER_REPORTS.md predates some of the code. Where they differ, the code above is right:
inactiveacceptsdaysfrom 1, not 3.inactivedoes not include every trainee who never checked in. New trainees are excluded until they are older than the window.birthdaysuses the studio’s time zone, not UTC.plan-expiryexcludes trainees who already have a scheduled follow-up plan.- The catalog description strings no longer contain a dash.