Filtering & Parameters
These rules hold on all three list endpoints, so you only learn them once.
Which filter each catalog accepts
| Filter | Exercises | Workouts | Plans |
search | Yes | Yes | Yes |
body_parts | Yes | Yes | Yes |
categories | Yes | No | No |
category | No | Yes (single value) | No |
difficulty_level (alias dif_level) | Yes | No | No |
dif_level | Yes (alias) | Yes (single value) | No |
level | No | No | Yes (integer) |
translation_languages | Yes | Yes | Yes |
remove_inactive | Yes | No | No |
include_exercises | No | Yes | No |
include_weeks | No | No | Yes |
include_shared_library | Yes | Yes | Yes |
lang | Yes | Yes | Yes |
limit / offset | Yes | Yes | Yes |
Array parameters: three interchangeable forms
1?body_parts=Chest,Triceps // comma-separated
2?body_parts=Chest&body_parts=Triceps // repeated
3?body_parts[]=Chest&body_parts[]=Triceps // bracketedSurrounding whitespace is trimmed, so ?body_parts=Chest, Triceps works. Multi-word values stay intact: ?body_parts=Lower Back,Full Body is two body parts, not four (URL-encode the space as %20).
body_parts, categories, difficulty_level, and translation_languages are arrays. category and dif_level on the workout list are single values: a comma in them is matched literally, so ?dif_level=easy,medium looks for a difficulty named "easy,medium" and returns an empty page. Issue one request per value and merge client-side.
Combining filters
- Within one filter, values are OR.
categories=Strength,Cardio/Endurancereturns exercises in either category. - Across filters it is AND.
body_parts=Chest&difficulty_level=easyreturns easy chest exercises. - There is no "match all" mode.
body_parts=Chest,Tricepsmeans chest or triceps, never both. - Every name filter is case-insensitive.
easy,Easy, andEASYare the same request.
Values that match nothing
Name filters compare on string equality, so an unrecognized value is not an error. You get 200 with an empty array and pagination.total: 0. The one exception is ?level= on the plan list, which must be an integer and returns 400 otherwise.
?body_parts= or ?categories= applies no filter and returns the full unfiltered page. If your UI builds query strings from optional fields, omit the parameter entirely rather than sending it blank.Booleans must be literal
include_shared_library, remove_inactive, include_exercises, include_weeks, and include_audio accept true / false (also 1 / 0, t / f). Anything else, such as yes or on, is read as false without an error. include_shared_library=yes silently hides the entire shared library.
Search is matched against English titles
search is a case-insensitive partial match on the English title, whatever lang you send. ?search=flexiones&lang=es returns nothing; search for push and read the Spanish titles off the result.
Result ordering
Newest first by creation date. When search is present, results are ranked by match quality first (exact title, then prefix, then partial) and creation date second.
Allowed values
Body parts (18), shared by all three catalogs:
1Neck Shoulders Chest External Oblique
2Abs Biceps Triceps Forearms
3Traps Lats Lower Back Glutes
4Quads Hamstrings Abductors Adductors
5Calves Full BodyExercise categories (8):
1Strength Muscle Gain Weight Loss Cardio/Endurance
2General Fitness Wellness/Flexibility Warm Up CooldownExercise difficulty (3): easy, medium, hard. Returned lowercase on exercises, accepted in any casing everywhere.
Workout category is free text set per workout rather than a fixed list (values in use include Fitness, Cardio, Strength, HIIT). Workout dif_level is stored capitalized (Easy, Medium, Hard) and returned as stored, so compare case-insensitively on your side. Read an unfiltered first page to build a picker rather than hard-coding either list.
The same filters work on the JWT routes. /api/exercises, /api/workouts, and /api/plans accept every parameter listed here and return the same data plus internal fields. The one difference: they spell the library toggle include_kinestex_library instead of include_shared_library.