# Workouts

A workout is an ordered sequence of exercises and rest periods, with its own difficulty, calorie estimate, and duration.

---

**List workouts**

```text
GET /api/workouts/client
```

| Parameter | Type | Default | Matches |
|-----------|------|---------|---------|
| `search` | string | none | Partial, case-insensitive match on the English title |
| `category` | string | none | **Single value.** Whole-string, case-insensitive match |
| `dif_level` | string | none | **Single value.** Whole-string, case-insensitive match |
| `body_parts` | string[] | none | Body part name, case-insensitive |
| `translation_languages` | string[] | none | Only workouts translated into one of these language codes |
| `include_exercises` | bool | `false` | `true` expands every workout with its full exercise sequence |
| `include_shared_library` | bool | `true` | `false` returns only your company's own workouts |
| `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/workouts/client?category=Strength&dif_level=easy&body_parts=Chest,Triceps&limit=2" \
  -H "x-api-key: YOUR_API_KEY" \
  -H "Language: en"
```

```json
{
  "workouts": [
    {
      "id": 88,
      "category": "Strength",
      "calories": 250,
      "type": "Upper Body",
      "body_img_url": "https://firebasestorage.googleapis.com/.../body.png?alt=media",
      "dif_level": "Easy",
      "desc_img_url": "https://firebasestorage.googleapis.com/.../desc.png?alt=media",
      "total_time": 1800,
      "body_parts": ["Chest", "Triceps"],
      "translation": {
        "id": 201,
        "workout_id": 88,
        "language": "en",
        "title": "Upper Body Starter",
        "description": "A short push-focused session."
      },
      "translation_languages": ["en", "es", "de"],
      "created_by": { "id": 1 },
      "created_at": "2026-01-14T10:00:00Z",
      "updated_at": "2026-02-02T08:30:00Z"
    }
  ],
  "pagination": {
    "total": 12,
    "limit": 2,
    "offset": 0,
    "total_pages": 6,
    "current_page": 1,
    "has_next": true,
    "has_prev": false
  }
}
```

**Workout fields (list view)**

| Field | Type | Description |
|-------|------|-------------|
| `id` | number | Workout identifier |
| `category` | string | Free-text category set per workout |
| `calories` | number | Estimated calories burned |
| `type` | string or null | Optional content type label |
| `dif_level` | string | Difficulty as stored, usually capitalized |
| `body_img_url` | string or null | Body-map image |
| `desc_img_url` | string or null | Cover / preview image |
| `total_time` | number | Duration in **seconds** |
| `body_parts` | string[] | Targeted body parts |
| `translation` | object or null | Localized `title` and `description` for the requested language |
| `translation_languages` | string[] | Every language code this workout has |
| `workout_sequences` | array | Present only with `include_exercises=true` |
| `created_by` | object | `{ "id": <company id> }` |
| `created_at` / `updated_at` | string | ISO 8601 timestamps |

> **The workout title is not a root-level field.** Always read `translation.title`.

> `translation` is `null` when the requested language has no row for that workout, because this endpoint does not fall back to English. `translation_languages` always lists what does exist, so use it to decide whether to re-request in another language. Passing `include_exercises=true` switches to the detail builder, which *does* fall back to English.

`include_exercises=true` adds a `workout_sequences` array to every item with full exercise and AI-model detail. It is a much larger payload: use it for a workout player, not for a browsing list.

---

**Get one workout**

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

`{id}` accepts a **numeric ID** (`88`), a legacy 20-character Firestore ID, or a **title** (`Upper Body Starter`). Titles are normalized before matching, so spacing, hyphens, and casing do not matter. URL-encode spaces as `%20`.

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

This endpoint **always** returns the full exercise sequence. There is no `include_exercises` toggle on it.

```bash
curl "https://data.kinestex.com/api/workouts/client/88?include_audio=false" \
  -H "x-api-key: YOUR_API_KEY"
```

```json
{
  "id": 88,
  "created_at": "2026-01-14T10:00:00Z",
  "updated_at": "2026-02-02T08:30:00Z",
  "category": "Strength",
  "calories": 250,
  "type": "Upper Body",
  "dif_level": "Easy",
  "desc_img_url": "https://firebasestorage.googleapis.com/.../desc.png?alt=media",
  "total_time": 1800,
  "total_minutes": 30,
  "body_parts": ["Chest", "Triceps"],
  "translation": {
    "id": 201,
    "language": "en",
    "title": "Upper Body Starter",
    "description": "A short push-focused session.",
    "dif_level": "Easy"
  },
  "auto_calculate": false,
  "turn_off_time_reminder": false,
  "turn_off_sound_reminder": false,
  "created_by": { "id": 1 },
  "workout_sequences": [
    {
      "id": 100,
      "workout_id": 88,
      "exercise_id": 0,
      "order": 1,
      "countdown": 10,
      "repeats": null,
      "video_url": null,
      "thumbnail_url": null,
      "is_rest_sequence": true,
      "created_at": "2026-01-14T10:00:00Z",
      "updated_at": "2026-01-14T10:00:00Z"
    },
    {
      "id": 101,
      "workout_id": 88,
      "exercise_id": 412,
      "order": 2,
      "countdown": null,
      "repeats": 12,
      "video_url": null,
      "thumbnail_url": null,
      "is_rest_sequence": false,
      "created_at": "2026-01-14T10:00:00Z",
      "updated_at": "2026-01-14T10:00:00Z",
      "exercise": {
        "id": "412",
        "ai_model": { "id": "57" },
        "repeats": 12,
        "countdown": null,
        "thumbnail_url": "https://firebasestorage.googleapis.com/.../thumb.png?alt=media",
        "video_url": "https://firebasestorage.googleapis.com/.../video.mp4?alt=media",
        "difficulty_level": "easy",
        "position": "lying",
        "calories_per_rep": 0.32,
        "body_parts": ["Chest", "Triceps"],
        "categories": ["Strength"],
        "is_active": true,
        "correct_second": 1.5,
        "translation": {
          "title": "Push Ups",
          "language": "en",
          "description": "A pushing movement for chest and triceps."
        },
        "created_by": { "id": 1 }
      }
    }
  ]
}
```

**Detail-only fields**

| Field | Type | Description |
|-------|------|-------------|
| `total_minutes` | number | `total_time` divided by 60, for display |
| `auto_calculate` | bool | Calories are recomputed from the exercise list |
| `turn_off_time_reminder` | bool | Time reminders disabled for this workout |
| `turn_off_sound_reminder` | bool | Sound reminders disabled for this workout |
| `workout_sequences` | array | Ordered entries, see below |

**Workout sequence entry**

| Field | Type | Description |
|-------|------|-------------|
| `order` | number | Position in the workout, ascending |
| `is_rest_sequence` | bool | `true` for a rest block; `exercise` is then absent |
| `countdown` | number or null | Timer length in seconds, for timed entries and rests |
| `repeats` | number or null | Repetition target, for rep-based entries |
| `video_url` / `thumbnail_url` | string or null | Per-sequence overrides of the exercise assets |
| `exercise` | object | Full exercise detail, same shape as `GET /api/exercises/client/{id}` |
| `exercise_id` | number | `0` on rest entries |
| `translation` | object | Rest-period speech text and audio, when the sequence has one |

**Not found** returns `404 { "error": "Workout not found" }`.

> A workout deactivated in the KinesteX library disappears from the list but stays fetchable by ID, so plans that already reference it keep working.

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