@trainheroic-unofficial/cli 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1361 @@
1
+ # TrainHeroic Coach API Documentation
2
+
3
+ Base URL: `https://api.trainheroic.com`
4
+
5
+ ## Authentication
6
+
7
+ **Login:** `POST https://apis.trainheroic.com/auth` (form post with email/password)
8
+
9
+ Returns:
10
+
11
+ ```json
12
+ {
13
+ "id": 444862,
14
+ "api_token": "...",
15
+ "refresh_token": "...",
16
+ "api_ttl": 7712.75,
17
+ "scope": "athlete",
18
+ "role": "athlete",
19
+ "session_id": "..."
20
+ }
21
+ ```
22
+
23
+ **Coach session:** The coach platform stores `session_id` in localStorage (`persist:trainheroic`) and uses it as the `session-token` header for all API calls.
24
+
25
+ **Athlete API token:** The `apis.trainheroic.com` login also returns an `api_token` which is used with the `api-token` header (used by `apis.trainheroic.com/user` endpoint).
26
+
27
+ **Headers:**
28
+
29
+ - `session-token: <session_id>` — used by coach platform (library, builder, coachapp)
30
+ - `api-token: <api_token>` — used by `apis.trainheroic.com` web app
31
+ - `content-type: application/json`
32
+
33
+ ---
34
+
35
+ ## Coach Platform Subdomains
36
+
37
+ | Subdomain | Purpose |
38
+ | -------------------------- | -------------------------------------------- |
39
+ | `apis.trainheroic.com` | Login portal, athlete web dashboard |
40
+ | `coach.trainheroic.com` | Athletes/Teams admin (AngularJS) |
41
+ | `library.trainheroic.com` | Exercise/Session/Program library (React/MUI) |
42
+ | `builder.trainheroic.com` | Session template builder |
43
+ | `coachapp.trainheroic.com` | Program builder (calendar view) |
44
+ | `teams.trainheroic.com` | Team management |
45
+
46
+ ---
47
+
48
+ ## Endpoints
49
+
50
+ ### User / Auth
51
+
52
+ | Method | Endpoint | Description |
53
+ | ------ | ---------------------------------- | -------------------------------------------- |
54
+ | GET | `/user/simple` | Current user profile (simplified) |
55
+ | GET | `/v5/users` | Current user info |
56
+ | GET | `/v5/users/{id}` | User by ID |
57
+ | GET | `/v5/users/{id}/features` | Feature flags for user |
58
+ | GET | `/v5/headCoach` | Head coach info (org, license, trial status) |
59
+ | GET | `/v5/coaches/orgs` | Coach organizations |
60
+ | GET | `/v5/userAgreementTerms/hasAgreed` | TOS agreement status |
61
+ | GET | `/avatars/user/{id}` | User avatar (302 redirect to static image) |
62
+
63
+ #### `GET /user/simple` Response
64
+
65
+ ```json
66
+ {
67
+ "id": 2771594,
68
+ "profileImg": "https://static.trainheroic.com/avatar-2025/J/avatar-JI.png",
69
+ "coverImg": "https://static.trainheroic.com/images/defaults/5.png",
70
+ "firstName": "jamon",
71
+ "lastName": "iberico",
72
+ "username": "neat.nest6259@fastmail.com",
73
+ "email": "neat.nest6259@fastmail.com",
74
+ "is_active": true,
75
+ "days_from_login": 0,
76
+ "roles": ["COACH", "TRIAL"],
77
+ "hasRole": { "COACH": 1, "TRIAL": 1 },
78
+ "org_id": 602402,
79
+ "mpEnabled": null,
80
+ "use_metric": false,
81
+ "fdhqUser": false,
82
+ "trial_days_remaining": 14
83
+ }
84
+ ```
85
+
86
+ #### `GET /v5/headCoach` Response
87
+
88
+ ```json
89
+ {
90
+ "id": 2771594,
91
+ "nameFirst": "jamon",
92
+ "nameLast": "iberico",
93
+ "profileImage": "...",
94
+ "orgId": 602402,
95
+ "orgName": "Jamon Fit",
96
+ "coachLicenseId": 10,
97
+ "nextBillingDate": "2026-03-24",
98
+ "isTrial": true,
99
+ "isMarketplaceEnabled": false,
100
+ "isExpiredTrial": false,
101
+ "daysLeftInTrial": 14,
102
+ "isFirstMobileLogin": true
103
+ }
104
+ ```
105
+
106
+ ---
107
+
108
+ ### Athletes
109
+
110
+ | Method | Endpoint | Description |
111
+ | ------ | ---------------------------------- | -------------------------------- |
112
+ | GET | `/v5/athletes` | List all athletes |
113
+ | POST | `/v5/emails/validate` | **Validate email addresses** |
114
+ | POST | `/v5/athletes/inviteToTeam` | **Invite athletes to team** |
115
+ | PUT | `/v5/athletes/archive` | **Archive athletes** |
116
+ | PUT | `/v5/athletes/{athleteId}/archive` | **Archive single athlete** |
117
+ | PUT | `/v5/athletes/restore` | **Restore (unarchive) athletes** |
118
+
119
+ #### `GET /v5/athletes` Response
120
+
121
+ ```json
122
+ [
123
+ {
124
+ "id": 2771594,
125
+ "fullName": "iberico, jamon",
126
+ "firstName": "jamon",
127
+ "lastName": "iberico",
128
+ "email": "neat.nest6259@fastmail.com",
129
+ "useMetric": 0,
130
+ "teamCount": 0,
131
+ "daysSinceLastLogin": 0,
132
+ "groups": [],
133
+ "groupTitles": [],
134
+ "userTags": [],
135
+ "imageProfile": "...",
136
+ "canUserBeRemovedFromTeam": false,
137
+ "athleteType": ""
138
+ }
139
+ ]
140
+ ```
141
+
142
+ #### `POST /v5/emails/validate`
143
+
144
+ Validates email addresses before sending invites.
145
+
146
+ **Request body:**
147
+
148
+ ```json
149
+ { "emails": "user@example.com" }
150
+ ```
151
+
152
+ **Response:** `["user@example.com"]` (array of valid emails)
153
+
154
+ #### `POST /v5/athletes/inviteToTeam`
155
+
156
+ Sends team invites to athletes via email.
157
+
158
+ **Request body:**
159
+
160
+ ```json
161
+ {
162
+ "teamType": 0,
163
+ "teamId": 4677619,
164
+ "orgId": null,
165
+ "emails": ["user@example.com"],
166
+ "message": "Follow these steps and you'll be set up and ready to go!"
167
+ }
168
+ ```
169
+
170
+ **Response:**
171
+
172
+ ```json
173
+ {
174
+ "sent": ["user@example.com"],
175
+ "notSent": []
176
+ }
177
+ ```
178
+
179
+ The invite dialog also supports:
180
+
181
+ - **1:1 invites** (direct coach-athlete pairing)
182
+ - **CSV upload** for bulk invitations
183
+
184
+ #### `PUT /v5/athletes/archive`
185
+
186
+ Archives athletes (removes from active roster but preserves data).
187
+
188
+ **Request body:**
189
+
190
+ ```json
191
+ { "athleteIds": [2771596] }
192
+ ```
193
+
194
+ #### `PUT /v5/athletes/restore`
195
+
196
+ Restores previously archived athletes.
197
+
198
+ **Request body:**
199
+
200
+ ```json
201
+ { "athleteIds": [2771596] }
202
+ ```
203
+
204
+ ---
205
+
206
+ ### Teams
207
+
208
+ | Method | Endpoint | Description |
209
+ | ------ | ----------------------------------------------------- | ----------------------------- |
210
+ | GET | `/1.0/coach/teams` | List coach teams |
211
+ | GET | `/1.0/coach/teams?page={n}&pageSize={n}&q={search}` | Paginated/searchable teams |
212
+ | POST | `/1.0/coach/team/createWithTitleAndCode` | **Create team** |
213
+ | GET | `/1.0/coach/team/allLicenseSubscribedTeams/{groupId}` | Teams subscribed to a program |
214
+ | GET | `/1.0/coach/programs/taggedByTeam/{groupId}?type=1` | Programs tagged to a team |
215
+ | GET | `/v5/teams/{teamId}` | Team details (full object) |
216
+ | GET | `/v5/teams/{teamId}/teamCodes` | **List team access codes** |
217
+ | POST | `/v5/teams/{teamId}/teamCodes` | **Create access code** |
218
+ | DELETE | `/v5/teamCodes/{codeId}` | **Delete access code** |
219
+
220
+ #### `POST /1.0/coach/team/createWithTitleAndCode`
221
+
222
+ Creates a new team. Automatically creates a team calendar (program).
223
+
224
+ **Request body:**
225
+
226
+ ```json
227
+ { "title": "Test Team" }
228
+ ```
229
+
230
+ **Side effects:** Creating a team triggers loading of:
231
+
232
+ - `/3.0/coach/program/{newCalendarId}` — the auto-created team calendar
233
+ - `/1.0/coach/programs/edit/{calendarId}/{year}/{month}/{day}` — calendar edit view
234
+ - `/2.0/coach/calendar/summary/{calendarId}/{year}/{month}/{day}` — calendar summary
235
+
236
+ #### `GET /v5/teams/{teamId}/teamCodes` Response
237
+
238
+ ```json
239
+ [
240
+ {
241
+ "id": 874576,
242
+ "code": "17731133064959",
243
+ "created_by": 2771594,
244
+ "date_created": "2026-03-10T03:28:26Z",
245
+ "date_modified": "2026-03-10T03:28:26Z",
246
+ "description": "",
247
+ "end_date": "2099-01-01T00:00:00Z",
248
+ "max_use_count": null,
249
+ "name": "",
250
+ "start_date": "2026-03-10T03:28:26Z",
251
+ "status": 1,
252
+ "team_id": 4677619,
253
+ "type": 2,
254
+ "use_count": 0
255
+ }
256
+ ]
257
+ ```
258
+
259
+ #### `POST /v5/teams/{teamId}/teamCodes`
260
+
261
+ Creates a new access code for athletes to join the team.
262
+
263
+ **Request body:**
264
+
265
+ ```json
266
+ { "type": 2 }
267
+ ```
268
+
269
+ **Response:**
270
+
271
+ ```json
272
+ {
273
+ "type": 2,
274
+ "team_id": 4677619,
275
+ "code": 1773116732,
276
+ "id": 874586,
277
+ "created_by": 2771594
278
+ }
279
+ ```
280
+
281
+ #### `DELETE /v5/teamCodes/{codeId}`
282
+
283
+ Deletes an access code. No request body needed.
284
+
285
+ ---
286
+
287
+ ### Programs
288
+
289
+ | Method | Endpoint | Description |
290
+ | ------ | ------------------------------- | ------------------------------- |
291
+ | GET | `/v5/programs` | List programs |
292
+ | GET | `/v5/programs/new` | New programs |
293
+ | GET | `/v5/programs/free` | Free programs |
294
+ | GET | `/v5/programs/fixed` | Fixed programs |
295
+ | GET | `/1.0/coach/programs` | Coach programs list |
296
+ | GET | `/1.0/coach/programs/edit/{id}` | Program edit data |
297
+ | GET | `/3.0/coach/program/{id}` | Program detail (full structure) |
298
+ | GET | `/1.0/coach/subscriptions` | Program subscriptions |
299
+
300
+ #### `GET /3.0/coach/program/{id}` Response
301
+
302
+ ```json
303
+ {
304
+ "id": 4713234,
305
+ "user_id": 2771594,
306
+ "published": 0,
307
+ "description": "",
308
+ "type": 2,
309
+ "length": 28,
310
+ "days": 0,
311
+ "title": "Test Program",
312
+ "group_id": 4677607,
313
+ "logo": "...",
314
+ "org_id": 602402
315
+ }
316
+ ```
317
+
318
+ #### `GET /1.0/coach/programs` Response
319
+
320
+ ```json
321
+ [
322
+ {
323
+ "id": 4677607,
324
+ "order": 0,
325
+ "owner_user_id": 2771594,
326
+ "featured": 0,
327
+ "title": "Test Program",
328
+ "date_created": 1773112569,
329
+ "description": "",
330
+ "slug": "iberico-program-1773112569",
331
+ "status": 0,
332
+ "group_program": 4713234,
333
+ "org_id": 602402,
334
+ "logo": "..."
335
+ }
336
+ ]
337
+ ```
338
+
339
+ ---
340
+
341
+ ### Exercises
342
+
343
+ | Method | Endpoint | Description |
344
+ | ------ | -------------------------------------------- | ----------------------------------------------- |
345
+ | GET | `/1.0/coach/exercises?page={n}&pageSize={n}` | Coach exercises (paginated) |
346
+ | GET | `/v5/exerciseLibrary/all` | Full exercise library (all available exercises) |
347
+ | POST | `/2.0/coach/exercise/create` | **Create exercise** |
348
+ | POST | `/2.0/coach/exercise/update/{exerciseId}` | **Update exercise** |
349
+ | DELETE | `/v5/exercises/{exerciseId}` | **Delete exercise** |
350
+
351
+ #### `POST /2.0/coach/exercise/create`
352
+
353
+ Creates a new custom exercise. See [Custom Exercise Creation](#custom-exercise-creation) section for full details and examples.
354
+
355
+ **Tags endpoint used during creation:** `GET /2.0/coach/tags/getByType/3` (exercise tags)
356
+
357
+ #### `POST /2.0/coach/exercise/update/{exerciseId}`
358
+
359
+ Updates a custom exercise. Takes the same fields as create. Response wraps the updated exercise in `{"success":1,"data":{...}}`.
360
+
361
+ #### `DELETE /v5/exercises/{exerciseId}`
362
+
363
+ Deletes a custom exercise. No request body needed. Only works on exercises where `can_edit: 1`.
364
+
365
+ ---
366
+
367
+ ### Sessions / Workouts
368
+
369
+ | Method | Endpoint | Description |
370
+ | ------ | ------------------------------------------------------------------------------------- | ----------------------------------------------------------------- |
371
+ | GET | `/1.0/coach/workouts?page={n}&pageSize={n}` | Session templates list |
372
+ | GET | `/2.0/coach/workoutSetExercise/template` | Prescription templates (sets/reps schemes) |
373
+ | POST | `/v5/sessions/template` | **Create session template** (library) |
374
+ | DELETE | `/v5/sessions/template/{sessionId}` | **Delete session template** |
375
+ | POST | `/2.0/coach/calendar/workout/createWorkoutForTimelineDay/{programId}/{day}/null` | **Create session in program** (timeline) |
376
+ | POST | `/2.0/coach/calendar/workout/createWorkoutForDay/{calendarId}/{year}/{month}/{day}/0` | **Create session on team calendar date** |
377
+ | POST | `/2.0/coach/calendar/saveProgramWorkoutSets` | **Add block to session** |
378
+ | POST | `/2.0/coach/calendar/saveWorkoutSetExercises` | **Add exercise to block** (with prescription) |
379
+ | POST | `/2.0/coach/calendar/programWorkout/publish` | **Publish session** |
380
+ | PUT | `/3.0/coach/workout/{workoutId}` | **Set session note** (Coach Instructions) / update programWorkout |
381
+ | GET | `/2.0/coach/calendar/summary/{calendarId}/{year}/{month}/{day}` | Calendar summary for date |
382
+ | GET | `/1.0/coach/programs/edit/{calendarId}/{year}/{month}/{day}` | Calendar edit view for date |
383
+
384
+ ---
385
+
386
+ ### Workout Creation Flow (Full Sequence)
387
+
388
+ The complete flow for creating a workout on a team calendar:
389
+
390
+ #### Step 1: Create Team (if needed)
391
+
392
+ ```
393
+ POST /1.0/coach/team/createWithTitleAndCode
394
+ Body: { "title": "My Team" }
395
+ → Returns team with calendar ID (e.g. 4713246)
396
+ ```
397
+
398
+ #### Step 2: Create Session on Calendar Date
399
+
400
+ ```
401
+ POST /2.0/coach/calendar/workout/createWorkoutForDay/{calendarId}/{year}/{month}/{day}/0
402
+ → Creates empty session, returns workout data including workout_id
403
+ ```
404
+
405
+ For program timelines (not date-based):
406
+
407
+ ```
408
+ POST /2.0/coach/calendar/workout/createWorkoutForTimelineDay/{programId}/{day}/null
409
+ ```
410
+
411
+ #### Step 3: Add Block to Session
412
+
413
+ ```
414
+ POST /2.0/coach/calendar/saveProgramWorkoutSets
415
+ Body:
416
+ [{
417
+ "workout_id": 140318787,
418
+ "order": 1,
419
+ "type": 4,
420
+ "instruction": "",
421
+ "is_redzone": null,
422
+ "redzone_type": 0,
423
+ "exercises": [],
424
+ "exerciseKeys": [],
425
+ "key": "k::9292",
426
+ "title": "Strength/Power"
427
+ }]
428
+ ```
429
+
430
+ Block types (observed `type` field):
431
+
432
+ - `1` = Conditioning
433
+ - `2` = Hypertrophy
434
+ - `4` = Strength/Power (default)
435
+
436
+ The `title` field can be set to any custom string. The `instruction` field provides block-level coach notes.
437
+
438
+ #### Step 4: Add Exercise(s) to Block (with prescription)
439
+
440
+ ```
441
+ POST /2.0/coach/calendar/saveWorkoutSetExercises
442
+ Body: [exercise1, exercise2, ...] // array of exercises
443
+ ```
444
+
445
+ > **Important:** This endpoint requires additional fields beyond the obvious ones (`set_num`, `key`, `setKey`, `eType`, all 10 param slots, etc.) or it returns 500. See [Required Fields for saveWorkoutSetExercises](#required-fields-for-saveworkoutsetexercises) for the complete field list.
446
+
447
+ **Single exercise:**
448
+
449
+ ```json
450
+ [
451
+ {
452
+ "exercise_id": 1,
453
+ "workout_set_id": 671133862,
454
+ "set_id": 671133862,
455
+ "title": "Back Squat",
456
+ "instruction": "",
457
+ "order": 1,
458
+ "param_1_type": 3,
459
+ "param_2_type": 1,
460
+ "param_1_data_1": "5",
461
+ "param_1_data_2": "5",
462
+ "param_1_data_3": "5",
463
+ "param_1_data_4": "",
464
+ "param_1_data_5": "",
465
+ "param_1_data_6": "",
466
+ "param_1_data_7": "",
467
+ "param_1_data_8": "",
468
+ "param_1_data_9": "",
469
+ "param_1_data_10": "",
470
+ "param_2_data_1": "225",
471
+ "param_2_data_2": "245",
472
+ "param_2_data_3": "265",
473
+ "param_2_data_4": "",
474
+ "param_2_data_5": "",
475
+ "param_2_data_6": "",
476
+ "param_2_data_7": "",
477
+ "param_2_data_8": "",
478
+ "param_2_data_9": "",
479
+ "param_2_data_10": "",
480
+ "workout_set_exercise_template_id": null,
481
+ "no_sets": 0,
482
+ "param_count": 3,
483
+ "set_num": 3,
484
+ "key": "k::1001",
485
+ "setKey": 671133862,
486
+ "video_url": "",
487
+ "thumbnail_url": "",
488
+ "tags": [],
489
+ "eType": "e",
490
+ "use_count": 0
491
+ }
492
+ ]
493
+ ```
494
+
495
+ **Superset (multiple exercises in same block):**
496
+ Send multiple exercises in the same array, all with the same `workout_set_id`/`set_id` but different `order` values:
497
+
498
+ ```json
499
+ [
500
+ { "exercise_id": 1, "workout_set_id": 671133862, "order": 1, "title": "Back Squat", ... },
501
+ { "exercise_id": 3, "workout_set_id": 671133862, "order": 2, "title": "Front Squat", ... }
502
+ ]
503
+ ```
504
+
505
+ This creates A1: Back Squat, A2: Front Squat displayed as a superset.
506
+
507
+ **Drop set pattern (decreasing weight, increasing reps):**
508
+
509
+ ```json
510
+ {
511
+ "exercise_id": 1162,
512
+ "title": "Bench Press",
513
+ "instruction": "Drop set - no rest between sets",
514
+ "param_1_type": 3,
515
+ "param_2_type": 1,
516
+ "param_1_data_1": "6",
517
+ "param_2_data_1": "185",
518
+ "param_1_data_2": "8",
519
+ "param_2_data_2": "165",
520
+ "param_1_data_3": "10",
521
+ "param_2_data_3": "145",
522
+ "param_1_data_4": "12",
523
+ "param_2_data_4": "125",
524
+ "param_1_data_5": "15",
525
+ "param_2_data_5": "95",
526
+ "param_count": 5
527
+ }
528
+ ```
529
+
530
+ **Parameter types (param_1_type / param_2_type):**
531
+ | Value | Type | Display |
532
+ |-------|------|---------|
533
+ | 0 | None | (no parameter) |
534
+ | 1 | Weight | `225 lb` |
535
+ | 2 | Weight (% of max) | `75%` |
536
+ | 3 | Reps | `5` |
537
+ | 4 | Time (seconds) | `1:00` |
538
+ | 5 | Distance (yards) | `50yd` |
539
+ | 6 | Distance (meters) | `50m` |
540
+ | 7 | Height | `inches` |
541
+ | 10 | Distance (miles) | miles |
542
+ | 11 | Distance (feet) | feet |
543
+ | 12 | Height (inches) | inches |
544
+ | 13 | Heart Rate | bpm |
545
+ | 14 | RPE | rating |
546
+ | 18 | Time (seconds, alt) | `0:30` |
547
+
548
+ > **The unit is fixed per exercise — you cannot set it at prescribe time (verified).**
549
+ > On save the API discards the `param_1_type`/`param_2_type` you send and restores
550
+ > the exercise's library defaults. The `param_*_data_N` _values_ are kept, so a
551
+ > value sent under the wrong assumed unit renders under the exercise's real unit.
552
+ >
553
+ > - **`param_1_type` (primary) is always forced to the exercise default.** e.g. stock
554
+ > `Run` (id 82) is `10` (miles); sending `6` (meters) is ignored and `200` shows as
555
+ > _200 miles_. To program meters, pick a meters-native exercise (`Sprint` 127,
556
+ > `Rowing` 101, `Shuttle Sprint` 42523) or a custom exercise — there is no metric
557
+ > "Run". `$TH exercise resolve` now prints a `units` array (ordered by entry slot,
558
+ > `[param 1, param 2]`); check it.
559
+ > - **`param_2_type` (secondary) is forced to the default too, with one exception:**
560
+ > if the exercise has no secondary param (default `0`/none) you may add weight
561
+ > (`1`) — this is how weighted Pull-Ups/Dips work. You cannot swap an exercise's
562
+ > existing secondary unit, and **`2` (% of max) and `14` (RPE) never stick on a
563
+ > weight-default lift — both coerce to weight, so the numbers render as pounds.**
564
+ > Put % or RPE in the `instruction` text instead.
565
+ > - the workout builder reads the local exercise cache and prints a `WARNING` when a
566
+ > sent param type will be overridden.
567
+
568
+ **Common param type combos (from 2067 exercises):**
569
+
570
+ - `p1=3, p2=None` — Reps only (801 exercises, e.g. Plyo Lunge, Lateral Lunge)
571
+ - `p1=3, p2=1` — Reps @ Weight (619 exercises, e.g. Back Squat, Bench Press, Deadlift)
572
+ - `p1=3, p2=0` — Reps only (170 exercises, e.g. Push-Up, Pull-Up, Burpee, Air Squat)
573
+ - `p1=5, p2=None` — Distance/yards only (133 exercises, e.g. Sled Push, Bear Crawl)
574
+ - `p1=4, p2=None` — Time only (91 exercises, e.g. Jog)
575
+ - `p1=3, p2=4` — Reps @ Time (51 exercises, e.g. L Drill)
576
+ - `p1=4, p2=0` — Time only (37 exercises, e.g. Plank, Rest)
577
+ - `p1=5, p2=1` — Distance @ Weight (11 exercises, e.g. Sled Drags)
578
+ - `p1=10, p2=4` — Miles @ Time (3 exercises, e.g. Run, Walk)
579
+
580
+ **Fields:**
581
+
582
+ - `param_1_data_N` = Value for param 1 in set N (1-10 max)
583
+ - `param_2_data_N` = Value for param 2 in set N (1-10 max)
584
+ - `param_count` = Number of sets
585
+ - `no_sets` = 0 (normal), non-zero may indicate special handling
586
+ - `instruction` = Per-exercise coach notes
587
+
588
+ **Note:** The API ignores the `title` field and uses the exercise's real title from `exercise_id`. It also overrides **both** `param_1_type` and `param_2_type` to the exercise's library defaults (see the fixed-unit note above) — the data values you send are kept, but the unit is the exercise's, not the one you requested.
589
+
590
+ #### Step 5: Publish Session
591
+
592
+ ```
593
+ POST /2.0/coach/calendar/programWorkout/publish
594
+ Body: [142002657] // array of programWorkout IDs
595
+ ```
596
+
597
+ #### (Optional) Set the session note — Coach Instructions
598
+
599
+ The **session-level** instruction (the day-note shown at the top of a session: a
600
+ greeting + workout writeup) is **not** the same as a block's `instruction`. It is
601
+ set with a PUT to the programWorkout, using the same `workout_id` returned at
602
+ create time:
603
+
604
+ ```
605
+ PUT /3.0/coach/workout/{workout_id}
606
+ Body: <the full programWorkout object, with `instruction` set>
607
+ ```
608
+
609
+ Build the body from the session object you already have — the create-time response,
610
+ or a day's entry from `GET /1.0/coach/programs/edit/{cal}/{y}/{m}/{d}` — then set
611
+ `instruction` and replace `sets`/`setKeys`:
612
+
613
+ ```json
614
+ {
615
+ "id": 151122476, // programWorkout id
616
+ "workout_id": 149544173,
617
+ "program_id": 2039741,
618
+ "date": "2026-06-22",
619
+ "day": 22,
620
+ "month": 6,
621
+ "year": 2026,
622
+ "title": "2026-6-22",
623
+ "type": 4,
624
+ "session": 0,
625
+ "timeline_day": 0,
626
+ "published": null,
627
+ "program_type": 0,
628
+ "logo": "...",
629
+ "team_logo": null,
630
+ "program_title": "...",
631
+ "team_title": "...",
632
+ "deleted": null,
633
+ "group_team_subscription_id": null,
634
+ "date_rescheduled": null,
635
+ "sets": [715883274, 715883275, 715883276], // block ids as a LIST (see caveat)
636
+ "setKeys": [715883274, 715883275, 715883276],
637
+ "instruction": "Welcome to Week 12...\n\n----\n\nFind a 1RM Strict Press..."
638
+ }
639
+ ```
640
+
641
+ Returns `200`. Notes (verified):
642
+
643
+ - **`sets`/`setKeys` must be a flat list of block ids**, sorted by each block's
644
+ `order`. The edit-GET returns `sets` as a **dict keyed by block id** — convert it.
645
+ - **Setting `instruction` does not publish.** `published` is echoed back exactly as
646
+ sent (e.g. `null` for a draft), so send the session's current state and set the
647
+ note _before_ publishing if it should stay a draft.
648
+ - Also carries `title`/`type`/`published` — this is the general programWorkout
649
+ update, not an instruction-only endpoint.
650
+ - Dead ends: `POST /2.0/coach/calendar/saveProgramWorkout` is a 404, and passing
651
+ `instruction` in the `createWorkoutForDay` body is ignored — this PUT is the path.
652
+
653
+ the workout builder does this automatically: add a top-level `"instruction"` to the
654
+ spec and it PUTs after the blocks are saved (before publish). Read it back with
655
+ `--read` — the session note prints under "Coach Instructions".
656
+
657
+ ---
658
+
659
+ #### `POST /v5/sessions/template`
660
+
661
+ Creates a reusable session template in the library.
662
+
663
+ **Request body:**
664
+
665
+ - `title` (required)
666
+ - `instructions` (optional)
667
+ - Blocks with exercises can be added
668
+
669
+ #### `GET /2.0/coach/workoutSetExercise/template` Response (prescription templates)
670
+
671
+ ```json
672
+ [
673
+ {
674
+ "id": 99,
675
+ "title": "4 x 3",
676
+ "type": 1,
677
+ "param_1_type": 3,
678
+ "param_2_type": 0,
679
+ "param_1_data_1": "3",
680
+ "param_1_data_2": "3",
681
+ "param_1_data_3": "3",
682
+ "param_1_data_4": "3",
683
+ "param_count": 4,
684
+ "tags": [{ "id": 251, "title": "Strength", "type": 4 }],
685
+ "editable": false,
686
+ "use_count": 0
687
+ }
688
+ ]
689
+ ```
690
+
691
+ ---
692
+
693
+ ### Tags
694
+
695
+ | Method | Endpoint | Description |
696
+ | ------ | ------------------------------- | -------------------- |
697
+ | GET | `/2.0/coach/tags/getByType/3` | Exercise tags |
698
+ | GET | `/2.0/coach/tags/getByType/4` | Training effect tags |
699
+ | GET | `/2.0/coach/tags/getSportsTags` | Sports tags |
700
+
701
+ **Tag types:**
702
+
703
+ - Type 3: Exercise category (Olympic Lifts, Primary Lifts, Accessory Lifts, Gymnastics, etc.)
704
+ - Type 4: Training effect (Strength, Hypertrophy, Power, Conditioning, etc.)
705
+
706
+ #### `GET /2.0/coach/tags/getByType/{type}` Response
707
+
708
+ ```json
709
+ {
710
+ "success": 1,
711
+ "data": {
712
+ "tagType": { "id": "3", "title": "Exercise" },
713
+ "tags": [
714
+ { "id": 211, "title": "Olympic Lifts", "type": 3, "use_count": 28056 },
715
+ { "id": 212, "title": "Primary Lifts", "type": 3, "use_count": 33280 },
716
+ { "id": 213, "title": "Accessory Lifts", "type": 3, "use_count": 171716 }
717
+ ]
718
+ }
719
+ }
720
+ ```
721
+
722
+ ---
723
+
724
+ ### Favorites / Preferences
725
+
726
+ | Method | Endpoint | Description |
727
+ | ------ | ---------------------------------- | ----------------- |
728
+ | GET | `/2.0/coach/favorite?pageSize={n}` | Favorited items |
729
+ | GET | `/2.0/coach/prefs` | Coach preferences |
730
+
731
+ #### `GET /2.0/coach/prefs` Response
732
+
733
+ ```json
734
+ {
735
+ "id": 2800633,
736
+ "email_workout_preview": 1,
737
+ "email_new_posts": 0,
738
+ "email_coach_posts": 1,
739
+ "mobile_workout_preview": 1,
740
+ "auto_update_wm": 0,
741
+ "show_exercises_trainheroic": 1,
742
+ "show_programs_trainheroic": 1,
743
+ "show_calendars_trainheroic": 1,
744
+ "show_workout_set_exercise_templates_trainheroic": 1,
745
+ "survey_team_enabled": 1,
746
+ "survey_one_to_one_enabled": 1
747
+ }
748
+ ```
749
+
750
+ ---
751
+
752
+ ### Messaging
753
+
754
+ Chat between a coach and athletes/teams. A **stream** is a conversation; a
755
+ **comment** is a message in it. Verified against a live
756
+ account (see `$TH message`).
757
+
758
+ | Method | Endpoint | Description |
759
+ | ------ | -------------------------------------------------------------- | ------------------------------------------------------------------------------ |
760
+ | GET | `/v5/messaging/streams` | List conversations (bucketed) |
761
+ | GET | `/v5/messaging/streams/{streamId}/comments?lastCommentId={id}` | Messages in a stream; `lastCommentId` returns only comments newer than that id |
762
+ | POST | `/v5/messaging/streams/{streamId}/comments` | **Send a message** (athlete-facing, immediate) |
763
+ | DELETE | `/v5/messaging/streams/{streamId}/comments/{commentId}` | **Delete a message** (soft delete) |
764
+ | GET | `/v5/messaging/reactions` | Reaction catalog (Like/Love/Fire/Trophy/…) |
765
+ | GET | `/v5/notifications/counts` | Unread message counts (cheap "anything new?" poll) |
766
+
767
+ #### `GET /v5/messaging/streams` Response
768
+
769
+ Conversations grouped into four buckets. Each entry's `id` is the **stream id**
770
+ (distinct from `teamId`/`userId`) used by every other messaging call.
771
+
772
+ ```json
773
+ {
774
+ "teams": [
775
+ {
776
+ "id": 37731134,
777
+ "teamId": 4945224,
778
+ "title": "Test Team",
779
+ "isOwner": true,
780
+ "logo": "...",
781
+ "lastViewed": 1781808863
782
+ }
783
+ ],
784
+ "athletes": [
785
+ {
786
+ "id": 37730920,
787
+ "userId": 2855688,
788
+ "teamId": 4945209,
789
+ "title": "[Demo] Kyle Jones",
790
+ "metadata": { "nameFirst": "...", "nameLast": "..." },
791
+ "logo": "..."
792
+ }
793
+ ],
794
+ "programs": [],
795
+ "coaches": []
796
+ }
797
+ ```
798
+
799
+ #### `GET /v5/messaging/streams/{streamId}/comments?lastCommentId={id}` Response
800
+
801
+ Array of comments, oldest→newest. Pass the highest `id` you've stored as
802
+ `lastCommentId` to fetch only newer ones (verified: returns `[]` when none are
803
+ newer). A blank `lastCommentId=` returns the whole stream.
804
+
805
+ ```json
806
+ [
807
+ {
808
+ "id": 125652586,
809
+ "timestamp": 1781809398,
810
+ "content": "Nice work today!",
811
+ "imageUrl": null,
812
+ "thumbnailUrl": null,
813
+ "authorName": "A Cohen",
814
+ "authorLogo": "https://static.trainheroic.com/avatar-2025/A/avatar-AC.png",
815
+ "replies": [],
816
+ "reactions": [],
817
+ "isAuthor": true
818
+ }
819
+ ]
820
+ ```
821
+
822
+ - `isAuthor: false` is a message you **received**; `true` is one you sent.
823
+ - `id` doubles as the incremental cursor (`lastCommentId`).
824
+ - `replies[]` holds threaded replies as nested comment objects; `reactions[]`
825
+ holds reactions on the comment (decode via `/v5/messaging/reactions`).
826
+
827
+ #### `POST /v5/messaging/streams/{streamId}/comments` (send)
828
+
829
+ Returns the created comment (same shape as above). **The required field the
830
+ obvious guesses miss is `feed_id`** — the stream id repeated in the body.
831
+ `{ "content": "..." }` alone returns `400 Invalid parameters`; the full body the
832
+ web client sends is:
833
+
834
+ ```json
835
+ {
836
+ "type": 0,
837
+ "content": "Nice work today!",
838
+ "photo_url": "",
839
+ "photoUrl": "",
840
+ "access_level": 0,
841
+ "parent_feed_item_id": null,
842
+ "feed_id": 37730920
843
+ }
844
+ ```
845
+
846
+ - `feed_id` = the `{streamId}` from the path (required).
847
+ - `parent_feed_item_id` = a comment id to post a threaded reply; `null` for a
848
+ top-level message.
849
+ - Trimming any of the other fields also returns `400 Invalid parameters` —
850
+ send the whole body.
851
+ - There is **no server-side draft state**: a POST is delivered immediately.
852
+
853
+ #### `DELETE /v5/messaging/streams/{streamId}/comments/{commentId}`
854
+
855
+ Soft delete (sets `deleted_at`). No body. Returns the underlying row, which
856
+ reveals a comment can attach to more than chat:
857
+
858
+ ```json
859
+ {
860
+ "id": 125652586,
861
+ "created_by": 2855687,
862
+ "type": 0,
863
+ "access_level": 0,
864
+ "content": "...",
865
+ "photo_url": "",
866
+ "deleted_at": "2026-06-18T19:05:35.000000Z",
867
+ "parent_feed_item_id": null,
868
+ "program_workout_id": null,
869
+ "saved_workout_set_id": null,
870
+ "group_id": null,
871
+ "saved_workout_id": null
872
+ }
873
+ ```
874
+
875
+ #### `GET /v5/notifications/counts` Response
876
+
877
+ The cheap gate before fanning out across streams:
878
+
879
+ ```json
880
+ {
881
+ "countNotViewed": 0,
882
+ "countNotificationNotViewed": 0,
883
+ "countMessagingNotViewed": 0,
884
+ "messaging": { "countDirectNotViewed": 0, "countTeamNotViewed": 0 }
885
+ }
886
+ ```
887
+
888
+ #### Real-time channel (not needed for a sync)
889
+
890
+ The web client receives live messages over a separate long-poll channel —
891
+ `adapter.trainheroic.com/messaging?timestamp={ms}` (global) and
892
+ `adapter.trainheroic.com/messaging/team/{teamId}?timestamp={ms}` (per team),
893
+ loaded as a cross-origin iframe stream. A coach-side **sync does not need it**:
894
+ polling the REST `comments` endpoint with `lastCommentId` captures the same
895
+ messages. The entire chat UI is served from `adapter.trainheroic.com` and embedded
896
+ in `coachapp.trainheroic.com/messaging` as the `messagingHub` iframe.
897
+
898
+ ---
899
+
900
+ ### Coach Admin
901
+
902
+ | Method | Endpoint | Description |
903
+ | ------ | ------------------------------------------ | ------------------------- |
904
+ | GET | `/2.0/coach/admin/coachAthletes/{coachId}` | Athletes managed by coach |
905
+ | GET | `/user/mobile` | User mobile info |
906
+
907
+ ---
908
+
909
+ ### Analytics
910
+
911
+ | Method | Endpoint | Description |
912
+ | ------ | ---------------------------------------------- | ---------------------------------------------------------- |
913
+ | GET | `/v5/analytics` | Analytics categories (lists all available analytics types) |
914
+ | POST | `/v5/analytics/readiness/teams` | **Readiness survey — team** |
915
+ | POST | `/v5/analytics/readiness/users` | **Readiness survey — single athlete** |
916
+ | POST | `/v5/analytics/lift-one-rep-max-history/users` | **1RM history — single athlete** |
917
+ | POST | `/v5/analytics/training-summary/users` | **Training summary — single athlete** |
918
+ | POST | `/v5/analytics/training-summary/teams` | **Training summary — team** |
919
+ | POST | `/v5/analytics/compliance` | **Compliance data** |
920
+ | POST | `/v5/analytics/lift-progress/teams` | **Lift progress — team** |
921
+ | POST | `/v5/analytics/working-max-history/users` | **Working max history** |
922
+
923
+ #### `GET /v5/analytics` Response
924
+
925
+ Returns all available analytics categories and their instances:
926
+
927
+ ```json
928
+ {
929
+ "categories": [
930
+ {
931
+ "key": "readiness",
932
+ "title": "Readiness",
933
+ "instances": ["readiness-survey-athlete", "readiness-survey-team"]
934
+ },
935
+ {
936
+ "key": "performance",
937
+ "title": "Performance",
938
+ "instances": ["lift-one-rep-max-history", "lift-one-rep-max-team-history"]
939
+ },
940
+ {
941
+ "key": "liftHistory",
942
+ "title": "Lift History",
943
+ "instances": ["lift-history-complete", "working-max-history"]
944
+ },
945
+ { "key": "trainingSummary", "title": "Training Summary", "instances": ["training-summary"] },
946
+ { "key": "compliance", "title": "Compliance", "instances": ["compliance-team"] },
947
+ { "key": "liftProgress", "title": "Lift Progress", "instances": ["lift-progress-team"] }
948
+ ]
949
+ }
950
+ ```
951
+
952
+ #### `POST /v5/analytics/readiness/teams`
953
+
954
+ **Request body:**
955
+
956
+ ```json
957
+ { "teamId": 4677619, "date": "2026-03-09" }
958
+ ```
959
+
960
+ **Response:** Returns columns (user_id, name_last, name_first, date_completed, sleep, mood, energy, stress, soreness, readiness) and rows of athlete readiness data.
961
+
962
+ #### `POST /v5/analytics/lift-one-rep-max-history/users`
963
+
964
+ **Request body:**
965
+
966
+ ```json
967
+ {
968
+ "date_start": "2026-02-07",
969
+ "date_end": "2026-03-09",
970
+ "user_ids": ["2771596"],
971
+ "exercise_id": "424",
972
+ "use_metric": false
973
+ }
974
+ ```
975
+
976
+ **Response:** Returns columns (exercise_title, user_id, name, date, estimated_1rm, etc.) and rows of 1RM history data.
977
+
978
+ ---
979
+
980
+ ### Notifications / Misc
981
+
982
+ | Method | Endpoint | Description |
983
+ | ------ | -------------------------- | ------------------------- |
984
+ | GET | `/v5/notifications` | Notifications list |
985
+ | GET | `/v5/notifications/counts` | Notification counts |
986
+ | GET | `/v5/site-banners` | Site banners |
987
+ | POST | `/v5/telemetry/track` | Telemetry/tracking events |
988
+
989
+ ---
990
+
991
+ ## Athlete API Endpoints (Mobile App)
992
+
993
+ These are used by the mobile app / athlete-facing client (documented in `train-heroic-schema.yml`):
994
+
995
+ | Method | Endpoint | Description |
996
+ | ------ | ------------------------------------------------------- | ---------------------------------------------- |
997
+ | POST | `/auth` | Login |
998
+ | GET | `/v5/users/exercises/history` | All exercise history |
999
+ | GET | `/v5/users/exercises/recent` | Recent exercises |
1000
+ | GET | `/v5/exercises/{id}/history` | Single exercise history |
1001
+ | GET | `/v5/exercises/{id}` | Exercise details |
1002
+ | GET | `/v5/exercises/{id}/personalRecords` | Exercise PRs |
1003
+ | GET | `/v5/exercises/{id}/stats` | Exercise stats |
1004
+ | GET | `/v5/exercises/{id}/stackUp/isSupportedExercise` | Stack-up support check |
1005
+ | GET | `/v5/users/circuits/recent` | Recent circuits |
1006
+ | GET | `/v5/users/circuits/history` | Circuit history |
1007
+ | GET | `/3.0/athlete/programworkout/range?startDate=&endDate=` | Workouts by date range |
1008
+ | GET | `/1.0/athlete/programming/programs` | Athlete programs |
1009
+ | GET | `/v5/users/{id}` | User profile |
1010
+ | GET | `/1.0/athlete/prefs` | Athlete preferences |
1011
+ | GET | `/2.0/athlete/workingMax` | Working maxes (all exercises with WM tracking) |
1012
+ | GET | `/1.0/athlete/savedworkoutset/{id}` | Saved workout set |
1013
+ | GET | `/1.0/athlete/savedworkout/{id}` | Saved workout |
1014
+ | GET | `/1.0/user/userInfo` | User info |
1015
+ | GET | `/v5/calendars/athletes/{id}/coachAthleteTeam` | Coach-athlete team calendar |
1016
+ | GET | `/v5/users/{id}/workingMaxes/{id1}` | Specific working max |
1017
+ | GET | `/v5/programs/new` | New programs |
1018
+ | GET | `/v5/programs/free` | Free programs |
1019
+ | GET | `/v5/athleteProfile/summary` | Athlete profile summary |
1020
+ | GET | `/3.0/athlete/leaderboard/{id}` | Leaderboard |
1021
+
1022
+ ---
1023
+
1024
+ ## Key Exercise IDs (from exercise library)
1025
+
1026
+ | ID | Title | param_1_type | param_2_type |
1027
+ | ---- | ----------- | ------------ | ------------ |
1028
+ | 1 | Back Squat | 3 (Reps) | 1 (Weight) |
1029
+ | 3 | Front Squat | 3 | 1 |
1030
+ | 7 | Pull-Up | 3 | 0 |
1031
+ | 24 | Burpee | 3 | 0 |
1032
+ | 36 | Air Squat | 3 | 0 |
1033
+ | 67 | Plank | 4 (Time) | 0 |
1034
+ | 100 | Push-Up | 3 | 0 |
1035
+ | 424 | Deadlift | 3 | 1 |
1036
+ | 1162 | Bench Press | 3 | 1 |
1037
+
1038
+ Full exercise library: `GET /v5/exerciseLibrary/all` (~2387 entries: 2067 exercises + 320 workout circuits)
1039
+
1040
+ ---
1041
+
1042
+ ## Custom Exercise Creation
1043
+
1044
+ `POST /2.0/coach/exercise/create`
1045
+
1046
+ Create exercises not in the standard library. You control the title and parameter types.
1047
+
1048
+ **Request body:**
1049
+
1050
+ ```json
1051
+ {
1052
+ "title": "Goblet Cossack Squat",
1053
+ "param_1_type": 3,
1054
+ "param_2_type": 1,
1055
+ "instruction": "",
1056
+ "video_url": "",
1057
+ "points_of_performance": "Deep lateral squat holding KB at chest",
1058
+ "is_circuit": false,
1059
+ "type": 0,
1060
+ "tags": [],
1061
+ "swaps": [],
1062
+ "reference_max_exercise_id": null,
1063
+ "trainheroic_reference_exercise_id": null
1064
+ }
1065
+ ```
1066
+
1067
+ **Response:**
1068
+
1069
+ ```json
1070
+ {
1071
+ "id": 7721170,
1072
+ "user_id": 2771594,
1073
+ "title": "Goblet Cossack Squat",
1074
+ "param_1_type": 3,
1075
+ "param_2_type": 1,
1076
+ "can_edit": 1,
1077
+ "use_count": 0
1078
+ }
1079
+ ```
1080
+
1081
+ Custom exercises can use any param type combo (see parameter types table). Examples:
1082
+
1083
+ - `p1=5, p2=0` → Distance only (e.g. "Walking Lunge for Distance" → shows as "3 x 50yd")
1084
+ - `p1=5, p2=1` → Distance @ Weight (e.g. "Weighted Walking Lunge for Distance" → "2 x 40yd @ 50lb")
1085
+ - `p1=3, p2=0` → Reps only / bodyweight (e.g. Push-Up → "3 x 15")
1086
+ - `p1=3, p2=1` → Reps @ Weight (e.g. "Goblet Cossack Squat" → "3 x 8 @ 35lb")
1087
+
1088
+ ---
1089
+
1090
+ ## Advanced Prescription Patterns
1091
+
1092
+ ### Pyramid Sets
1093
+
1094
+ Use varying values across `param_1_data_N` and `param_2_data_N` to create pyramids (ascending then descending):
1095
+
1096
+ ```json
1097
+ {
1098
+ "exercise_id": 1,
1099
+ "title": "Back Squat",
1100
+ "instruction": "Pyramid: work up to heavy single then back down",
1101
+ "param_1_type": 3,
1102
+ "param_2_type": 1,
1103
+ "param_1_data_1": "5",
1104
+ "param_2_data_1": "185",
1105
+ "param_1_data_2": "3",
1106
+ "param_2_data_2": "225",
1107
+ "param_1_data_3": "1",
1108
+ "param_2_data_3": "275",
1109
+ "param_1_data_4": "3",
1110
+ "param_2_data_4": "225",
1111
+ "param_1_data_5": "5",
1112
+ "param_2_data_5": "185",
1113
+ "param_count": 5
1114
+ }
1115
+ ```
1116
+
1117
+ Displays as: `5, 3, 1, 3, 5 @ 185, 225, 275, 225, 185lb`
1118
+
1119
+ ### Bodyweight Exercises (No Weight)
1120
+
1121
+ Set `param_2_type: 0` and leave all `param_2_data_N` empty:
1122
+
1123
+ ```json
1124
+ {
1125
+ "exercise_id": 100,
1126
+ "title": "Push-Up",
1127
+ "param_1_type": 3,
1128
+ "param_2_type": 0,
1129
+ "param_1_data_1": "20",
1130
+ "param_1_data_2": "15",
1131
+ "param_1_data_3": "10",
1132
+ "param_2_data_1": "",
1133
+ "param_2_data_2": "",
1134
+ "param_2_data_3": "",
1135
+ "param_count": 3
1136
+ }
1137
+ ```
1138
+
1139
+ Displays as: `20, 15, 10` (reps only)
1140
+
1141
+ Many existing exercises default to bodyweight (p2=0 or p2=None):
1142
+
1143
+ - Push-Up (100), Pull-Up (7), Air Squat (36), Burpee (24)
1144
+ - ~970+ exercises in the library are reps-only (p2=0 or p2=None)
1145
+
1146
+ ### Distance-Based Exercises
1147
+
1148
+ The distance unit is the exercise's fixed default (see the fixed-unit note under
1149
+ "Parameter types") — you only get the unit the exercise already carries. The
1150
+ examples below work because the `exercise_id`s are **custom** exercises created
1151
+ with that `param_1_type`; sending `param_1_type: 5` to a stock reps or miles
1152
+ exercise is ignored. To get a unit a stock exercise lacks, create a custom
1153
+ exercise with the unit you want, or pick a stock exercise whose default matches
1154
+ (`Sprint` 127 = meters, a `*yd` carry = yards, `Run` 82 = miles).
1155
+
1156
+ For a custom exercise created with `param_1_type: 5`, distance displays as yards:
1157
+
1158
+ ```json
1159
+ {
1160
+ "exercise_id": 7721172,
1161
+ "title": "Walking Lunge for Distance",
1162
+ "param_1_type": 5,
1163
+ "param_2_type": 0,
1164
+ "param_1_data_1": "50",
1165
+ "param_1_data_2": "50",
1166
+ "param_1_data_3": "50",
1167
+ "param_count": 3
1168
+ }
1169
+ ```
1170
+
1171
+ Displays as: `3 x 50yd`
1172
+
1173
+ With weight (`param_2_type: 1`):
1174
+
1175
+ ```json
1176
+ {
1177
+ "exercise_id": 7721174,
1178
+ "title": "Weighted Walking Lunge for Distance",
1179
+ "param_1_type": 5,
1180
+ "param_2_type": 1,
1181
+ "param_1_data_1": "40",
1182
+ "param_1_data_2": "40",
1183
+ "param_2_data_1": "50",
1184
+ "param_2_data_2": "50",
1185
+ "param_count": 2
1186
+ }
1187
+ ```
1188
+
1189
+ Displays as: `2 x 40yd @ 50lb`
1190
+
1191
+ **Distance type values:**
1192
+ | param type | Unit |
1193
+ |-----------|------|
1194
+ | 5 | yards |
1195
+ | 6 | meters |
1196
+ | 10 | miles |
1197
+ | 11 | feet |
1198
+
1199
+ Existing distance exercises: 193 in library (e.g. Sled Push, Bear Crawl, Sprint, Carioca, Run)
1200
+
1201
+ ### Lunge Exercises in Library
1202
+
1203
+ 102 lunge exercises exist. Most use Reps (p1=3):
1204
+
1205
+ - Walking Lunges (77): `p1=6 (meters), p2=None` — already distance-based!
1206
+ - DB Walking Lunge (5947818): `p1=3, p2=1`
1207
+ - Body Weight Lunge (5947644): `p1=3, p2=None`
1208
+ - Tandem Resisted Lunge Walks (688456): `p1=5 (yards), p2=None` — distance-based
1209
+
1210
+ To make any lunge distance-based, create a custom exercise with `param_1_type: 5`.
1211
+
1212
+ ---
1213
+
1214
+ ## Required Fields for saveWorkoutSetExercises
1215
+
1216
+ The API requires these additional fields beyond the basic ones (discovered via UI capture):
1217
+
1218
+ ```json
1219
+ {
1220
+ "exercise_id": 1,
1221
+ "workout_set_id": 671135671,
1222
+ "set_id": 671135671,
1223
+ "title": "Back Squat",
1224
+ "instruction": "",
1225
+ "order": 1,
1226
+ "param_1_type": 3,
1227
+ "param_2_type": 1,
1228
+ "param_1_data_1": "5", "param_1_data_2": "", ..., "param_1_data_10": "",
1229
+ "param_2_data_1": "225", "param_2_data_2": "", ..., "param_2_data_10": "",
1230
+ "workout_set_exercise_template_id": null,
1231
+ "no_sets": 0,
1232
+ "param_count": 3,
1233
+ "set_num": 3,
1234
+ "key": "k::5001",
1235
+ "setKey": 671135671,
1236
+ "video_url": "",
1237
+ "thumbnail_url": "",
1238
+ "tags": [],
1239
+ "eType": "e",
1240
+ "use_count": 0
1241
+ }
1242
+ ```
1243
+
1244
+ **Critical fields that cause 500 errors if missing:**
1245
+
1246
+ - `set_num` — number of sets (same as `param_count`)
1247
+ - `key` — unique key string (format: `"k::<number>"`, can be any unique value)
1248
+ - `setKey` — same as `workout_set_id`
1249
+ - All 10 `param_1_data_N` and `param_2_data_N` fields (empty string `""` for unused slots)
1250
+ - `eType` — `"e"` for exercise
1251
+ - `tags` — array (can be empty `[]`)
1252
+ - `video_url`, `thumbnail_url` — strings (can be empty `""`)
1253
+ - `use_count` — integer (can be `0`)
1254
+ - `workout_set_exercise_template_id` — null
1255
+
1256
+ ---
1257
+
1258
+ ## Session Management
1259
+
1260
+ | Method | Endpoint | Description |
1261
+ | ------ | ---------------------------------------------------------------------- | --------------------------- |
1262
+ | POST | `/2.0/coach/calendar/programWorkout/unPublish/{programWorkoutId}` | **Unpublish session** |
1263
+ | POST | `/2.0/coach/calendar/removeProgramWorkout` | **Delete session** |
1264
+ | POST | `/2.0/coach/calendar/copyProgramWorkout` | **Copy/Repeat session** |
1265
+ | POST | `/2.0/coach/calendar/programWorkout/saveWorkoutAsTemplate/{workoutId}` | **Save session to library** |
1266
+
1267
+ #### `POST /2.0/coach/calendar/programWorkout/unPublish/{programWorkoutId}`
1268
+
1269
+ Unpublishes a previously published session. No request body needed.
1270
+
1271
+ #### `POST /2.0/coach/calendar/removeProgramWorkout`
1272
+
1273
+ Deletes a session from the calendar.
1274
+
1275
+ **Request body:**
1276
+
1277
+ ```json
1278
+ {
1279
+ "programId": 4713246,
1280
+ "pwId": 142002657
1281
+ }
1282
+ ```
1283
+
1284
+ #### `POST /2.0/coach/calendar/copyProgramWorkout`
1285
+
1286
+ Copies or repeats a session to a target date. Used for both "Copy" and "Repeat" context menu actions.
1287
+
1288
+ **Request body:**
1289
+
1290
+ ```json
1291
+ {
1292
+ "toProgramId": 4713246,
1293
+ "pwId": 142002657,
1294
+ "toDate": {
1295
+ "date": "2026-03-15",
1296
+ "day": 15,
1297
+ "month": 3,
1298
+ "year": 2026,
1299
+ "dayOfWeek": 0,
1300
+ "isToday": false
1301
+ }
1302
+ }
1303
+ ```
1304
+
1305
+ #### `POST /2.0/coach/calendar/programWorkout/saveWorkoutAsTemplate/{workoutId}`
1306
+
1307
+ Saves an existing session as a reusable template in the session library. No request body needed — uses the workout ID in the URL path.
1308
+
1309
+ ---
1310
+
1311
+ ## Team Management
1312
+
1313
+ | Method | Endpoint | Description |
1314
+ | ------ | --------------------------------------- | -------------------------------------- |
1315
+ | PUT | `/v5/teams/{teamId}` | **Update team settings** (title, etc.) |
1316
+ | DELETE | `/v5/teams/{teamId}` | **Delete team** |
1317
+ | POST | `/1.0/coach/team/updatePublishSettings` | **Update auto-publish settings** |
1318
+
1319
+ #### `PUT /v5/teams/{teamId}`
1320
+
1321
+ Updates team properties like title.
1322
+
1323
+ **Request body:**
1324
+
1325
+ ```json
1326
+ {
1327
+ "title": "New Team Name"
1328
+ }
1329
+ ```
1330
+
1331
+ #### `POST /1.0/coach/team/updatePublishSettings`
1332
+
1333
+ Updates the auto-publish settings for a team's program. Takes the full program object with `pub_*` fields controlling auto-publish behavior.
1334
+
1335
+ ---
1336
+
1337
+ ## Coach Dashboard
1338
+
1339
+ | Method | Endpoint | Description |
1340
+ | ------ | --------------------------------------------------------------- | --------------------------------- |
1341
+ | GET | `/v5/coaches/activityFeed?page={n}&pageSize={n}` | Coach activity feed |
1342
+ | GET | `/v5/coaches/lowProgramming` | Low programming alerts |
1343
+ | GET | `/v5/coaches/onboarding` | Onboarding tracking |
1344
+ | GET | `/v5/coaches/athletes/{athleteId}/workouts?startDate=&endDate=` | Athlete workout data with surveys |
1345
+
1346
+ ---
1347
+
1348
+ ## Still Unexplored
1349
+
1350
+ - Program update (title, description, settings) — PUT/POST patterns return 405/404
1351
+ - Athlete remove from specific team (endpoint pattern not found via probing)
1352
+ - Working max set/update from coach side for specific athletes
1353
+ - Marketplace endpoints (publishing, pricing, purchases)
1354
+ - Notification management (mark read, dismiss — 401/404 on tested patterns)
1355
+ - `apis.trainheroic.com/user` endpoint (uses `api-token` header, returns full user profile with teams)
1356
+ - Circuit creation (vs superset — circuits may use different block type or field)
1357
+ - Prescription template CRUD
1358
+ - Coach preferences update (PATCH/PUT on `2.0/coach/prefs` returns 403/405)
1359
+ - Library settings
1360
+ - Session template update (edit existing template)
1361
+ - Workout set reorder / move exercises between blocks