The partner lane is a read-mostly API that SmartSend calls to show a trainee’s Perform data next to a WhatsApp conversation, to build campaign audiences and to act on a subscription. Its response shapes are a wire contract with SmartSend. Treat them as frozen. Mount: /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 422 VALIDATION.
  • One exception. Freeze and reactivate rejections return 409 with { "error": { "code": "FREEZE_REJECTED", "message": "..." } }.
  • Auth errors. 401 UNAUTHORIZED with missing partner api key or invalid partner api key. See Authentication.
  • Rate limit. Only the global /v1 limiter: 120 requests per 60 seconds per IP by default. The module adds none.
  • Trainee ids. :id is always a Client.id of the key’s studio. Any other id, and a soft-deleted trainee, returns 404 trainee not found.
  • Dates. Instants are ISO strings. Day keys are YYYY-MM-DD in the trainee’s time zone (client zone, then studio zone, then Asia/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, and prefix, 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 revokedAt set, or whose studio is archived, is refused.
  • lastUsedAt is 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.
Errors: 403 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.
Response: 201. secret is the only time the full key is available.
Errors: 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.
Revoking an already revoked key succeeds and leaves the original revokedAt. Webhooks registered with the key through the automation lane are not deleted by this call. Response: 200.
Errors: 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.
Errors: 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.
The number is normalized with 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.
Errors: 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.
Response: 200.
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.
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.
Today’s nutrition numbers use the same math as the trainee’s home screen: the saved day snapshot priced against the plan, plus free meal logs that the snapshot does not already account for. Errors: 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.
Exercise names are resolved from the library by 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.
Errors: 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.
Every day in the range is returned, including days with nothing logged. Each day maps to a plan day by its offset from today in the trainee’s zone, using the newest active nutrition plan as it is now. Past days are therefore priced against the current plan, not the plan that was active then. Response: 200.
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.
Response: 200.
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.
Answers are built from the response’s own schema snapshot, so they reflect the form as it was when answered. Display-only blocks are skipped. Formatting (formatAnswerValue): Response: 200.
Errors: 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.
Response: 200.
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.
Response: 200. With no active program of that type the body is { "program": null }. A training program:
A nutrition program has the same top level, with days shaped like this:
A file plan returns 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.
Returns InboxItem rows with status OPEN or SNOOZED, newest first. Response: 200.
Errors: 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.
No body. The call goes through the same 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.
No body. Mirrors the freeze call through 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.
The feed is assembled by assembleActivity in the clients service from recorded coach actions plus the trainee’s own meals, completed workouts, form responses and technique videos. Response: 200.
The first entry is a note. Its text is in 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.
How the author is resolved (resolvePartnerActor):
  • The studio’s settings.smartsend.userMap maps Perform coach ids to SmartSend users. If author.id matches a mapped user and that coach has an externalUserId, 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 labelled SmartSend · <author.name>, or just SmartSend without a name.
An AuditLog row with action partner.activity_note.create records the API key. Response: 201.
Errors: 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.
An AuditLog row with action partner.activity_note.delete is written. Response: 200.
Errors:

Audience reports

Reports return lists of trainees for SmartSend’s campaign wizard. A report’s clients become a mailing list, and every context key becomes a per-recipient field usable as a template variable. Rules for every report (reports.service.ts, reports.repository.ts):
  • Only clients with status ACTIVE and no deletedAt are 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.
  • phoneNumber is international digits without a plus sign. A local number starting with 0 becomes 972 plus the rest.
  • Names: firstName and lastName when stored, else the full name split on the first space.
  • context values 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.
The catalog has five entries: 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.
Query parameters and meaning per report: Notes:
  • workout-goal counts 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-expiry reports endsOnDate and daysRemaining from the coverage end, which includes queued plans.
  • The days parameter of plan-expiry is validated for expired too, but only used for expiring.
Response: 200.
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:
  • inactive accepts days from 1, not 3.
  • inactive does not include every trainee who never checked in. New trainees are excluded until they are older than the window.
  • birthdays uses the studio’s time zone, not UTC.
  • plan-expiry excludes trainees who already have a scheduled follow-up plan.
  • The catalog description strings no longer contain a dash.