# Exercises

An exercise is a single movement: demo video, difficulty, target body parts, coaching tips, and the AI model that scores the user's form.

---

**List exercises**

```text
GET /api/exercises/client
```

| Parameter | Type | Default | Matches |
|-----------|------|---------|---------|
| `search` | string | none | Partial, case-insensitive match on the English title |
| `body_parts` | string[] | none | Body part name, case-insensitive |
| `categories` | string[] | none | Category name, case-insensitive. Deleted categories never match |
| `difficulty_level` | string[] | none | `easy`, `medium`, `hard`. Alias: `dif_level`. If both are sent, `difficulty_level` wins |
| `translation_languages` | string[] | none | Only exercises that have a translation in one of these language codes |
| `remove_inactive` | bool | `true` | `false` also returns deactivated exercises |
| `include_shared_library` | bool | `true` | `false` returns only your company's own exercises |
| `lang` | string | `en` | Language of the returned text |
| `limit` | int | `10` | Page size, clamped to 100 |
| `offset` | int | `0` | Rows to skip |

```bash
curl "https://data.kinestex.com/api/exercises/client?body_parts=Chest,Triceps&categories=Strength&difficulty_level=easy,medium&lang=es&limit=2" \
  -H "x-api-key: YOUR_API_KEY"
```

```json
{
  "exercises": [
    {
      "id": 412,
      "title": "Flexiones",
      "description": "Ejercicio de empuje para pecho y triceps.",
      "difficulty_level": "easy",
      "position": "lying",
      "calories_per_rep": 0.32,
      "is_active": true,
      "body_parts": [{ "name": "Chest" }, { "name": "Triceps" }],
      "categories": ["Strength", "Muscle Gain"],
      "contraindications": [],
      "equipment": [
        {
          "id": 3,
          "title": "Esterilla",
          "description": "Esterilla de ejercicio estandar.",
          "thumbnail_url": "https://firebasestorage.googleapis.com/.../mat.png?alt=media"
        }
      ],
      "translation_languages": ["es", "en"],
      "created_at": "2026-01-14T10:00:00Z",
      "thumbnail_url": "https://firebasestorage.googleapis.com/.../thumb.png?alt=media",
      "video_url": "https://firebasestorage.googleapis.com/.../video.mp4?alt=media",
      "male_thumbnail_url": null,
      "male_video_url": null,
      "rest_speech": "descansa-treinta-segundos-abc123",
      "rest_speech_text": "Descansa 30 segundos",
      "repeats": 12,
      "countdown": 3,
      "correct_second": 1.5,
      "tips": ["Manten el core activo."],
      "steps": ["Colocate en posicion de plancha.", "Baja el pecho hacia el suelo."],
      "common_mistakes": "Dejar caer la cadera.",
      "created_by": { "id": 1 }
    }
  ],
  "pagination": {
    "total": 42,
    "limit": 2,
    "offset": 0,
    "total_pages": 21,
    "current_page": 1,
    "has_next": true,
    "has_prev": false
  }
}
```

**Exercise fields (list view)**

| Field | Type | Description |
|-------|------|-------------|
| `id` | number | Exercise identifier |
| `title` / `description` | string | Localized text, falls back to English |
| `difficulty_level` | string | `easy`, `medium`, or `hard` |
| `position` | string | `standing`, `sitting`, or `lying`. Returned, not filterable |
| `calories_per_rep` | number | Estimated calories per repetition |
| `is_active` | bool or null | Null is treated as active (legacy rows) |
| `body_parts` | object[] | Array of `{ "name": "Chest" }` objects |
| `categories` | string[] | Flat array of category names |
| `contraindications` | string[] | Body areas where caution is needed |
| `equipment` | object[] | `id`, `title`, `description`, `thumbnail_url`, localized |
| `translation_languages` | string[] | Language codes available for this exercise |
| `thumbnail_url` / `video_url` | string | Default (female) demo assets |
| `male_thumbnail_url` / `male_video_url` | string or null | Male demo assets where produced |
| `repeats` | number or null | Default repetition count |
| `countdown` | number or null | Timer length in seconds, for timer-based exercises |
| `correct_second` | number or null | Seconds one correct repetition should take |
| `tips` | string[] | Coaching tips, localized |
| `steps` | string[] | Step-by-step instructions, localized |
| `common_mistakes` | string | Omitted when empty |
| `rest_speech` / `rest_speech_text` | string | Rest-period audio identifier and its text |
| `created_by` | object | `{ "id": <company id> }` |
| `created_at` | string | ISO 8601 timestamp |

> An exercise matching several of your requested categories or body parts appears **once**, and `pagination.total` counts distinct exercises, so paging never repeats or skips a row.

> **Known quirk:** on this endpoint `translation_languages` reports only the language you asked for plus English, because only those translation rows are loaded. To test coverage for another language, filter with `translation_languages=<code>` instead: that filter queries the full translation table and is accurate. The workout list reports the complete set.

---

**Get one exercise**

```text
GET /api/exercises/client/{id}
```

`{id}` accepts a **numeric ID** (`412`) or a **title** (`Push Ups`). Titles are normalized before matching (lowercased, non-alphanumeric characters removed), so `Push Ups`, `push-ups`, and `pushups` all resolve to the same exercise. URL-encode spaces as `%20`.

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `lang` | string | `en` | Language code, overrides the headers |
| `include_audio` | bool | `true` | `false` omits the generated audio URLs and returns a lighter payload |

```bash
curl "https://data.kinestex.com/api/exercises/client/Push%20Ups?lang=es" \
  -H "x-api-key: YOUR_API_KEY"
```

```json
{
  "id": "412",
  "ai_model": { "id": "57" },
  "repeats": 12,
  "countdown": 3,
  "thumbnail_url": "https://firebasestorage.googleapis.com/.../thumb.png?alt=media",
  "video_url": "https://firebasestorage.googleapis.com/.../video.mp4?alt=media",
  "male_thumbnail_url": null,
  "male_video_url": null,
  "difficulty_level": "easy",
  "position": "lying",
  "calories_per_rep": 0.32,
  "body_parts": ["Chest", "Triceps"],
  "categories": ["Strength", "Muscle Gain"],
  "contraindications": ["shoulder"],
  "equipment": [],
  "is_active": true,
  "correct_second": 1.5,
  "non_motivational": false,
  "translation": {
    "id": 50,
    "language": "es",
    "title": "Flexiones",
    "description": "Ejercicio de empuje para pecho y triceps.",
    "tips": ["Manten el core activo."],
    "exercise_steps": ["Colocate en posicion de plancha.", "Baja el pecho hacia el suelo."],
    "common_mistakes": "Dejar caer la cadera.",
    "rest_speech": "descansa-treinta-segundos-abc123",
    "rest_speech_text": "Descansa 30 segundos",
    "rest_speech_url_m4a": "https://firebasestorage.googleapis.com/.../rest.m4a?alt=media",
    "rest_speech_url_webm": "https://firebasestorage.googleapis.com/.../rest.webm?alt=media",
    "voice_actor": "Glinda"
  },
  "created_by": { "id": 1 }
}
```

Differences from the list view, worth handling explicitly:

- `id` is a **string** here and a number in the list.
- `body_parts` is a flat `string[]` here and an array of `{name}` objects in the list.
- Localized text lives under `translation` instead of at the root, and it carries the audio URLs, `voice_actor`, and `exercise_steps` (the list calls the same data `steps`).
- `ai_model` is reduced to `{ "id": ... }`. Pass that ID to the SDK when you build a fully custom workout UI.
- Deactivated exercises are still fetchable by ID or title. The `remove_inactive` filter applies to the list only.

**Not found** returns `404`:

```json
{
  "message": "Exercise not found",
  "details": "exercise with normalized title 'unknown' not found in company or library exercises"
}
```

---
Source: https://www.kinestex.com/docs/content-api-v2/content-api-v2-exercises · Index: https://www.kinestex.com/llms.txt
