Gym · API reference

Plan his training through the API

A personal gym tracker for one person. You write planned workouts here; he opens the app, starts the next one and logs sets with prefilled weights. JSON over HTTPS at https://gym.abdellatif.io/api, no auth, CORS open.

Quick start

Base URL https://gym.abdellatif.io/api. Every response is JSON; errors look like {"error": {"code", "message"}}.

bash
# What's next (and the planned queue)
curl -sS "https://gym.abdellatif.io/api/next"

# Exercise names to use in plans
curl -sS "https://gym.abdellatif.io/api/exercises?q=bench"
  • Reference exercises by their existing names ("exercise": "Incline Bench Press"). Look them up first; a new spelling creates a new exercise with no history.
  • Put the coach-style text in prescription ("3 x 12-10-8 reps, rest 30-60s\n*Go down in 3 sec*"): sets, reps, rest and cue are parsed from it.
  • Label supersets A1, A2, A3; one workout per training day with a date (YYYY-MM-DD).
  • Don't send sets or weights in plans: the app prefills them from his history.
  • Don't change completed workouts (his history) unless he asks.

Then follow Plan next week.

Concepts

A workout holds items; an item holds sets. Items that share a letter form a superset. Prescriptions are text; the API keeps the text and parses targets from it.

  • Program Hypertrophy block Oct 2026 optional
    • Workout Chest + Biceps + Core · 2026-09-28 · planned
      • A1Incline Bench Press · 3 x 12-10-8 reps, rest 30-60s
        • Sets 60 kg × 12 · 60 kg × 10 · 60 kg × 8 created from the suggestion when he starts, then logged
      • A2Alternating Dumbbell Curl · 3 x 14-12-10 reps, rest 30-60s
      • A3Hollow Body Hold · 3x 30-40s, rest as needed
      • B1Dumbbell Fly · … B and C tri-sets follow
      • DCore circuit free-form block: no exercise, logged as text
Workout
One session with a date and a status: plannedin_progresscompleted, or skipped. The agent writes planned ones.
Item
One exercise in a workout, with a superset label (A1, A2, A3), the coach-style prescription and targets parsed from it. Or a free-form block.
Set
Weight + unit (kg or lb, as entered) + reps or seconds, and a done flag. Plans have none until he starts.
Exercise
A canonical movement shared by every workout, so it carries the history, PRs and the next suggestion.
Program
An optional grouping of workouts: a training block.
Suggestion
The prefill for each set: last time's weights and reps, adjusted to the prescription, +1 step when he hit the top of the range.

Recipes

Every command runs as is, in order, against a fresh copy of his data (today = 2026-09-24). jq is only used to trim or edit output.

See what's next and what's planned

Start here. workout is what the app offers next, with prefill; upcoming is the rest of the queue. A planned workout with a past date is overdue and stays next until it is done, moved, skipped or deleted.

bash
curl -sS "https://gym.abdellatif.io/api/next"

Every planned workout, earliest date first:

bash
curl -sS "https://gym.abdellatif.io/api/workouts?status=planned"

One line per workout, in queue order (needs jq):

bash
curl -sS "https://gym.abdellatif.io/api/workouts?status=planned,in_progress&order=asc" | jq -r '.workouts[] | "\(.date)  \(.status)  \(.id)  \(.title)"'

Find the exercise names to use

Plans reference exercises by name, and only a name that matches an exercise (its name or an alias, ignoring case, spacing, hyphens and plurals) links to his history. Search before writing a plan (every word of q must appear), and prefer exercises he has done (times_performed, last_performed count sessions where he did it, not ones he skipped). Read notes too (e.g. a machine his gym doesn't have).

Search names and aliases:

bash
curl -sS "https://gym.abdellatif.io/api/exercises?q=curl"

By muscle group, most performed first:

bash
curl -sS "https://gym.abdellatif.io/api/exercises?muscle_group=chest&sort=frequency"

The whole library, one line each (id, name, muscle, times done, last done):

bash
curl -sS "https://gym.abdellatif.io/api/exercises?sort=frequency" | jq -r '.exercises[] | [.id, .name, .muscle_group, .times_performed, .last_performed] | @tsv'

Check an exercise's history and PRs before progressing it

history is every completed session, newest first, with the prescription at the time and the sets he logged. records holds his PRs and suggestion is what the app would prefill next. Weights are as entered (kg or lb per set).

bash
curl -sS "https://gym.abdellatif.io/api/exercises/incline-bench-press"

The last four sessions, compact:

bash
curl -sS "https://gym.abdellatif.io/api/exercises/incline-bench-press" | jq '{records, next: .suggestion.reason, history: [.history[:4][] | {date, prescription, sets: [.sets[] | "\(.weight) \(.unit) x \(.reps)"]}]}'

What the app would prefill for a prescription you are considering (max marks a max-reps set):

bash
curl -sS "https://gym.abdellatif.io/api/exercises/incline-bench-press/suggestion?target_reps=12,10,8&today=$(date +%F)"

Read recent sessions and stats

Before planning, read what he actually did: completed workouts, his log_notes, and item notes (skip reasons such as Skipped: Machine not available.). prefill=0 leaves out the suggestion fields.

The last six completed workouts (summaries):

bash
curl -sS "https://gym.abdellatif.io/api/workouts?status=completed&limit=6"

One workout in full: items, prescriptions, sets and notes:

bash
curl -sS "https://gym.abdellatif.io/api/workouts/w_0f676309a14e?prefill=0"

A month at a glance, one line per workout:

bash
curl -sS "https://gym.abdellatif.io/api/workouts?status=completed&from=2026-08-01&to=2026-08-31&order=asc" | jq -r '.workouts[] | "\(.date)  \(.title)  \(.done_set_count) sets  \(.volume_kg) kg"'

Totals, weekly volume, streak, muscle recency and recent PRs, for your local day:

bash
curl -sS "https://gym.abdellatif.io/api/stats?today=$(date +%F)"

Create a program

A program groups the workouts of a block so you can list or clear them together. It is optional. Choose the id yourself so the next calls can use it straight away.

bash
curl -sS -X POST "https://gym.abdellatif.io/api/programs" \
  -H 'Content-Type: application/json' \
  --data-binary @- <<'JSON'
{
  "id": "p_oct_2026",
  "name": "Hypertrophy block Oct 2026",
  "description": "4 weeks, 3-day rotation: Chest + Biceps + Core / Back + Triceps + Core / Shoulders + Arms + Legs. Reps wave 12-10-8 → 15-12-10 → 12-10-8 → 10-10-8."
}
JSON

Programs with workout counts, and one program with its workouts:

bash
curl -sS "https://gym.abdellatif.io/api/programs"
curl -sS "https://gym.abdellatif.io/api/programs/p_oct_2026"

Plan next week: one workout per training day

One POST /api/workouts per training day, with the items nested in order. Reference exercises by name, put the coach-style text in prescription (targets are parsed from it) and label supersets A1/A2/A3. Don't send sets or weights: when he starts, the app prefills every set from his history and the prescription. With your own ids a retried POST returns 409 instead of a duplicate (PUT /api/workouts/{id} with the same body is an idempotent alternative: items without ids are matched to the current ones, so their ids stay the same). Check created_exercises in each response: it lists names that matched no exercise.

Monday:

bash
curl -sS -X POST "https://gym.abdellatif.io/api/workouts" \
  -H 'Content-Type: application/json' \
  --data-binary @- <<'JSON'
{
  "id": "w_2026-09-28_chest",
  "program_id": "p_oct_2026",
  "title": "Chest + Biceps + Core",
  "date": "2026-09-28",
  "notes": "Week 1/4. The last set should always be challenging.",
  "items": [
    {"label": "A1", "exercise": "Incline Bench Press", "prescription": "3 x 12-10-8 reps, rest 30-60s\n*Go down in 3 sec*"},
    {"label": "A2", "exercise": "Alternating Dumbbell Curl", "prescription": "3 x 14-12-10 reps, rest 30-60s"},
    {"label": "A3", "exercise": "Hollow Body Hold", "prescription": "3x 30-40s, rest as needed"},
    {"label": "B1", "exercise": "Dumbbell Fly", "prescription": "3 x 10-12 reps, rest 30-60s\n*Slow and Controlled*"},
    {"label": "B2", "exercise": "Barbell Curl", "prescription": "3 x 8-10 reps, rest 30-60s"},
    {"label": "B3", "exercise": "Med Ball Russian Twist", "prescription": "3x 30-40s, rest as needed"},
    {"label": "C1", "exercise": "Hand Release Push-Up", "prescription": "3 x Max reps (-2), rest 30-60s\n*Leave 2 reps in the tank*"},
    {"label": "C2", "exercise": "Single Arm Dumbbell Zottman Curl", "prescription": "3 x 8-10 reps/side, rest 30-60s"},
    {"label": "C3", "exercise": "Hanging Oblique Raises", "prescription": "3x 30-40s, rest as needed"}
  ]
}
JSON

Wednesday:

bash
curl -sS -X POST "https://gym.abdellatif.io/api/workouts" \
  -H 'Content-Type: application/json' \
  --data-binary @- <<'JSON'
{
  "id": "w_2026-09-30_back",
  "program_id": "p_oct_2026",
  "title": "Back + Triceps + Core",
  "date": "2026-09-30",
  "notes": "Week 1/4.",
  "items": [
    {"label": "A", "exercise": "Strict Pull-Up", "prescription": "3 Sets, 5-7 reps (1-sec pause at the top)\nRest as needed"},
    {"label": "B1", "exercise": "V-Bar Cable Lat Pulldown Machine", "prescription": "3 x 12-10-8 reps, rest 30-60s\n*Go up in 3 sec, controlled reps*"},
    {"label": "B2", "exercise": "Dumbbell Incline Tricep Extension", "prescription": "3 x 12-10-8 reps, rest 30-60s"},
    {"label": "B3", "exercise": "Hollow Body Hold", "prescription": "3x 30-40s, rest as needed"},
    {"label": "C1", "exercise": "Bent Over Barbell Row", "prescription": "3 x 12-10-8 reps, rest 30-60s\n*Squeeze at the top and hold for 1 sec*"},
    {"label": "C2", "exercise": "Single Arm Tricep Pushdown", "prescription": "3 x 10-12 reps/side, rest 30-60s\n*Locked elbow*"},
    {"label": "C3", "exercise": "Alternating V-Ups", "prescription": "3x 16-20 Alt Reps, rest as needed"}
  ]
}
JSON

Friday (the D block has no exercise: it is a free-form circuit logged as text):

bash
curl -sS -X POST "https://gym.abdellatif.io/api/workouts" \
  -H 'Content-Type: application/json' \
  --data-binary @- <<'JSON'
{
  "id": "w_2026-10-02_legs",
  "program_id": "p_oct_2026",
  "title": "Shoulders + Arms + Legs",
  "date": "2026-10-02",
  "notes": "Week 1/4.",
  "items": [
    {"label": "A1", "exercise": "Seated Alternating Arnold Dumbbell Press", "prescription": "3 x 12-10-8 reps, rest 30-60s\n*Go down in 3 sec*"},
    {"label": "A2", "exercise": "One and a Quarter Goblet Squat", "prescription": "3 x 12-10-8 reps, rest 30-60s\n*Go down in 3 sec*"},
    {"label": "A3", "exercise": "Dumbbell Curl", "prescription": "3 x 12-10-8 reps, rest 30-60s"},
    {"label": "B1", "exercise": "Dumbbell Tricep Extension", "prescription": "3 x 12-10-8 reps, rest 30-60s"},
    {"label": "B2", "exercise": "Dumbbell Walking Lunge", "prescription": "3 x 16-14-12 alternating reps, rest as needed"},
    {"label": "B3", "exercise": "Chainsaw Row", "prescription": "3 x 8-10 reps/side, rest 30-60s"},
    {"label": "C1", "exercise": "Dumbbell Hip Thrust", "prescription": "3 x 10-12 reps @2 sec pause at the top"},
    {"label": "C2", "exercise": "Leg Extension Machine", "prescription": "3 x 12-15 reps, rest 30-60s"},
    {"label": "D", "name": "Core circuit", "prescription": "3 Sets:\n12 DB Side Plank Rotations/side\n20 Hollow rocks\n20 Alternating V-ups\n-Rest 60-90 Sec-"}
  ]
}
JSON

Change a planned workout

From the smallest change to a full rewrite. Item ids come from the workout (GET …?prefill=0). Commands in this recipe reuse $ITEM from the first one.

Find an item by its label:

bash
ITEM=$(curl -sS "https://gym.abdellatif.io/api/workouts/w_2026-09-28_chest?prefill=0" | jq -r '.items[] | select(.label == "B1") | .id')

Change its prescription (targets and cue are parsed again):

bash
curl -sS -X PATCH "https://gym.abdellatif.io/api/items/$ITEM" \
  -H 'Content-Type: application/json' \
  -d '{"prescription": "3 x 12-10-8 reps, rest 30-60s\n*Slow and controlled*"}'

Swap the exercise, keeping label and prescription (the item takes the new exercise's name):

bash
curl -sS -X PATCH "https://gym.abdellatif.io/api/items/$ITEM" \
  -H 'Content-Type: application/json' \
  -d '{"exercise": "Chest Fly Machine"}'

Add an exercise at the end (send position to put it elsewhere, then reorder):

bash
curl -sS -X POST "https://gym.abdellatif.io/api/workouts/w_2026-09-28_chest/items" \
  -H 'Content-Type: application/json' \
  --data-binary @- <<'JSON'
{
  "id": "i_2026-09-28_chest_d",
  "label": "D",
  "exercise": "Dumbbell Hammer Curl",
  "prescription": "2 x 15-20 reps, rest 30s\n*Squeeze at the top*"
}
JSON

Remove an item:

bash
curl -sS -X DELETE "https://gym.abdellatif.io/api/items/i_2026-09-28_chest_d"

Reorder: item_ids must list every item. This sorts them by label:

bash
curl -sS "https://gym.abdellatif.io/api/workouts/w_2026-09-28_chest?prefill=0" | jq '{item_ids: (.items | sort_by(.label) | map(.id))}' \
  | curl -sS -X POST "https://gym.abdellatif.io/api/workouts/w_2026-09-28_chest/reorder" -H 'Content-Type: application/json' --data-binary @-

GET, edit, PUT back. The GET response is valid PUT input; a changed prescription is parsed again (the old target fields next to it are ignored), and a changed exercise name swaps the exercise:

bash
curl -sS "https://gym.abdellatif.io/api/workouts/w_2026-09-28_chest?prefill=0" \
  | jq '.notes = "Week 1/4. Heavier than last cycle." | (.items[] | select(.label == "A1")) |= (.prescription = "4 x 10-8-8-6 reps, rest 60-90s\n*Go down in 3 sec*")' \
  | curl -sS -X PUT "https://gym.abdellatif.io/api/workouts/w_2026-09-28_chest" -H 'Content-Type: application/json' --data-binary @-

Rewrite a plan completely: PUT the new version. Items without ids take the id of a current item with the same label and exercise; the rest replace the current items. On a plan nothing is lost; on a started workout a PUT that would delete logged sets is refused (409 would_lose_logged_sets):

bash
curl -sS -X PUT "https://gym.abdellatif.io/api/workouts/w_2026-10-02_legs" \
  -H 'Content-Type: application/json' \
  --data-binary @- <<'JSON'
{
  "title": "Lower Body",
  "notes": "Week 1/4. No overhead pressing this week (shoulder).",
  "items": [
    {"label": "A1", "exercise": "One and a Quarter Goblet Squat", "prescription": "3 x 12-10-8 reps, rest 30-60s\n*Go down in 3 sec*"},
    {"label": "A2", "exercise": "Prone Hamstring Curl Machine", "prescription": "3 x 12-10-8 reps, rest 30-60s"},
    {"label": "B1", "exercise": "Dumbbell Walking Lunge", "prescription": "3 x 16-14-12 alternating reps, rest 30-60s"},
    {"label": "B2", "exercise": "Leg Extension Machine", "prescription": "3 x 12-15 reps, rest 30-60s"},
    {"label": "C1", "exercise": "Dumbbell Hip Thrust", "prescription": "3 x 10-12 reps @2 sec pause at the top"},
    {"label": "C2", "exercise": "Leg Press Machine Calf Raises", "prescription": "3 x 15-20 reps, rest 30-60s\n*Pause 1 sec at the bottom*"}
  ]
}
JSON

Reschedule, reorder, skip, restore or delete planned workouts

The queue is ordered by date, then position. Skipping keeps a workout (restorable); deleting removes it with its items. Don't delete or rewrite completed workouts unless he asks.

Move a plan to another day:

bash
curl -sS -X PATCH "https://gym.abdellatif.io/api/workouts/w_2026-09-30_back" \
  -H 'Content-Type: application/json' \
  -d '{"date": "2026-10-01"}'

The overdue plan from 7 Sep (Week 1/4) is still next. Move it to tomorrow…

bash
curl -sS -X PATCH "https://gym.abdellatif.io/api/workouts/w_486229e6a086" \
  -H 'Content-Type: application/json' \
  -d '{"date": "2026-09-25"}'

…or skip it:

bash
curl -sS -X POST "https://gym.abdellatif.io/api/workouts/w_486229e6a086/skip"

Restore a skipped workout to the plan:

bash
curl -sS -X PATCH "https://gym.abdellatif.io/api/workouts/w_486229e6a086" \
  -H 'Content-Type: application/json' \
  -d '{"status": "planned", "date": "2026-09-25"}'

Two plans on the same day: the lower position goes first:

bash
curl -sS -X PATCH "https://gym.abdellatif.io/api/workouts/w_486229e6a086" \
  -H 'Content-Type: application/json' \
  -d '{"position": -1}'

Repeat a day next week (items and prescriptions, no sets):

bash
curl -sS -X POST "https://gym.abdellatif.io/api/workouts/w_2026-09-28_chest/duplicate" \
  -H 'Content-Type: application/json' \
  -d '{"id": "w_2026-10-05_chest", "date": "2026-10-05"}'

Delete a plan:

bash
curl -sS -X DELETE "https://gym.abdellatif.io/api/workouts/w_2026-10-05_chest"

Create or adjust an exercise

A new name in a plan creates the exercise with guessed metadata. To control the unit, step or tracking, create it first. Aliases make other spellings resolve to it.

bash
curl -sS -X POST "https://gym.abdellatif.io/api/exercises" \
  -H 'Content-Type: application/json' \
  --data-binary @- <<'JSON'
{
  "name": "Cable Crossover",
  "muscle_group": "chest",
  "secondary_muscles": [
    "shoulders"
  ],
  "equipment": "cable",
  "tracking": "weight_reps",
  "default_unit": "lb",
  "weight_step": 5
}
JSON

Add aliases and a note (aliases replaces the list):

bash
curl -sS -X PATCH "https://gym.abdellatif.io/api/exercises/cable-crossover" \
  -H 'Content-Type: application/json' \
  --data-binary @- <<'JSON'
{
  "aliases": [
    "Standing Cable Fly",
    "Cable Crossovers"
  ],
  "notes": "High-to-low, slight forward lean."
}
JSON

Now "exercise": "Standing Cable Fly" in a plan links to it:

bash
curl -sS "https://gym.abdellatif.io/api/exercises?q=cable%20fly"

Record a session done outside the app (only when he asks)

Post it as completed with its sets. On a workout created as completed, sets default to done: true and items to done (or skipped without sets or a result), as the complete action would do. It then counts for history, records, stats and future suggestions. A result_text stays text: send sets for anything that should count.

bash
curl -sS -X POST "https://gym.abdellatif.io/api/workouts" \
  -H 'Content-Type: application/json' \
  --data-binary @- <<'JSON'
{
  "id": "w_2026-09-23_hotel",
  "title": "Hotel gym: Upper",
  "date": "2026-09-23",
  "status": "completed",
  "log_notes": "Hotel gym, dumbbells only up to 25 kg.",
  "items": [
    {"label": "A1", "exercise": "Dumbbell Bench Press", "prescription": "3 x 10-12 reps, rest 60s", "status": "done", "sets": [{"weight": 25, "unit": "kg", "reps": 12, "done": true}, {"weight": 25, "unit": "kg", "reps": 11, "done": true}, {"weight": 25, "unit": "kg", "reps": 10, "done": true}]},
    {"label": "A2", "exercise": "Chainsaw Row", "prescription": "3 x 8-10 reps/side, rest 60s", "status": "done", "sets": [{"weight": 22.5, "unit": "kg", "reps": 10, "done": true}, {"weight": 22.5, "unit": "kg", "reps": 10, "done": true}, {"weight": 22.5, "unit": "kg", "reps": 9, "done": true}]}
  ]
}
JSON

Export everything, import in bulk

The export is one JSON document with every exercise, program and workout (items and sets nested). Import upserts any subset of that shape in one call; workouts follow the PUT rules (by id, else external_id).

Back up:

bash
curl -sS "https://gym.abdellatif.io/api/export" -o gym-export.json

Create or update several plans at once (safe to run twice):

bash
curl -sS -X POST "https://gym.abdellatif.io/api/import" \
  -H 'Content-Type: application/json' \
  --data-binary @- <<'JSON'
{
  "workouts": [
    {"id": "w_2026-10-05_chest", "program_id": "p_oct_2026", "title": "Chest + Biceps + Core", "date": "2026-10-05", "notes": "Week 2/4.", "items": [{"label": "A1", "exercise": "Incline Bench Press", "prescription": "3 x 15-12-10 reps, rest 30-60s\n*Go down in 3 sec*"}, {"label": "A2", "exercise": "Alternating Dumbbell Curl", "prescription": "3 x 15-12-10 reps, rest 30-60s"}, {"label": "A3", "exercise": "Hollow Body Hold", "prescription": "3x 30-40s, rest as needed"}]},
    {"id": "w_2026-10-07_back", "program_id": "p_oct_2026", "title": "Back + Triceps + Core", "date": "2026-10-07", "notes": "Week 2/4.", "items": [{"label": "A1", "exercise": "V-Bar Cable Lat Pulldown Machine", "prescription": "3 x 15-12-10 reps, rest 30-60s"}, {"label": "A2", "exercise": "Rope Tricep Pushdown", "prescription": "3 x 15-12-10 reps, rest 30-60s"}, {"label": "A3", "exercise": "Alternating V-Ups", "prescription": "3x 16-20 Alt Reps, rest as needed"}]}
  ]
}
JSON

Restore the backup. Workouts in the file get exactly the file's items and sets; workouts created since are untouched:

bash
curl -sS -X POST "https://gym.abdellatif.io/api/import" \
  -H 'Content-Type: application/json' \
  --data-binary @gym-export.json

Preview how text will be parsed

Check a prescription before writing it. The result parser shows how a hand-typed result reads as sets; writes never convert result_text into sets.

bash
curl -sS -X POST "https://gym.abdellatif.io/api/parse" \
  -H 'Content-Type: application/json' \
  --data-binary @- <<'JSON'
{
  "prescription": "3 x Max Reps (-2), rest 60-90s\n*Leave 2 reps in the tank*",
  "result": "25kg x 12 x 3"
}
JSON

Rules

Status lifecycle

  • planned: a plan. This is what the agent writes.
  • in_progress: started in the app (POST /api/workouts/{id}/start).
  • completed: history. Only completed workouts count for history, records, suggestions and stats.
  • skipped: not done. Restore with PATCH {"status": "planned"}.

Transitions go through the actions: start (planned or skipped → in_progress), complete (→ completed) and skip (planned → skipped; a completed or in-progress workout needs force=1). Setting status with PUT or PATCH keeps the timestamps consistent but does not create or clean up sets. Don't change or delete completed workouts unless he asks.

The next workout

GET /api/next returns the in_progress workout (latest started), else the first planned workout ordered by date (unscheduled last), then position (lower first), then creation time. Past dates stay eligible: an overdue plan remains next (the app marks it overdue) until it is started, skipped, rescheduled or deleted. upcoming lists the other planned workouts in the same order. Give every plan a date; to put a plan first among those on the same day, give it a lower position.

Referencing exercises

Items point at an exercise with exercise_id, or with exercise (a name). A name is matched in this order: an exercise's name, then an alias (both trimmed, case-insensitive), then loosely (case, spaces, hyphens, apostrophes and a plural s don't matter: Strict Pull Up, strict pull-ups → Strict Pull-Up; Captains Chair Knee Tuck → Captain's Chair Knee Tucks), then a name whose slug is an exercise id. An exercise's own name always beats another's alias. When nothing matches, a new exercise is created: its id is the slug of the name (Cable Crossovercable-crossover), muscle group, equipment, tracking (reps for bodyweight moves, time for holds and planks) and per-side are guessed from whole words in the name, and the write response lists it in created_exercises. A new exercise has no history, so the app has nothing to prefill. Look names up first with GET /api/exercises?q=… (every word must appear: q=pull up finds Strict Pull-Up), and check created_exercises after writing: a different movement name (Leg Extension vs Leg Extension Machine) still splits the history. Fix a duplicate with POST /api/exercises/{id}/merge.

The item's name defaults to the exercise's canonical name. Send name as well to show a variant ("Paused Bench Press") and keep the history link. A new item with only name that is exactly an exercise's name or alias links to that exercise. Any other item with name and no exercise is a free-form block (AMRAP, circuit, cardio): no sets, no suggestion, logged as text in result_text (send exercise_id: null to force a free-form block).

Prescriptions

Put the plan in prescription the way a coach writes it; he sees this text as is. The server parses it into targets (target_sets, target_reps, rep_min, rep_max, target_seconds, rest_seconds, rir) and a cue from the extra lines. Any of those fields sent in the same request overrides the parsed value (null included); leave them out unless the parser gets it wrong. A new prescription is parsed again on update, and target fields that just repeat the stored values are ignored then (an edited GET response); clearing it (null or "") clears its targets and cue. Line breaks are \n inside the JSON string. Preview with POST /api/parse.

PrescriptionParsed as
3 x 12-10-8 reps, rest 30-60s\n*Go down in 3 sec*3 sets of 12, 10, 8 (range 8-12), rest 45 s, cue "Go down in 3 sec"
3 x 10-12 reps, rest 1-2 min3 sets, range 10-12, rest 90 s
3x 30-40s, rest as needed3 sets, hold 40 s, rest null
3 x Max Reps (-1), rest 1-2 min3 max-rep sets (target_reps [null, null, null]), rir 1, rest 90 s
3 x 5 reps @2 RIR, rest 2 min3 sets of 5, rir 2, rest 120 s
3 x 8-10 reps/side, rest 30-60s3 sets, range 8-10, rest 45 s, per side
4 sets, 10-8-64 sets of 10, 8, 6, 6
3 x 12, 10, 8 reps · 3 x 12/10/8 · 3 x 12 10 83 sets of 12, 10, 8 (commas, slashes and spaces work like dashes)
3 x 10-12 reps, tempo 3-1-1-0range 10-12, cue "Tempo 3-1-1-0" (a tempo is never a rep scheme)
3 x 10-12 reps @2 sec pause at the toprange 10-12, cue "2 sec pause at the top"
AMRAP, EMOM or a list of circuit movesfree-form: no set or rep targets (rest only)

Supersets and labels

label groups items. Consecutive items that share a letter are one group done in rotation: A1 + A2 is a superset, A1-A3 a tri-set, four or more a circuit. A bare letter (D) is a single block. Items appear in position order (the array order you send), so keep each group together and the letters in order. His recent programs are three tri-sets per day with a core exercise in the third slot (A3, B3, C3).

Writes and merge rules

  • POST /api/workouts creates a workout with nested items (and sets) in one atomic call. 409 if the id or external_id already exists, so a retried POST never duplicates.
  • PUT /api/workouts/{id} creates or updates the workout with that id (201 or 200) and merges:
    • Workout fields you omit keep their values.
    • items, when present, is the complete list. Items with the id of an existing item of this workout are updated; an item without an id updates a current item with the same label and exercise (else the same exercise), so re-sending a plan keeps its item ids (the phone may have cached them); other items are created; existing items left out are deleted with their sets. Omit items to leave them alone.
    • Updated items merge too: omitted item fields keep their values.
    • sets inside an item, when present, is its complete set list (same rules). Omit sets to keep them.
    • Array order sets position (1, 2, 3 …) unless you send position.
    • Logged work is protected: deleting a done set or an item with done sets or a logged result, undoing a done set, overwriting a set that changed since you read it (updated_at), or changing the exercise of an item with done sets (PUT or item PATCH) is a 409 would_lose_logged_sets unless ?force=1. On an in-progress or completed workout use the item and set PATCHes instead.
  • PATCH /api/workouts/{id} changes workout fields only. PATCH /api/items/{id} and PATCH /api/sets/{id} merge the same way; an item PATCH may carry a complete sets list.
  • A GET response can be sent back to PUT unchanged (read-only fields are ignored). Editing an item's prescription in it re-parses the targets and cue (the stale copies next to it are ignored); editing its exercise name swaps the exercise and the item's name. Set ids copied from last.sets belong to another item: a 409.
  • Every write returns the updated resource. Unknown fields are ignored.

Ids and safe retries

Every create accepts a client-generated id (workouts, items, sets, programs, exercises) matching ^[A-Za-z0-9_-]{1,64}$; otherwise the server makes one (w_…, i_…, s_…, p_…, and a slug of the name for exercises). Choosing ids up front (w_2026-09-28_chest) lets you refer to a workout without reading the response and makes retries safe: a repeated POST returns 409 instead of a duplicate, and a repeated PUT has the same result (items without ids are matched to the current ones, so their ids don't change). Ids are global per table: an item id that belongs to another workout, or a set id of another item, is a 409; the same set id twice in one body is a 400. Giving items ids too (i_2026-09-28_chest_a1) makes later edits exact.

Start, complete, skip, duplicate

  • Start (POST …/start): status → in_progress, started_at = now, date = body date or today (an overdue plan moves to the day it is done). Every exercise item without sets (except text tracking) gets sets from its suggestion: prefilled weight, reps or seconds, done: false. Starting again only fills items that still have no sets. A completed workout → 409 already_completed.
  • Complete (POST …/complete): sets never marked done are deleted (discard_undone, default true); each item becomes done (a done set, a non-empty result_text, or already done) or skipped; status → completed, completed_at = now; date = body date, else the workout's date if it was in progress or already completed, else today; log_notes is replaced when sent. Works on a planned workout too, and repeating it keeps the original date and time.
  • Skip (POST …/skip): for plans; status → skipped, nothing else changes. Undo with PATCH {"status": "planned"}. A completed workout → 409 already_completed, an in-progress one → 409 in_progress (?force=1 to skip anyway).
  • Duplicate (POST …/duplicate): a new planned workout with the same items and prescriptions, without sets, results or his post-session item notes. Body {id?, title?, date?}.

Suggestions (prefill)

When a workout is read with prefill, each exercise item carries last (the most recent completed session of that exercise other than this workout that he did; a session with logged sets beats a text-only one, and skipped items don't count) and suggestion:

  • Sets = target_sets, else the length of target_reps, else the number of working sets last time, else 3 (1-20).
  • Unit = the unit of last time's working sets, else the exercise's default_unit.
  • Weight of set i = last time's working set i (the last one repeats), rounded to 0.25.
  • Reps of set i = target_reps[i]; else, with a rep range, last time's reps + 1 kept within rep_min-rep_max (rep_max without history); else last time's reps.
  • Progress: if every working set last time reached the top of last time's target (target_reps[i] or rep_max, falling back to this plan's) and no fewer sets than prescribed were done, weights go up by the step and reps restart at target_reps[i] or rep_min (kind: progress), except on sets whose rep target is above what he did (never heavier and more reps at once). Step = the exercise's weight_step when last time's unit is its default_unit, else 5 lb or 2.5 kg.
  • More reps: a rep target up to 2 above what he did at that weight keeps the weight (progress, e.g. "→ 11·11·11 reps"). More than 2 above (a new block: 8 → 20 reps): the weight is scaled from that session's best Epley 1RM, e1RM / (1 + reps / 30), rounded down to the step and never above last time's (kind: deload, "lighter weight").
  • Long break: he last did it (with or without logged sets) more than 28 days before today → no step and no +1 rep; same weights, "ease back in" (repeat, or progress when the plan's own reps are higher).
  • Fell short: more than half the sets 2 or more reps under the minimum → same weights (repeat).
  • Bodyweight (reps tracking, or no weights last time): last time's reps (+1 within a range); max-rep sets (null in target_reps) without an rir target aim for one more (progress).
  • Time tracking: last time's seconds, else target_seconds. Text tracking: no sets.
  • Logged as text only ("Done", no sets): reps or hold time from the targets, weights null, kind: repeat ("Last time (…) logged as text").
  • No history: kind: new, weights null, reps from the targets.

Write prescriptions, not weights: the app shows the suggestion with its reason and he adjusts. To prescribe a specific load on purpose (a deload, a test day), put undone sets on the plan item (sets: [{"weight": 55, "unit": "kg", "reps": 10}, …], no done) and say why in cue or notes: start keeps an item's own sets instead of the suggestion, and on a planned workout the item's suggestion describes them ("Set in the plan: …", deload when lighter than last time). To program progression, read GET /api/exercises/{id} (history, records, suggestion) or try a prescription with GET /api/exercises/{id}/suggestion?target_reps=12,10,8.

Dates and the today parameter

date fields are local calendar days (YYYY-MM-DD); *_at fields are UTC timestamps. The server clock is UTC, so every endpoint accepts ?today=YYYY-MM-DD for the local day. It drives the long-break check in suggestions, the stats windows (this week, streak, 7- and 30-day counts) and the default date of start and complete. An invalid value is a 400, including a day that doesn't exist (2026-09-31, 2027-02-29) in any date field.

Units and derived numbers

Weights are stored exactly as entered, with unit kg or lb on each set (machines and cables are often in lb). Never convert when writing. Fields ending in _kg (volume_kg, best_weight_kg, top_weight_kg, e1rm_kg …) are converted (lb × 0.45359237) and rounded to 0.1 kg (except an item's best, which is exact for record checks); stats totals to 1 kg. e1RM is Epley, weight × (1 + reps / 30), for sets of 1-12 reps. Volume is Σ weight × reps over done, non-warm-up sets.

Limits and validation

FieldLimit
ids^[A-Za-z0-9_-]{1,64}$
title, name, exercise, external_id200 characters
exercise aliases50 names of up to 200 characters
item label, prescription, cue, result_text, notes; exercise notes5,000 characters
set notes2,000 characters
warmup, workout notes, log_notes, program description20,000 characters
items per workout body100
sets per item50
target_reps50 entries, integers or null
weight0-2,000
repsinteger 0-1,000
seconds and item targetsinteger 0-86,400
set rir0-20
weight_step0-100
positionworkouts: integer ±1,000,000; items and sets: 0-1,000
GET /api/workoutslimit 1-200, default 50
POST /api/parse5,000 characters per text

Numbers may be sent as plain decimal strings ("12.5", also inside target_reps); "0x14", "1e1" and the like are a 400, and a blank string means null. Booleans must be JSON booleans; enums are case-sensitive; dates must be real calendar days. Unknown fields are ignored.

Errors

Errors are JSON with a 4xx or 5xx status: {"error": {"code": "…", "message": "…", "details": …}}. The message names the problem and where it is (items[1].sets[0].reps must be an integer, items[2] needs exercise_id, exercise or name).

StatuscodeWhen
400bad_requestInvalid JSON, a field of the wrong type or out of range, a bad date, a missing title/name, items on PATCH, a set id used twice in one body
404not_foundUnknown id, or no such route
409conflictThe id, external_id, exercise name or alias already exists; an item or set id belongs to another workout or item
409already_completedStarting or skipping a completed workout
409in_progressSkipping a workout in progress (force=1 to skip anyway)
409would_lose_logged_setsA PUT would delete or undo logged sets or results, overwrite a set changed since it was read, or move done sets to another exercise (also an item PATCH); details.losses lists them (force=1 to replace anyway)
422unknown_exerciseexercise_id does not exist
422unknown_programprogram_id does not exist
422invalid_referenceAnother broken reference
500internalUnexpected; the message says what failed

HTTP

No authentication and open CORS: anyone can read and write, so never put secrets in notes. Send JSON bodies with Content-Type: application/json. GET responses are Cache-Control: no-store.

Endpoints

Tap an endpoint for its parameters, body, responses and examples. Machine-readable: openapi.json.

Plan

What is next and what is queued.

GET /api/nextThe next workout (prefilled) and the planned queue

workout is the in_progress workout (latest started), else the first planned workout by date (unscheduled last), then position, then creation time. Past dates stay eligible, so an overdue plan remains next until it is started, skipped, rescheduled or deleted. upcoming lists the other planned workouts in the same order (at most 10).

Parameters

today date

Your local day (YYYY-MM-DD). Drives the long-break check in suggestions, the stats windows and default dates. Default: the server's UTC date.

Responses

  • 200 NextResponse The next workout and the queue. workout is null when nothing is planned.
Example response (200)
json
{
  "workout": {
    "id": "w_486229e6a086",
    "program_id": "p_4wk_sep_2026",
    "title": "Week 1/4",
    "date": "2026-09-07",
    "status": "planned",
    "position": 0,
    "warmup": "2-3 Min Blood Flow on any machine @EZ pace",
    "notes": "Push Day",
    "log_notes": null,
    "started_at": null,
    "completed_at": null,
    "source": "coachrx",
    "external_id": "workout-486229e6a086",
    "created_at": "2026-09-24T00:00:00.000Z",
    "updated_at": "2026-09-24T00:00:00.000Z",
    "items": [
      {
        "target_seconds": null,
        "rir": null,
        "cue": "Go down in 3 sec",
        "result_text": null,
        "notes": null,
        "external_id": "exercise-5dcb10ef751b",
        "id": "i_5dcb10ef751b",
        "workout_id": "w_486229e6a086",
        "exercise_id": "bench-press",
        "position": 1,
        "label": "A1",
        "name": "Bench Press",
        "prescription": "3 x 12-10-8 Reps, rest 1-2 min\n* Go down in 3 sec*",
        "target_sets": 3,
        "target_reps": [
          12,
          10,
          8
        ],
        "rep_min": 8,
        "rep_max": 12,
        "rest_seconds": 90,
        "status": "pending",
        "created_at": "2026-09-24T00:00:00.000Z",
        "updated_at": "2026-09-24T00:00:00.000Z",
        "sets": [],
        "exercise": {
          "id": "bench-press",
          "name": "Bench Press",
          "aliases": [],
          "muscle_group": "chest",
          "secondary_muscles": [
            "triceps",
            "shoulders"
          ],
          "equipment": "barbell",
          "tracking": "weight_reps",
          "per_side": false,
          "default_unit": "kg",
          "weight_step": 2.5,
          "notes": null,
          "created_at": "2026-09-24T00:00:00.000Z",
          "updated_at": "2026-09-24T00:00:00.000Z"
        },
        "last": {
          "workout_id": "w_82bdd0102ef5",
          "workout_title": "Chest + Biceps + Core",
          "date": "2026-07-10",
          "item_id": "i_f9ef7b68d2cb",
          "prescription": "3 x 8-8-8 reps, rest 30-60s",
          "result_text": "50kg x 10\n55kg x 8\n60kg x8",
          "sets": [
            {
              "id": "s_f9ef7b68d2cb_1",
              "item_id": "i_f9ef7b68d2cb",
              "position": 1,
              "weight": 50,
              "unit": "kg",
              "reps": 10,
              "seconds": null,
              "rir": null,
              "is_warmup": false,
              "done": true,
              "done_at": "2026-07-10T12:00:00.000Z",
              "notes": null,
              "created_at": "2026-09-24T00:00:00.000Z",
              "updated_at": "2026-09-24T00:00:00.000Z"
            },
            {
              "id": "s_f9ef7b68d2cb_2",
              "item_id": "i_f9ef7b68d2cb",
              "position": 2,
              "weight": 55,
              "unit": "kg",
              "reps": 8,
              "seconds": null,
              "rir": null,
              "is_warmup": false,
              "done": true,
              "done_at": "2026-07-10T12:00:00.000Z",
              "notes": null,
              "created_at": "2026-09-24T00:00:00.000Z",
              "updated_at": "2026-09-24T00:00:00.000Z"
            },
            {
              "id": "s_f9ef7b68d2cb_3",
              "item_id": "i_f9ef7b68d2cb",
              "position": 3,
              "weight": 60,
              "unit": "kg",
              "reps": 8,
              "seconds": null,
              "rir": null,
              "is_warmup": false,
              "done": true,
              "done_at": "2026-07-10T12:00:00.000Z",
              "notes": null,
              "created_at": "2026-09-24T00:00:00.000Z",
              "updated_at": "2026-09-24T00:00:00.000Z"
            }
          ]
        },
        "suggestion": {
          "sets": [
            {
              "weight": 50,
              "unit": "kg",
              "reps": 12,
              "seconds": null
            },
            {
              "weight": 55,
              "unit": "kg",
              "reps": 10,
              "seconds": null
            },
            {
              "weight": 60,
              "unit": "kg",
              "reps": 8,
              "seconds": null
            }
          ],
          "reason": "Last done 11 weeks ago (50 kg × 10, 55 kg × 8, 60 kg × 8) — same weight to ease back in, 12·10·8 reps as planned",
          "kind": "progress"
        },
        "best": {
          "weight_kg": 60,
          "e1rm_kg": 76,
          "reps": 15
        }
      }
    ]
  },
  "upcoming": [
    {
      "id": "w_2026-09-28_chest",
      "program_id": "p_oct_2026",
      "title": "Chest + Biceps + Core",
      "date": "2026-09-28",
      "status": "planned",
      "position": 0,
      "warmup": null,
      "notes": "Week 1/4. The last set should always be challenging.",
      "log_notes": null,
      "started_at": null,
      "completed_at": null,
      "source": "api",
      "external_id": null,
      "created_at": "2026-09-24T09:45:17.363Z",
      "updated_at": "2026-09-24T09:45:17.363Z",
      "item_count": 9,
      "set_count": 0,
      "done_set_count": 0,
      "volume_kg": 0,
      "muscle_groups": [
        "chest",
        "biceps",
        "core"
      ],
      "exercise_names": [
        "Incline Bench Press",
        "Alternating Dumbbell Curl",
        "Hollow Body Hold",
        "Dumbbell Fly",
        "Barbell Curl",
        "Med Ball Russian Twist",
        "Hand Release Push-Up",
        "Single Arm Dumbbell Zottman Curl",
        "Hanging Oblique Raises"
      ]
    }
  ]
}

Workouts

Plans and history, with nested items and sets, plus the start / complete / skip actions.

GET /api/workoutsList workouts (summaries, no items)

Newest first by default; when status lists only planned and/or in_progress (the queue), oldest first. Unscheduled workouts (no date) sort last in ascending order and first in descending order. Date filters exclude unscheduled workouts.

Parameters

status string

Comma list of statuses, e.g. planned,in_progress.

from date

Only dates on or after this day.

to date

Only dates on or before this day.

program_id string

Only this program's workouts.

q string

Substring of the title or of an item name (case-insensitive, literal: % and _ are not wildcards), e.g. hip thrust.

limit integer

Page size, 1-200 (larger is capped at 200; 0 or a non-integer is a 400). Default 50.

offset integer

Rows to skip.

order "asc" | "desc"

By date, then position.

Responses

  • 200 WorkoutList One page of summaries and the total count.
  • 400 Error Invalid input (bad_request).
Example response (200)
json
{
  "workouts": [
    {
      "id": "w_0f676309a14e",
      "program_id": "p_split_2026",
      "title": "Chest + Biceps + Core",
      "date": "2026-08-24",
      "status": "completed",
      "position": 0,
      "warmup": "2-3 Min Blood Flow on any machine @EZ pace\n\n2x\n10 Banded Pull-aparts\n5 Slow Prone T on Bench\n20s High Plank",
      "notes": null,
      "log_notes": null,
      "started_at": null,
      "completed_at": "2026-08-24T12:00:00.000Z",
      "source": "coachrx",
      "external_id": "workout-0f676309a14e",
      "created_at": "2026-09-24T00:00:00.000Z",
      "updated_at": "2026-09-24T00:00:00.000Z",
      "item_count": 9,
      "set_count": 17,
      "done_set_count": 17,
      "volume_kg": 4285,
      "muscle_groups": [
        "chest",
        "biceps",
        "core"
      ],
      "exercise_names": [
        "Incline Bench Press",
        "Alternating Dumbbell Curl",
        "Hollow Body Hold",
        "Dumbbell Fly",
        "Barbell Curl",
        "Med Ball Russian Twist",
        "Hand Release Push-Up",
        "Single Arm Dumbbell Zottman Curl",
        "Hanging Oblique Raises"
      ]
    }
  ],
  "total": 73,
  "limit": 1,
  "offset": 0
}
POST /api/workoutsCreate a workout with nested items (and sets) in one call

Atomic: everything is saved or nothing is. status defaults to planned, source to api, position to 0. Items reference exercises by exercise (name) or exercise_id; unknown names create exercises, and the response lists them in created_exercises (check it for typos). Targets are parsed from each prescription. Leave sets out of plans. 409 when the id or external_id already exists, so a retried POST never duplicates. Created as completed (a past session), new sets default to done: true and items to done (with sets or a result) or skipped.

Parameters

today date

Your local day (YYYY-MM-DD). Drives the long-break check in suggestions, the stats windows and default dates. Default: the server's UTC date.

Body

WorkoutCreate. The workout, with items in order.

json
{
  "id": "w_2026-09-28_chest",
  "program_id": "p_oct_2026",
  "title": "Chest + Biceps + Core",
  "date": "2026-09-28",
  "notes": "Week 1/4. The last set should always be challenging.",
  "items": [
    {
      "label": "A1",
      "exercise": "Incline Bench Press",
      "prescription": "3 x 12-10-8 reps, rest 30-60s\n*Go down in 3 sec*"
    },
    {
      "label": "A2",
      "exercise": "Alternating Dumbbell Curl",
      "prescription": "3 x 14-12-10 reps, rest 30-60s"
    },
    {
      "label": "A3",
      "exercise": "Hollow Body Hold",
      "prescription": "3x 30-40s, rest as needed"
    },
    {
      "label": "B1",
      "exercise": "Dumbbell Fly",
      "prescription": "3 x 10-12 reps, rest 30-60s\n*Slow and Controlled*"
    },
    {
      "label": "B2",
      "exercise": "Barbell Curl",
      "prescription": "3 x 8-10 reps, rest 30-60s"
    },
    {
      "label": "B3",
      "exercise": "Med Ball Russian Twist",
      "prescription": "3x 30-40s, rest as needed"
    },
    {
      "label": "C1",
      "exercise": "Hand Release Push-Up",
      "prescription": "3 x Max reps (-2), rest 30-60s\n*Leave 2 reps in the tank*"
    },
    {
      "label": "C2",
      "exercise": "Single Arm Dumbbell Zottman Curl",
      "prescription": "3 x 8-10 reps/side, rest 30-60s"
    },
    {
      "label": "C3",
      "exercise": "Hanging Oblique Raises",
      "prescription": "3x 30-40s, rest as needed"
    }
  ]
}

Responses

  • 201 WorkoutDetail The created workout with prefill (this example: the first of its nine items).
  • 400 Error Invalid input (bad_request).
  • 409 Error Already exists or belongs elsewhere (conflict, already_completed, in_progress, would_lose_logged_sets).
  • 422 Error A reference does not exist (unknown_exercise, unknown_program, invalid_reference).
Example response (201)
json
{
  "id": "w_2026-09-28_chest",
  "program_id": "p_oct_2026",
  "title": "Chest + Biceps + Core",
  "date": "2026-09-28",
  "status": "planned",
  "position": 0,
  "warmup": null,
  "notes": "Week 1/4. The last set should always be challenging.",
  "log_notes": null,
  "started_at": null,
  "completed_at": null,
  "source": "api",
  "external_id": null,
  "created_at": "2026-09-24T09:45:17.363Z",
  "updated_at": "2026-09-24T09:45:17.363Z",
  "items": [
    {
      "target_seconds": null,
      "rir": null,
      "cue": "Go down in 3 sec",
      "result_text": null,
      "notes": null,
      "external_id": null,
      "id": "i_g1txsse06",
      "workout_id": "w_2026-09-28_chest",
      "exercise_id": "incline-bench-press",
      "position": 1,
      "label": "A1",
      "name": "Incline Bench Press",
      "prescription": "3 x 12-10-8 reps, rest 30-60s\n*Go down in 3 sec*",
      "target_sets": 3,
      "target_reps": [
        12,
        10,
        8
      ],
      "rep_min": 8,
      "rep_max": 12,
      "rest_seconds": 45,
      "status": "pending",
      "created_at": "2026-09-24T09:45:17.363Z",
      "updated_at": "2026-09-24T09:45:17.363Z",
      "sets": [],
      "exercise": {
        "id": "incline-bench-press",
        "name": "Incline Bench Press",
        "aliases": [],
        "muscle_group": "chest",
        "secondary_muscles": [
          "shoulders",
          "triceps"
        ],
        "equipment": "barbell",
        "tracking": "weight_reps",
        "per_side": false,
        "default_unit": "kg",
        "weight_step": 2.5,
        "notes": null,
        "created_at": "2026-09-24T00:00:00.000Z",
        "updated_at": "2026-09-24T00:00:00.000Z"
      },
      "last": {
        "workout_id": "w_0f676309a14e",
        "workout_title": "Chest + Biceps + Core",
        "date": "2026-08-24",
        "item_id": "i_87d562828dc0",
        "prescription": "3 x 10-10-8 reps, rest 30-60s",
        "result_text": "60kg x 12-10-10",
        "sets": [
          {
            "id": "s_87d562828dc0_1",
            "item_id": "i_87d562828dc0",
            "position": 1,
            "weight": 60,
            "unit": "kg",
            "reps": 12,
            "seconds": null,
            "rir": null,
            "is_warmup": false,
            "done": true,
            "done_at": "2026-08-24T12:00:00.000Z",
            "notes": null,
            "created_at": "2026-09-24T00:00:00.000Z",
            "updated_at": "2026-09-24T00:00:00.000Z"
          },
          {
            "id": "s_87d562828dc0_2",
            "item_id": "i_87d562828dc0",
            "position": 2,
            "weight": 60,
            "unit": "kg",
            "reps": 10,
            "seconds": null,
            "rir": null,
            "is_warmup": false,
            "done": true,
            "done_at": "2026-08-24T12:00:00.000Z",
            "notes": null,
            "created_at": "2026-09-24T00:00:00.000Z",
            "updated_at": "2026-09-24T00:00:00.000Z"
          },
          {
            "id": "s_87d562828dc0_3",
            "item_id": "i_87d562828dc0",
            "position": 3,
            "weight": 60,
            "unit": "kg",
            "reps": 10,
            "seconds": null,
            "rir": null,
            "is_warmup": false,
            "done": true,
            "done_at": "2026-08-24T12:00:00.000Z",
            "notes": null,
            "created_at": "2026-09-24T00:00:00.000Z",
            "updated_at": "2026-09-24T00:00:00.000Z"
          }
        ]
      },
      "suggestion": {
        "sets": [
          {
            "weight": 60,
            "unit": "kg",
            "reps": 12,
            "seconds": null
          },
          {
            "weight": 60,
            "unit": "kg",
            "reps": 10,
            "seconds": null
          },
          {
            "weight": 60,
            "unit": "kg",
            "reps": 8,
            "seconds": null
          }
        ],
        "reason": "Last done 4 weeks ago (60 kg × 12·10·10) — same weight to ease back in",
        "kind": "repeat"
      },
      "best": {
        "weight_kg": 60,
        "e1rm_kg": 84,
        "reps": 12
      }
    }
  ]
}
GET /api/workouts/{id}One workout with items and sets

Items carry exercise, last, suggestion and best unless prefill=0. The response (with or without prefill) is valid input for PUT.

Parameters

id string path

Workout id.

prefill "0" | "1"

0 leaves out exercise, last, suggestion and best on items: smaller and faster, the right choice before editing. Default 1.

today date

Your local day (YYYY-MM-DD). Drives the long-break check in suggestions, the stats windows and default dates. Default: the server's UTC date.

Responses

  • 200 WorkoutDetail The workout (this example: prefill=0, two of its nine items).
  • 404 Error Unknown id (not_found).
Example response (200)
json
{
  "id": "w_0f676309a14e",
  "program_id": "p_split_2026",
  "title": "Chest + Biceps + Core",
  "date": "2026-08-24",
  "status": "completed",
  "position": 0,
  "warmup": "2-3 Min Blood Flow on any machine @EZ pace\n\n2x\n10 Banded Pull-aparts\n5 Slow Prone T on Bench\n20s High Plank",
  "notes": null,
  "log_notes": null,
  "started_at": null,
  "completed_at": "2026-08-24T12:00:00.000Z",
  "source": "coachrx",
  "external_id": "workout-0f676309a14e",
  "created_at": "2026-09-24T00:00:00.000Z",
  "updated_at": "2026-09-24T00:00:00.000Z",
  "items": [
    {
      "target_seconds": null,
      "rir": null,
      "cue": null,
      "result_text": "60kg x 12-10-10",
      "notes": null,
      "external_id": "exercise-87d562828dc0",
      "id": "i_87d562828dc0",
      "workout_id": "w_0f676309a14e",
      "exercise_id": "incline-bench-press",
      "position": 1,
      "label": "A1",
      "name": "Incline Bench Press",
      "prescription": "3 x 10-10-8 reps, rest 30-60s",
      "target_sets": 3,
      "target_reps": [
        10,
        10,
        8
      ],
      "rep_min": 8,
      "rep_max": 10,
      "rest_seconds": 45,
      "status": "done",
      "created_at": "2026-09-24T00:00:00.000Z",
      "updated_at": "2026-09-24T00:00:00.000Z",
      "sets": [
        {
          "id": "s_87d562828dc0_1",
          "item_id": "i_87d562828dc0",
          "position": 1,
          "weight": 60,
          "unit": "kg",
          "reps": 12,
          "seconds": null,
          "rir": null,
          "is_warmup": false,
          "done": true,
          "done_at": "2026-08-24T12:00:00.000Z",
          "notes": null,
          "created_at": "2026-09-24T00:00:00.000Z",
          "updated_at": "2026-09-24T00:00:00.000Z"
        },
        {
          "id": "s_87d562828dc0_2",
          "item_id": "i_87d562828dc0",
          "position": 2,
          "weight": 60,
          "unit": "kg",
          "reps": 10,
          "seconds": null,
          "rir": null,
          "is_warmup": false,
          "done": true,
          "done_at": "2026-08-24T12:00:00.000Z",
          "notes": null,
          "created_at": "2026-09-24T00:00:00.000Z",
          "updated_at": "2026-09-24T00:00:00.000Z"
        },
        {
          "id": "s_87d562828dc0_3",
          "item_id": "i_87d562828dc0",
          "position": 3,
          "weight": 60,
          "unit": "kg",
          "reps": 10,
          "seconds": null,
          "rir": null,
          "is_warmup": false,
          "done": true,
          "done_at": "2026-08-24T12:00:00.000Z",
          "notes": null,
          "created_at": "2026-09-24T00:00:00.000Z",
          "updated_at": "2026-09-24T00:00:00.000Z"
        }
      ]
    },
    {
      "target_seconds": 40,
      "rir": null,
      "cue": null,
      "result_text": "Done",
      "notes": null,
      "external_id": "exercise-b79178d0c772",
      "id": "i_b79178d0c772",
      "workout_id": "w_0f676309a14e",
      "exercise_id": "hollow-body-hold",
      "position": 3,
      "label": "A3",
      "name": "Hollow Body Hold",
      "prescription": "3x 30-40s, rest as needed",
      "target_sets": 3,
      "target_reps": null,
      "rep_min": null,
      "rep_max": null,
      "rest_seconds": null,
      "status": "done",
      "created_at": "2026-09-24T00:00:00.000Z",
      "updated_at": "2026-09-24T00:00:00.000Z",
      "sets": []
    }
  ]
}
PUT /api/workouts/{id}Create or update a workout by id (upsert with merge rules)

Keyed by the URL id: creates the workout (201; title required) or updates it (200). Merge rules:

  • Workout fields you omit keep their values.
  • items, when present, is the complete list: items whose id belongs to this workout are updated; an item without an id updates a current item with the same label and exercise (else one with the same exercise), so re-sending the same plan keeps its item ids; other items are created; existing items left out are deleted with their sets. Omit items to keep them.
  • Updated items merge: omitted item fields keep their values. A new prescription is parsed again; target fields sent alongside override the parsed values, except ones that merely repeat the stored values (so editing prescription in a GET response works). On a GET response, changing exercise (while exercise_id still holds the old one) swaps the exercise and its name.
  • sets inside an item, when present, is that item's complete set list (same rules). Omit it to keep the sets. A set id used twice is a 400; one of another item is a 409.
  • Array order sets position unless you send position.
  • Logged work is protected: a PUT that would delete a done set, an item with done sets or a logged result, mark a done set not done, overwrite a set that changed since you read it (its updated_at differs), or point an item with done sets at another exercise (they would count for an exercise he never did) is refused with 409 would_lose_logged_sets, listing what would be lost. For an in-progress or completed workout, change single items and sets with PATCH instead, and add a new item for a new exercise; add force=1 only when he asked to replace them.
  • Items a PUT adds to an in-progress workout without sets get their suggested sets (done: false), like POST …/items.

The body id, if sent, must equal the URL id. An external_id owned by another workout is a 409.

Parameters

id string path

Workout id.

today date

Your local day (YYYY-MM-DD). Drives the long-break check in suggestions, the stats windows and default dates. Default: the server's UTC date.

force "1"

1 = replace even if logged sets or results are deleted or changed.

Body

WorkoutInput. Fields to set. Same shape as POST; a GET response also works.

json
{
  "title": "Lower Body",
  "notes": "Week 1/4. No overhead pressing this week (shoulder).",
  "items": [
    {
      "label": "A1",
      "exercise": "One and a Quarter Goblet Squat",
      "prescription": "3 x 12-10-8 reps, rest 30-60s\n*Go down in 3 sec*"
    },
    {
      "label": "A2",
      "exercise": "Prone Hamstring Curl Machine",
      "prescription": "3 x 12-10-8 reps, rest 30-60s"
    },
    {
      "label": "B1",
      "exercise": "Dumbbell Walking Lunge",
      "prescription": "3 x 16-14-12 alternating reps, rest 30-60s"
    },
    {
      "label": "B2",
      "exercise": "Leg Extension Machine",
      "prescription": "3 x 12-15 reps, rest 30-60s"
    },
    {
      "label": "C1",
      "exercise": "Dumbbell Hip Thrust",
      "prescription": "3 x 10-12 reps @2 sec pause at the top"
    },
    {
      "label": "C2",
      "exercise": "Leg Press Machine Calf Raises",
      "prescription": "3 x 15-20 reps, rest 30-60s\n*Pause 1 sec at the bottom*"
    }
  ]
}

Responses

  • 200 WorkoutDetail Updated.
  • 201 WorkoutDetail Created.
  • 400 Error Invalid input (bad_request).
  • 409 Error Already exists or belongs elsewhere (conflict, already_completed, in_progress, would_lose_logged_sets).
  • 422 Error A reference does not exist (unknown_exercise, unknown_program, invalid_reference).
PATCH /api/workouts/{id}Change workout fields (date, position, title, notes, status …)

Only workout fields; a body with items is rejected. date: null unschedules. Setting status keeps timestamps consistent (in_progress sets started_at; completed sets completed_at and a missing date; back to planned clears both) but does not touch sets or item statuses: use the start / complete / skip actions for real transitions.

Parameters

id string path

Workout id.

today date

Your local day (YYYY-MM-DD). Drives the long-break check in suggestions, the stats windows and default dates. Default: the server's UTC date.

Body

WorkoutPatch. Fields to change.

json
{
  "date": "2026-10-01"
}

Responses

  • 200 WorkoutDetail The workout with items, sets and prefill.
  • 400 Error Invalid input (bad_request).
  • 404 Error Unknown id (not_found).
  • 409 Error Already exists or belongs elsewhere (conflict, already_completed, in_progress, would_lose_logged_sets).
  • 422 Error A reference does not exist (unknown_exercise, unknown_program, invalid_reference).
DELETE /api/workouts/{id}Delete a workout with its items and sets

Permanent. Works on any status; don't delete history unless he asks. To drop a plan but keep a trace, skip it instead.

Parameters

id string path

Workout id.

Responses

Example response (200)
json
{
  "deleted": true,
  "id": "w_2026-10-05_chest"
}
POST /api/workouts/{id}/startStart: status in_progress, sets materialised from the suggestions

Sets status: in_progress, started_at = now and date = body date or today (an overdue plan moves to the day it is done). Every exercise item without sets (except text tracking) gets its suggested sets with done: false. Calling it again on an in-progress workout (or two calls at once) only fills items that still have no sets. The app calls this; agents rarely need it.

Parameters

id string path

Workout id.

today date

Your local day (YYYY-MM-DD). Drives the long-break check in suggestions, the stats windows and default dates. Default: the server's UTC date.

Body (optional)

StartInput. Optional.

json
{
  "date": "2026-09-28"
}

Responses

  • 200 WorkoutDetail The started workout with its new sets.
  • 400 Error Invalid input (bad_request).
  • 404 Error Unknown id (not_found).
  • 409 Error Already exists or belongs elsewhere (conflict, already_completed, in_progress, would_lose_logged_sets).
POST /api/workouts/{id}/completeComplete: status completed, unlogged sets dropped, items marked done/skipped

Deletes sets never marked done (unless discard_undone: false); marks each item done (it has a done set, a non-empty result_text, or was already done) or skipped; sets status: completed and completed_at; date = body date, else the workout's date if it was in progress or already completed, else today; replaces log_notes when sent (null or "" clears it). Works on a planned workout too. Repeating it keeps the original date and completion time.

Parameters

id string path

Workout id.

today date

Your local day (YYYY-MM-DD). Drives the long-break check in suggestions, the stats windows and default dates. Default: the server's UTC date.

Body (optional)

CompleteInput. Optional.

json
{
  "discard_undone": true,
  "log_notes": "Felt strong. Left shoulder fine."
}

Responses

POST /api/workouts/{id}/skipSkip a planned workout (restore with PATCH status planned)

For plans. Only the status changes; the workout keeps its items and date. Restore with PATCH {"status": "planned"}. The response has no prefill. A completed workout is a 409 already_completed and an in-progress one a 409 in_progress (skipping would drop logged work from his history); force=1 skips it anyway, e.g. a session abandoned days ago.

Parameters

id string path

Workout id.

force "1"

1 = skip even a completed or in-progress workout.

Responses

  • 200 WorkoutDetail The skipped workout (no prefill).
  • 404 Error Unknown id (not_found).
  • 409 Error Already exists or belongs elsewhere (conflict, already_completed, in_progress, would_lose_logged_sets).
POST /api/workouts/{id}/duplicateCopy a workout as a new plan (items and prescriptions, no sets)

Copies title, program, warm-up, notes and every item (exercise, label, prescription, targets, cue). Item notes are copied only from a planned workout (without the app's Swapped from … / Skipped: … lines): on a done session they are his log. Results, sets and log notes are not copied. The copy is planned, with source: api unless the body sets source.

Parameters

id string path

Workout to copy id.

today date

Your local day (YYYY-MM-DD). Drives the long-break check in suggestions, the stats windows and default dates. Default: the server's UTC date.

Body (optional)

DuplicateInput. Optional.

json
{
  "id": "w_2026-10-05_chest",
  "date": "2026-10-05"
}

Responses

  • 201 WorkoutDetail The copy.
  • 400 Error Invalid input (bad_request).
  • 404 Error Unknown id (not_found).
  • 409 Error Already exists or belongs elsewhere (conflict, already_completed, in_progress, would_lose_logged_sets).
POST /api/workouts/{id}/reorderReorder a workout's items

item_ids must list every item of the workout exactly once. Positions become 1…n in that order.

Parameters

id string path

Workout id.

today date

Your local day (YYYY-MM-DD). Drives the long-break check in suggestions, the stats windows and default dates. Default: the server's UTC date.

Body

ReorderInput. The new order.

json
{
  "item_ids": [
    "i_g1txsse06",
    "i_hshn07zli",
    "i_3fr4bg06e"
  ]
}

Responses

  • 200 WorkoutDetail The workout with items, sets and prefill.
  • 400 Error Invalid input (bad_request).
  • 404 Error Unknown id (not_found).

Items and sets

Edit one item (exercise in a workout) or one set.

POST /api/workouts/{id}/itemsAdd an item (exercise or free-form block) to a workout

Appended after the last item unless position is sent (other items are not shifted: reorder afterwards if needed). Needs exercise, exercise_id or name. When the workout is in progress and no sets are sent, suggested sets are created. Set ids that already exist are a 409.

Parameters

id string path

Workout id.

today date

Your local day (YYYY-MM-DD). Drives the long-break check in suggestions, the stats windows and default dates. Default: the server's UTC date.

Body

ItemInput. The item.

json
{
  "id": "i_2026-09-28_chest_d",
  "label": "D",
  "exercise": "Dumbbell Hammer Curl",
  "prescription": "2 x 15-20 reps, rest 30s\n*Squeeze at the top*"
}

Responses

  • 201 WorkoutItem The created item with prefill.
  • 400 Error Invalid input (bad_request).
  • 404 Error Unknown id (not_found).
  • 409 Error Already exists or belongs elsewhere (conflict, already_completed, in_progress, would_lose_logged_sets).
  • 422 Error A reference does not exist (unknown_exercise, unknown_program, invalid_reference).
PATCH /api/items/{id}Change an item (prescription, exercise, label, notes …)

Merges: omitted fields keep their values. A new prescription is parsed into targets and cue (fields sent alongside override); clearing it clears them. Changing exercise/exercise_id also renames the item to the exercise's name unless name is sent; on an item with done sets it is a 409 would_lose_logged_sets (they stay with the exercise he did: add a new item for the new one), unless force=1. sets, when sent, is the complete set list, merged like PUT: a set id used twice is a 400, and one that belongs to another item (e.g. copied from last.sets) is a 409, never moved or overwritten.

Parameters

id string path

Item id.

today date

Your local day (YYYY-MM-DD). Drives the long-break check in suggestions, the stats windows and default dates. Default: the server's UTC date.

force "1"

1 = change the exercise even though the item has done sets (they move with it).

Body

ItemInput. Fields to change (id is ignored).

json
{
  "prescription": "3 x 12-10-8 reps, rest 30-60s\n*Slow and controlled*"
}

Responses

  • 200 WorkoutItem The item with prefill.
  • 400 Error Invalid input (bad_request).
  • 404 Error Unknown id (not_found).
  • 409 Error Already exists or belongs elsewhere (conflict, already_completed, in_progress, would_lose_logged_sets).
  • 422 Error A reference does not exist (unknown_exercise, unknown_program, invalid_reference).
DELETE /api/items/{id}Delete an item and its sets

Parameters

id string path

Item id.

Responses

Example response (200)
json
{
  "deleted": true,
  "id": "i_2026-09-28_chest_d"
}
POST /api/items/{id}/setsAdd a set (defaults copied from the previous set)

Weight, unit, reps and seconds default to the item's last set; done and is_warmup default to false. Appended after the last set. An id that already exists is a 409.

Parameters

id string path

Item id.

Body (optional)

SetInput. Values that differ from the previous set.

json
{
  "reps": 8,
  "done": true
}

Responses

  • 201 WorkoutSet The created set.
  • 400 Error Invalid input (bad_request).
  • 404 Error Unknown id (not_found).
  • 409 Error Already exists or belongs elsewhere (conflict, already_completed, in_progress, would_lose_logged_sets).
PATCH /api/sets/{id}Change a set (e.g. log it: done true)

Parameters

id string path

Set id.

Body

SetInput. Fields to change.

json
{
  "weight": 62.5,
  "reps": 10,
  "done": true
}

Responses

Example response (200)
json
{
  "id": "s_87d562828dc0_1",
  "item_id": "i_87d562828dc0",
  "position": 1,
  "weight": 60,
  "unit": "kg",
  "reps": 12,
  "seconds": null,
  "rir": null,
  "is_warmup": false,
  "done": true,
  "done_at": "2026-08-24T12:00:00.000Z",
  "notes": null,
  "created_at": "2026-09-24T00:00:00.000Z",
  "updated_at": "2026-09-24T00:00:00.000Z"
}
DELETE /api/sets/{id}Delete a set

Parameters

id string path

Set id.

Responses

Exercises

The exercise library: history, records and suggestions.

GET /api/exercisesList exercises with usage stats (search before naming one in a plan)

Parameters

q string

Words to find in the name or an alias: every word must appear, ignoring case, spaces, hyphens, apostrophes and a plural s (pull up finds Strict Pull-Up, triceps finds Rope Tricep Pushdown). db, bb, kb, rdl and ohp are understood.

muscle_group MuscleGroup

Primary or secondary muscle group.

equipment Equipment

Equipment.

sort "recent" | "name" | "frequency"

recent (default: last performed first), name, or frequency (most performed first).

Responses

  • 200 object All matches (no paging).
  • 400 Error Invalid input (bad_request).
Example response (200)
json
{
  "exercises": [
    {
      "id": "incline-bench-press",
      "name": "Incline Bench Press",
      "aliases": [],
      "muscle_group": "chest",
      "secondary_muscles": [
        "shoulders",
        "triceps"
      ],
      "equipment": "barbell",
      "tracking": "weight_reps",
      "per_side": false,
      "default_unit": "kg",
      "weight_step": 2.5,
      "notes": null,
      "created_at": "2026-09-24T00:00:00.000Z",
      "updated_at": "2026-09-24T00:00:00.000Z",
      "times_performed": 8,
      "last_performed": "2026-08-24",
      "best_weight_kg": 60,
      "best_e1rm_kg": 84
    }
  ]
}
POST /api/exercisesCreate an exercise with explicit metadata

Items can also create exercises implicitly by name; create one explicitly to set the unit, step or tracking. 409 when the id or name exists, or when the name or an alias is already another exercise's name or alias.

Body

ExerciseInput. The exercise.

json
{
  "name": "Cable Crossover",
  "muscle_group": "chest",
  "secondary_muscles": [
    "shoulders"
  ],
  "equipment": "cable",
  "tracking": "weight_reps",
  "default_unit": "lb",
  "weight_step": 5
}

Responses

  • 201 Exercise The created exercise.
  • 400 Error Invalid input (bad_request).
  • 409 Error Already exists or belongs elsewhere (conflict, already_completed, in_progress, would_lose_logged_sets).
GET /api/exercises/{id}Exercise detail: full history, PRs and next suggestion

Parameters

id string path

Exercise id.

today date

Your local day (YYYY-MM-DD). Drives the long-break check in suggestions, the stats windows and default dates. Default: the server's UTC date.

Responses

  • 200 ExerciseDetail The exercise with its history (newest first), records and suggestion.
  • 404 Error Unknown id (not_found).
Example response (200)
json
{
  "id": "incline-bench-press",
  "name": "Incline Bench Press",
  "aliases": [],
  "muscle_group": "chest",
  "secondary_muscles": [
    "shoulders",
    "triceps"
  ],
  "equipment": "barbell",
  "tracking": "weight_reps",
  "per_side": false,
  "default_unit": "kg",
  "weight_step": 2.5,
  "notes": null,
  "created_at": "2026-09-24T00:00:00.000Z",
  "updated_at": "2026-09-24T00:00:00.000Z",
  "history": [
    {
      "workout_id": "w_0f676309a14e",
      "workout_title": "Chest + Biceps + Core",
      "date": "2026-08-24",
      "item_id": "i_87d562828dc0",
      "label": "A1",
      "prescription": "3 x 10-10-8 reps, rest 30-60s",
      "result_text": "60kg x 12-10-10",
      "sets": [
        {
          "id": "s_87d562828dc0_1",
          "item_id": "i_87d562828dc0",
          "position": 1,
          "weight": 60,
          "unit": "kg",
          "reps": 12,
          "seconds": null,
          "rir": null,
          "is_warmup": false,
          "done": true,
          "done_at": "2026-08-24T12:00:00.000Z",
          "notes": null,
          "created_at": "2026-09-24T00:00:00.000Z",
          "updated_at": "2026-09-24T00:00:00.000Z"
        },
        {
          "id": "s_87d562828dc0_2",
          "item_id": "i_87d562828dc0",
          "position": 2,
          "weight": 60,
          "unit": "kg",
          "reps": 10,
          "seconds": null,
          "rir": null,
          "is_warmup": false,
          "done": true,
          "done_at": "2026-08-24T12:00:00.000Z",
          "notes": null,
          "created_at": "2026-09-24T00:00:00.000Z",
          "updated_at": "2026-09-24T00:00:00.000Z"
        },
        {
          "id": "s_87d562828dc0_3",
          "item_id": "i_87d562828dc0",
          "position": 3,
          "weight": 60,
          "unit": "kg",
          "reps": 10,
          "seconds": null,
          "rir": null,
          "is_warmup": false,
          "done": true,
          "done_at": "2026-08-24T12:00:00.000Z",
          "notes": null,
          "created_at": "2026-09-24T00:00:00.000Z",
          "updated_at": "2026-09-24T00:00:00.000Z"
        }
      ],
      "top_weight_kg": 60,
      "best_e1rm_kg": 84,
      "volume_kg": 1920,
      "total_reps": 32,
      "status": "done",
      "notes": null,
      "cue": null,
      "target_sets": 3,
      "target_reps": [
        10,
        10,
        8
      ],
      "rep_min": 8,
      "rep_max": 10,
      "target_seconds": null,
      "rest_seconds": 45,
      "rir": null
    }
  ],
  "records": {
    "heaviest": {
      "weight": 60,
      "unit": "kg",
      "reps": 10,
      "date": "2026-07-21"
    },
    "best_e1rm": {
      "e1rm_kg": 84,
      "weight": 60,
      "unit": "kg",
      "reps": 12,
      "date": "2026-08-24"
    },
    "most_reps": {
      "reps": 12,
      "weight": 40,
      "unit": "kg",
      "date": "2026-04-29"
    },
    "best_volume": {
      "volume_kg": 1920,
      "date": "2026-08-24"
    }
  },
  "suggestion": {
    "sets": [
      {
        "weight": 60,
        "unit": "kg",
        "reps": 10,
        "seconds": null
      },
      {
        "weight": 60,
        "unit": "kg",
        "reps": 10,
        "seconds": null
      },
      {
        "weight": 60,
        "unit": "kg",
        "reps": 8,
        "seconds": null
      }
    ],
    "reason": "Last done 4 weeks ago (60 kg × 12·10·10) — same weight to ease back in",
    "kind": "repeat"
  }
}
PATCH /api/exercises/{id}Change an exercise (aliases, notes, unit, step …)

aliases replaces the whole list. Renaming keeps the old name as an alias (unless aliases is sent), because past items still carry it. Renaming to another exercise's name, or adding an alias that is another exercise's name or alias, is a 409 that names that exercise (merge duplicates instead).

Parameters

id string path

Exercise id.

Body

ExercisePatch. Fields to change.

json
{
  "aliases": [
    "Standing Cable Fly",
    "Cable Crossovers"
  ]
}

Responses

  • 200 Exercise The exercise.
  • 400 Error Invalid input (bad_request).
  • 404 Error Unknown id (not_found).
  • 409 Error Already exists or belongs elsewhere (conflict, already_completed, in_progress, would_lose_logged_sets).
DELETE /api/exercises/{id}Delete an exercise (items keep their name, lose the link)

Items that used it keep their name but get exercise_id: null, which cuts them out of history and suggestions. To fix a duplicate, merge instead.

Parameters

id string path

Exercise id.

Responses

POST /api/exercises/{id}/mergeMerge a duplicate into another exercise (history moves, name becomes an alias)

Every item of {id} moves to into; {id}'s name and aliases become aliases of into; {id} is deleted.

Parameters

id string path

Exercise to remove id.

Body

MergeInput. The exercise to keep.

json
{
  "into": "cable-crossover"
}

Responses

  • 200 Exercise The kept exercise.
  • 400 Error Invalid input (bad_request).
  • 404 Error Unknown id (not_found).
GET /api/exercises/{id}/suggestionWhat the app would prefill for a prescription you are considering

Pass the prescription you plan to write (prescription, parsed like an item's) and/or its targets; explicit targets override the parsed ones. Without any, last time's targets are used. Same rule as the prefill on workout items. Non-integer values are a 400.

Parameters

id string path

Exercise id.

prescription string

Coach-style text, parsed into the targets below.

target_sets integer

Number of sets.

target_reps string

Comma list of per-set reps; max for a max-reps set.

rep_min integer

Bottom of a rep range.

rep_max integer

Top of a rep range.

target_seconds integer

Hold time.

rir integer

Target reps in reserve: max-rep sets then repeat last time's reps instead of "beat it by one".

today date

Your local day (YYYY-MM-DD). Drives the long-break check in suggestions, the stats windows and default dates. Default: the server's UTC date.

Responses

  • 200 Suggestion The suggested sets and the reason.
  • 400 Error Invalid input (bad_request).
  • 404 Error Unknown id (not_found).
Example response (200)
json
{
  "sets": [
    {
      "weight": 15,
      "unit": "kg",
      "reps": 8,
      "seconds": null
    },
    {
      "weight": 15,
      "unit": "kg",
      "reps": 8,
      "seconds": null
    },
    {
      "weight": 15,
      "unit": "kg",
      "reps": 8,
      "seconds": null
    }
  ],
  "reason": "Hit 10 reps on every set on 24 Aug → +2.5 kg",
  "kind": "progress"
}

Programs

Optional groupings of workouts.

GET /api/programsList programs with workout counts

Active programs first, then newest first.

Responses

  • 200 object Every program.
Example response (200)
json
{
  "programs": [
    {
      "id": "p_oct_2026",
      "name": "Hypertrophy block Oct 2026",
      "description": "4 weeks, 3-day rotation: Chest + Biceps + Core / Back + Triceps + Core / Shoulders + Arms + Legs. Reps wave 12-10-8 → 15-12-10 → 12-10-8 → 10-10-8.",
      "status": "active",
      "created_at": "2026-09-24T09:45:17.344Z",
      "updated_at": "2026-09-24T09:45:17.344Z",
      "workout_count": 3,
      "completed_count": 0
    }
  ]
}
POST /api/programsCreate a program

Body

ProgramInput. The program.

json
{
  "id": "p_oct_2026",
  "name": "Hypertrophy block Oct 2026",
  "description": "4 weeks, 3-day rotation: Chest + Biceps + Core / Back + Triceps + Core / Shoulders + Arms + Legs. Reps wave 12-10-8 → 15-12-10 → 12-10-8 → 10-10-8."
}

Responses

  • 201 Program The created program.
  • 400 Error Invalid input (bad_request).
  • 409 Error Already exists or belongs elsewhere (conflict, already_completed, in_progress, would_lose_logged_sets).
GET /api/programs/{id}A program with its workouts (summaries)

Parameters

id string path

Program id.

Responses

PATCH /api/programs/{id}Change a program (name, description, status)

Parameters

id string path

Program id.

Body

ProgramPatch. Fields to change.

json
{
  "status": "archived"
}

Responses

  • 200 Program The program.
  • 400 Error Invalid input (bad_request).
  • 404 Error Unknown id (not_found).
DELETE /api/programs/{id}Delete a program (optionally with its planned workouts)

Its workouts stay, with program_id: null, unless delete_workouts=1, which also deletes its planned workouts (completed and skipped ones stay).

Parameters

id string path

Program id.

delete_workouts "1"

1 also deletes the program's planned workouts.

Responses

Data

Stats, export, import and the text parser.

GET /api/statsTotals, weekly volume, streak, muscle recency, recent PRs

Parameters

today date

Your local day (YYYY-MM-DD). Drives the long-break check in suggestions, the stats windows and default dates. Default: the server's UTC date.

Responses

  • 200 Stats Stats (this example: today=2026-08-27, lists shortened).
Example response (200)
json
{
  "total_workouts": 73,
  "total_sets": 1163,
  "total_volume_kg": 295661,
  "this_week": {
    "workouts": 3,
    "sets": 41,
    "volume_kg": 9401
  },
  "streak_weeks": 3,
  "weekly": [
    {
      "week_start": "2026-08-10",
      "workouts": 4,
      "sets": 65,
      "volume_kg": 14015
    },
    {
      "week_start": "2026-08-17",
      "workouts": 2,
      "sets": 39,
      "volume_kg": 8798
    },
    {
      "week_start": "2026-08-24",
      "workouts": 3,
      "sets": 41,
      "volume_kg": 9401
    }
  ],
  "muscle_recency": {
    "chest": {
      "last_date": "2026-08-24",
      "days_ago": 3,
      "sets_7d": 8
    },
    "back": {
      "last_date": "2026-08-25",
      "days_ago": 2,
      "sets_7d": 12
    },
    "biceps": {
      "last_date": "2026-08-26",
      "days_ago": 1,
      "sets_7d": 18
    }
  },
  "recent_prs": [
    {
      "exercise_id": "dumbbell-fly",
      "exercise_name": "Dumbbell Fly",
      "kind": "heaviest",
      "value": 12.5,
      "unit": "kg",
      "date": "2026-08-24"
    },
    {
      "exercise_id": "incline-bench-press",
      "exercise_name": "Incline Bench Press",
      "kind": "e1rm",
      "value": 84,
      "unit": "kg",
      "date": "2026-08-24"
    }
  ]
}
GET /api/exportEverything as one JSON document (backup)

Sent with Content-Disposition: attachment. The file can be sent back to POST /api/import.

Responses

  • 200 ExportData Every exercise, program and workout.
POST /api/importBulk upsert exercises, programs and workouts

Exercises upsert by id (default: slug of the name), programs by id, workouts like PUT (by id, else external_id, else created; items and sets are complete lists). Every row is validated first, so a malformed row (400, with its index: workouts[3].items[1].sets[0].reps …) writes nothing. Rows are then saved one at a time: a row that fails while saving (409, 422) stops the import with its index in the message, and earlier rows stay saved. Import restores data, so it may replace logged sets (no would_lose_logged_sets check). Re-importing an export into the same database changes nothing; into an empty one it restores the data, including started_at, completed_at and done_at.

Body

ImportInput. Any subset of the export shape.

json
{
  "workouts": [
    {
      "id": "w_2026-10-05_chest",
      "program_id": "p_oct_2026",
      "title": "Chest + Biceps + Core",
      "date": "2026-10-05",
      "notes": "Week 2/4.",
      "items": [
        {
          "label": "A1",
          "exercise": "Incline Bench Press",
          "prescription": "3 x 15-12-10 reps, rest 30-60s\n*Go down in 3 sec*"
        },
        {
          "label": "A2",
          "exercise": "Alternating Dumbbell Curl",
          "prescription": "3 x 15-12-10 reps, rest 30-60s"
        },
        {
          "label": "A3",
          "exercise": "Hollow Body Hold",
          "prescription": "3x 30-40s, rest as needed"
        }
      ]
    },
    {
      "id": "w_2026-10-07_back",
      "program_id": "p_oct_2026",
      "title": "Back + Triceps + Core",
      "date": "2026-10-07",
      "notes": "Week 2/4.",
      "items": [
        {
          "label": "A1",
          "exercise": "V-Bar Cable Lat Pulldown Machine",
          "prescription": "3 x 15-12-10 reps, rest 30-60s"
        },
        {
          "label": "A2",
          "exercise": "Rope Tricep Pushdown",
          "prescription": "3 x 15-12-10 reps, rest 30-60s"
        },
        {
          "label": "A3",
          "exercise": "Alternating V-Ups",
          "prescription": "3x 16-20 Alt Reps, rest as needed"
        }
      ]
    }
  ]
}

Responses

  • 200 ImportResult Rows processed per table.
  • 400 Error Invalid input (bad_request).
  • 409 Error Already exists or belongs elsewhere (conflict, already_completed, in_progress, would_lose_logged_sets).
  • 422 Error A reference does not exist (unknown_exercise, unknown_program, invalid_reference).
Example response (200)
json
{
  "imported": {
    "exercises": 0,
    "programs": 0,
    "workouts": 2
  }
}
POST /api/parsePreview how a prescription will be parsed (and how a result text reads as sets)

Nothing is saved. Workout writes parse prescription the same way; result_text is stored as text and never turned into sets (send sets). Each text is at most 5000 characters.

Body

ParseInput. Text to parse.

json
{
  "prescription": "3 x Max Reps (-2), rest 60-90s\n*Leave 2 reps in the tank*",
  "result": "25kg x 12 x 3"
}

Responses

  • 200 ParseOutput Parsed values; null for a field not sent.
  • 400 Error Invalid input (bad_request).
Example response (200)
json
{
  "prescription": {
    "target_sets": 3,
    "target_reps": [
      null,
      null,
      null
    ],
    "rep_min": null,
    "rep_max": null,
    "target_seconds": null,
    "rest_seconds": 75,
    "rir": 2,
    "cue": null,
    "per_side": false,
    "kind": "sets"
  },
  "result": {
    "status": "done",
    "sets": [
      {
        "weight": 25,
        "unit": "kg",
        "reps": 12,
        "seconds": null,
        "is_warmup": false
      },
      {
        "weight": 25,
        "unit": "kg",
        "reps": 12,
        "seconds": null,
        "is_warmup": false
      },
      {
        "weight": 25,
        "unit": "kg",
        "reps": 12,
        "seconds": null,
        "is_warmup": false
      }
    ],
    "note": null,
    "confidence": "high"
  }
}

Discovery

This documentation.

GET /apiAPI index: resources, endpoints, links to the docs

Responses

GET /api/openapi.jsonThis OpenAPI 3.1 document

Responses

  • 200 object The OpenAPI document.
GET /api/docsHuman-readable reference with recipes (HTML)

Responses

  • 200 string HTML page.
GET /llms.txtGuide for coding agents (markdown)

Responses

  • 200 string Markdown text.
GET /api/healthHealth check

Responses

Example response (200)
json
{
  "ok": true,
  "workouts": 76,
  "time": "2026-09-24T09:36:35.578Z"
}

Types

Response fields are always present (null when empty) unless marked otherwise. Input fields are optional unless marked required.

MuscleGroup

Muscle groups used by exercises, stats and the recovery map.

chest back shoulders biceps triceps forearms core quads hamstrings glutes calves adductors abductors full_body cardio

Equipment

barbell dumbbell ez_bar cable machine bodyweight kettlebell med_ball band other

Tracking

How sets are logged: weight_reps (weight × reps), reps (bodyweight reps), time (seconds, e.g. planks and holds), text (a free-text result, no sets).

weight_reps reps time text

Unit

Weights are stored as entered with their unit; nothing is converted on write.

kg lb

WorkoutStatus

plannedin_progress (start) → completed (complete); skipped when skipped.

planned in_progress completed skipped

ItemStatus

pending until the workout is completed; then done or skipped.

pending done skipped

Exercise

A canonical movement. Items link to it, so history, records and suggestions span every workout.

id string

Slug, e.g. incline-bench-press.

name string

Canonical name; unique, case-insensitive.

aliases string[]

Other names that resolve to this exercise when an item references it by name.

muscle_group MuscleGroup | null

Primary muscle group.

secondary_muscles MuscleGroup[]
equipment Equipment | null
tracking Tracking
per_side boolean

Reps and weight are per arm or leg.

default_unit Unit

Unit used when there is no history.

weight_step number | null

Progression increment in default_unit. null (or a session logged in the other unit) = 2.5 kg / 5 lb.

notes string | null

His note about the exercise (e.g. availability). Read it before programming.

created_at timestamp

When the row was created (UTC).

updated_at timestamp

When the row last changed (UTC).

ExerciseSummary

A row of GET /api/exercises: the exercise plus usage stats.

Every field of Exercise, plus:

times_performed integer

Completed workouts where he did it (the item is done or has a done set; skipped items don't count).

last_performed date | null

Date of the latest of those workouts.

best_weight_kg number | null

Heaviest done working set, in kg.

best_e1rm_kg number | null

Best estimated 1RM (Epley, sets of 1-12 reps), in kg.

ExerciseHistoryEntry

One completed session of an exercise.

workout_id string
workout_title string
date date | null
item_id string
label string | null
prescription string | null

The prescription at the time.

result_text string | null
sets WorkoutSet[]

Done sets only; empty for a text-only result like Done.

top_weight_kg number | null
best_e1rm_kg number | null
volume_kg number
total_reps integer
status ItemStatus

skipped when he skipped it that day (the reason is in notes).

notes string | null

His note on the item: skip reason, question, mistake.

cue string | null
target_sets integer | null
target_reps (integer | null)[] | null

Only for per-set schemes (12-10-8) or max-rep sets (nulls).

rep_min integer | null
rep_max integer | null
target_seconds integer | null
rest_seconds integer | null
rir integer | null

PersonalRecords

All-time bests over done working sets. Weights are as entered; comparisons use kg; the first date a record was set wins ties.

heaviest object | null
weight number
unit Unit
reps integer | null
date date | null
best_e1rm object | null
e1rm_kg number
weight number
unit Unit
reps integer
date date | null
most_reps object | null
reps integer
weight number | null
unit Unit
date date | null
best_volume object | null

Best single-session volume.

volume_kg number
date date | null

ExerciseDetail

GET /api/exercises/{id}.

Every field of Exercise, plus:

history ExerciseHistoryEntry[]

Every completed session, newest first.

suggestion Suggestion

Next-session prefill, assuming last time's prescription is repeated.

Program

An optional grouping of workouts, e.g. a 4-week block.

id string
name string
description string | null
status "active" | "archived"
created_at timestamp

When the row was created (UTC).

updated_at timestamp

When the row last changed (UTC).

ProgramListEntry

A row of GET /api/programs.

Every field of Program, plus:

workout_count integer
completed_count integer

ProgramDetail

GET /api/programs/{id}.

Every field of Program, plus:

workouts WorkoutSummary[]

Dated workouts by date, then unscheduled ones; ties by position.

WorkoutSet

One set of an item.

id string
item_id string
position integer

Order within the item, from 1.

weight number | null

As entered, in unit. null = bodyweight or not recorded.

unit Unit
reps integer | null
seconds integer | null

For time-tracked exercises.

rir number | null

Reps in reserve he reported.

is_warmup boolean

Warm-up sets are left out of volume, records and suggestions.

done boolean

Performed and logged. Only done sets count as history.

done_at timestamp | null
notes string | null
created_at timestamp

When the row was created (UTC).

updated_at timestamp

When the row last changed (UTC).

WorkoutItem

One exercise (or free-form block) in a workout.

id string
workout_id string
exercise_id string | null

Linked exercise; null for a free-form block (circuit, AMRAP, cardio).

position integer

Order in the workout, from 1.

label string | null

Superset label. Consecutive items sharing a letter form one group: A1, A2, A3 = tri-set A; a bare D is a single block.

name string

The exercise name, or the block title of a free-form block.

prescription string | null

The plan as the coach writes it, shown verbatim, e.g. 3 x 12-10-8 reps, rest 30-60s\n*Go down in 3 sec*.

target_sets integer | null
target_reps (integer | null)[] | null

Per-set rep targets, e.g. [12, 10, 8]; a null entry means max reps.

rep_min integer | null

Bottom of the rep range (for a scheme: its smallest entry).

rep_max integer | null

Top of the rep range (for a scheme: its largest entry).

target_seconds integer | null

Hold time per set: the top of a range (30-40s → 40).

rest_seconds integer | null

Rest between sets or rounds; a range becomes its midpoint rounded to 15 s (30-60s → 45).

rir integer | null

Target reps in reserve (Max reps (-2) → 2).

cue string | null

Technique cue, e.g. Go down in 3 sec.

status ItemStatus
result_text string | null

Free-text result: text blocks (Done, 5 rounds + 12 reps) and imported logs like 25kg x 12 x 3.

notes string | null

His notes, e.g. Skipped: Machine not available. Read them when planning.

external_id string | null
created_at timestamp

When the row was created (UTC).

updated_at timestamp

When the row last changed (UTC).

sets WorkoutSet[]

By position. Empty for a planned workout until it is started.

exercise Exercise | null

With prefill only.

last LastPerformance | null

With prefill only: the most recent completed session of this exercise.

suggestion Suggestion | null

With prefill only: the prefill for this session; null for free-form blocks.

best object | null

With prefill only: bests before this workout (live PR detection).

weight_kg number | null
e1rm_kg number | null
reps integer | null

Workout

A training session: a plan (planned), a session in progress, or history (completed).

id string
program_id string | null
title string

e.g. Chest + Biceps + Core.

date date | null

The planned day, or the day it was done. null = unscheduled.

position integer

Tie-breaker among planned workouts on the same date; lower first. May be negative.

warmup string | null

Warm-up routine text.

notes string | null

Plan notes from the coach or agent, shown before starting.

log_notes string | null

His notes on how it went. Read them when planning.

started_at timestamp | null
completed_at timestamp | null
source "app" | "api" | "coachrx"

Who created it: api (API writes), app, or coachrx (imported history).

external_id string | null
created_at timestamp

When the row was created (UTC).

updated_at timestamp

When the row last changed (UTC).

WorkoutDetail

A workout with its items and their sets.

Every field of Workout, plus:

items WorkoutItem[]

By position.

created_exercises object[]

Only in POST / PUT responses, and only when some item names matched no exercise: the exercises this request created. Check it: a near-miss spelling here means a split history.

id string
name string

WorkoutSummary

A list row: the workout without items, plus counts.

Every field of Workout, plus:

item_count integer
set_count integer

Working (non-warm-up) sets.

done_set_count integer
volume_kg number

Σ weight × reps over done working sets, in kg.

muscle_groups MuscleGroup[]

Primary groups of the items, most frequent first.

exercise_names string[]

Item names in order.

WorkoutList

GET /api/workouts.

workouts WorkoutSummary[]
total integer

Matches before limit/offset.

limit integer
offset integer

LastPerformance

The most recent completed session of an exercise before this workout.

workout_id string
workout_title string
date date | null
item_id string
prescription string | null

The prescription at the time.

result_text string | null
sets WorkoutSet[]

Done sets only; empty for a text-only result.

SuggestedSet

weight number | null
unit Unit
reps integer | null
seconds integer | null

Suggestion

Prefill for the next session of an exercise (see the suggestion rule).

reason string

Why, e.g. Hit 10 reps on every set on 24 Aug → +2.5 kg, or for per-set targets Hit every rep target (12·10·10) on 24 Aug → +2.5 kg.

kind "progress" | "repeat" | "deload" | "new"

progress = heavier, or more reps at the same weight; repeat = same as last time (also: history logged only as text); deload = lighter, because the rep target is well above what he did at that weight; new = no history.

NextResponse

workout WorkoutDetail | null

The in-progress workout, else the first planned one, with prefill. null when nothing is planned.

upcoming WorkoutSummary[]

The other planned workouts in queue order (at most 10).

Stats

GET /api/stats. Counts cover completed workouts and done working sets.

total_workouts integer

Completed workouts with a date.

total_sets integer

Done working sets.

total_volume_kg integer

Rounded to 1 kg.

this_week object

The ISO week (Monday start) that contains today.

workouts integer
sets integer
volume_kg integer
streak_weeks integer

Consecutive weeks with a completed workout, ending this week (or last week when this week has none yet).

weekly object[]

The last 12 weeks, oldest first.

week_start date

Monday.

workouts integer
sets integer
volume_kg integer
muscle_recency map of object

One entry per MuscleGroup. last_date counts primary and secondary muscles (a squat trains the glutes too); sets_7d counts sets for the primary muscle only.

recent_prs object[]

Records set in the 30 days before today, newest first, at most 10: a session's heaviest set or best e1RM that beats every earlier workout (a first session is not a record; an e1RM record is listed only when the session did not also set a heaviest record).

exercise_id string
exercise_name string
kind "heaviest" | "e1rm"
value number

The weight as entered (heaviest) or the e1RM in kg.

unit Unit
date date

ExportData

GET /api/export: every table.

version integer
exported_at timestamp
exercises Exercise[]
programs Program[]
workouts WorkoutDetail[]

Every workout with items and sets (no prefill fields), by date.

Error

Every 4xx/5xx response.

error object
code string

Machine-readable code (see Errors).

message string

What went wrong, e.g. items[2] needs exercise_id, exercise or name.

details any

Optional extra data.

Deleted

deleted boolean
id string

Health

ok boolean
workouts integer

Workouts stored.

time timestamp

Server time.

ApiIndex

GET /api: resources, endpoints and links to the docs.

SetInput

A set to create or update. Omitted fields keep their current value (or the default for a new set).

id string

Client-generated id (optional), matching ^[A-Za-z0-9_-]{1,64}$. Default: s_ + random.

position integer

Order within the item. Default: array order (or after the last set).

weight number | null

In unit, as entered. null = bodyweight or not recorded.

unit Unit

Default kg for a new set.

reps integer | null
seconds integer | null

For time-tracked exercises.

rir number | null

Reps in reserve he reported.

is_warmup boolean

Default false.

done boolean

Default false, except for a new set on a workout saved as completed (a session recorded afterwards), where it defaults to true. true stamps done_at; false clears it.

done_at timestamp | null

When it was logged. Default: now when done turns true. Only kept while done is true (import passes it through).

notes string | null
updated_at timestamp

Optional precondition (PUT): the set's updated_at as you read it. If the set changed since, the PUT is refused with 409 would_lose_logged_sets (unless force=1). A GET response carries it, so a GET → edit → PUT can't overwrite a set he logged in between.

ItemInput

An item to create or update. Reference the exercise with exercise (name) or exercise_id; send only name for a free-form block. Omitted fields keep their current value.

id string

Client-generated id (optional), matching ^[A-Za-z0-9_-]{1,64}$. Default: i_ + random. On PUT, the id of an existing item of this workout updates that item; an item without an id takes the id of a current item with the same label and exercise, if any.

exercise_id string | null

Existing exercise id. Wins over exercise (except on an edited GET response: when exercise_id is the item's current exercise and exercise names another one, the name is the change). Unknown id: 422 unknown_exercise.

exercise string

Exercise name, matched against every exercise's name, then its aliases, then loosely (case, spaces, hyphens, apostrophes and a plural s ignored: Strict Pull Up = Strict Pull-Up), then by slug = exercise id. No match creates the exercise (listed in the response's created_exercises), with muscle group, equipment, tracking and per-side inferred from the words in the name.

position integer

Default: array order in a workout body, or after the last item for POST …/items.

label string | null

Superset label: A1, A2, A3, B1 … or a bare letter for a single block.

name string

Display name. Default: the exercise's canonical name. Required for a free-form block (no exercise). A new item with only name that is exactly an exercise's name or alias links to that exercise; send exercise_id: null to keep it free-form.

prescription string | null

Coach-style plan text, shown verbatim. Targets and cue are parsed from it; target fields sent alongside override the parsed values, except ones that only repeat the item's stored values when the text changed (an edited GET response). Clearing it (null or "") clears the targets and cue parsed from it.

target_sets integer | null

Parsed from prescription unless sent.

target_reps (integer | null)[] | null

Per-set targets, e.g. [12, 10, 8]; null = max reps.

rep_min integer | null

Bottom of the rep range.

rep_max integer | null

Top of the rep range.

target_seconds integer | null

Hold time per set.

rest_seconds integer | null

Rest between sets or rounds.

rir integer | null

Target reps in reserve.

cue string | null

Technique cue. Parsed from the prescription's extra lines unless sent.

status ItemStatus

Default pending.

result_text string | null

Free-text result (text blocks, AMRAP scores).

notes string | null
external_id string | null

Optional key from another system.

sets SetInput[]

The item's complete set list: listed sets (by id) are updated, sets without an id are created, sets left out are deleted. Omit to keep the current sets. A set id used twice in one body is a 400; one that belongs to another item is a 409. Normally left out of plans: sets are created from the suggestion when the workout starts (plan sets are only for a load you prescribe on purpose).

WorkoutInput

A workout with optional nested items and sets. Omitted fields keep their current value (PUT) or take the default (create).

id string

Client-generated id (optional), matching ^[A-Za-z0-9_-]{1,64}$. Default: w_ + random.

program_id string | null

Existing program id (else 422 unknown_program).

title string

Required when creating, e.g. Chest + Biceps + Core.

date date | null

YYYY-MM-DD. null = unscheduled (queued after every dated plan).

status WorkoutStatus

Default planned. Prefer the start / complete / skip actions for transitions.

position integer

Tie-breaker among planned workouts on the same date; lower first. Default 0.

warmup string | null

Warm-up routine text.

notes string | null

Plan notes shown before he starts (e.g. Week 1/4. Heavier than last week.).

log_notes string | null

His notes on how it went. Normally written by the app.

source "app" | "api" | "coachrx"

Default api.

external_id string | null

Optional unique key from another system. Create rejects a duplicate; import matches on it when the id is unknown.

started_at timestamp | null

Default: set by the status (in_progress → now). Send it only to restore history (import).

completed_at timestamp | null

Default: set by the status (completed → now). Send it only to restore history (import).

items ItemInput[]

The complete item list: listed items (by id) are updated; an item without an id updates a current item with the same label and exercise (so re-sending a plan keeps its item ids), else it is created; items left out are deleted with their sets. Omit to keep the current items. On PUT, a change that would delete or undo logged work is a 409 unless force=1.

WorkoutCreate

Body of POST /api/workouts: a WorkoutInput with a title.

Every field of WorkoutInput, plus:

Required: title.

WorkoutPatch

Workout fields only; items is rejected (use PUT or the item endpoints).

program_id string | null

Existing program id (else 422 unknown_program).

title string

Required when creating, e.g. Chest + Biceps + Core.

date date | null

YYYY-MM-DD. null = unscheduled (queued after every dated plan).

status WorkoutStatus

Default planned. Prefer the start / complete / skip actions for transitions.

position integer

Tie-breaker among planned workouts on the same date; lower first. Default 0.

warmup string | null

Warm-up routine text.

notes string | null

Plan notes shown before he starts (e.g. Week 1/4. Heavier than last week.).

log_notes string | null

His notes on how it went. Normally written by the app.

source "app" | "api" | "coachrx"

Default api.

external_id string | null

Optional unique key from another system. Create rejects a duplicate; import matches on it when the id is unknown.

started_at timestamp | null

Default: set by the status (in_progress → now). Send it only to restore history (import).

completed_at timestamp | null

Default: set by the status (completed → now). Send it only to restore history (import).

ExerciseInput

A new exercise.

id string

Client-generated id (optional), matching ^[A-Za-z0-9_-]{1,64}$. Default: slug of the name.

name string required

Unique, case-insensitive, and not another exercise's alias (409). Renaming keeps the old name as an alias (unless aliases is sent), so items that still use it link here.

aliases string[]

Other names that resolve to this exercise when an item references it by name. Trimmed and de-duplicated; at most 50. A name or alias of another exercise is a 409.

muscle_group MuscleGroup | null
secondary_muscles MuscleGroup[]
equipment Equipment | null
tracking Tracking

Default weight_reps.

per_side boolean

Default false.

default_unit Unit

Default kg.

weight_step number | null

Progression increment in default_unit. null = 2.5 kg / 5 lb.

notes string | null

ExercisePatch

Exercise fields to change.

name string

Unique, case-insensitive, and not another exercise's alias (409). Renaming keeps the old name as an alias (unless aliases is sent), so items that still use it link here.

aliases string[]

Other names that resolve to this exercise when an item references it by name. Trimmed and de-duplicated; at most 50. A name or alias of another exercise is a 409.

muscle_group MuscleGroup | null
secondary_muscles MuscleGroup[]
equipment Equipment | null
tracking Tracking

Default weight_reps.

per_side boolean

Default false.

default_unit Unit

Default kg.

weight_step number | null

Progression increment in default_unit. null = 2.5 kg / 5 lb.

notes string | null

ProgramInput

A new program.

id string

Client-generated id (optional), matching ^[A-Za-z0-9_-]{1,64}$. Default: p_ + random.

name string required
description string | null
status "active" | "archived"

Default active.

ProgramPatch

Program fields to change.

name string
description string | null
status "active" | "archived"

Default active.

StartInput

date date

The day it is done. Default: today.

sets map of SetInput[]

Sets to create per item id instead of the suggestion (the app sends these after an offline start). Only applies to items without sets; items not listed get the suggestion. A set id used twice is a 400.

items map of object

Optional, per item id in sets: what the phone knew about the item. When that id no longer exists (the plan was rewritten after the phone cached it), its sets go to the item for the same exercise that has no sets, or the item is re-created, so offline logging is not lost.

CompleteInput

discard_undone boolean

Default true: delete sets never marked done. false keeps them (still not done). A JSON boolean (a string is a 400).

log_notes string | null

How it went. Replaces log_notes when sent; null or "" clears it.

date date

The day it was done. Default: the workout's date if it was in progress or already completed, else today.

DuplicateInput

id string

Client-generated id (optional), matching ^[A-Za-z0-9_-]{1,64}$. Default: w_ + random.

title string

Default: the source title.

date date | null

Default: null (unscheduled).

source "app" | "api" | "coachrx"

Default api. The app sends app for copies made on the phone.

ReorderInput

item_ids string[] required

Every item id of the workout exactly once, in the new order.

MergeInput

into string required

Id of the exercise to keep.

ImportInput

Any subset of the export shape. Processed in order (exercises, programs, workouts), one row at a time: a failing row stops the import, earlier rows stay saved.

exercises ExerciseInput[]

Upserted by id (default: slug of the name).

programs ProgramInput[]

Upserted by id (no id: a new program each time).

workouts WorkoutInput[]

Upserted like PUT: by id, else by external_id, else created. Export rows are accepted as they are.

ImportResult

imported object
exercises integer
programs integer
workouts integer

ParseInput

prescription string

Prescription text to parse.

result string

A free-text result to parse into sets, e.g. 25kg x 12 x 3.

ParsedPrescription

What the API stores when this text is sent as prescription.

target_sets integer | null
target_reps (integer | null)[] | null

Only for per-set schemes (12-10-8) or max-rep sets (nulls).

rep_min integer | null
rep_max integer | null
target_seconds integer | null
rest_seconds integer | null
rir integer | null
cue string | null
per_side boolean

/side, per leg, each arm

kind "sets" | "freeform"

freeform for AMRAP / EMOM / circuits: no set or rep targets.

ParsedSet

weight number | null
unit Unit
reps integer | null
seconds integer | null
is_warmup boolean

ParsedResult

status "done" | "skipped" | "empty"
sets ParsedSet[]
note string | null

Text that isn't set data.

confidence "high" | "low"

low when the parser had to guess.

ParseOutput

prescription ParsedPrescription | null
result ParsedResult | null