# Plans

A plan is a multi-week program: weeks contain days, and each non-rest day points at a workout.

---

**List plans**

```text
GET /api/plans/client
```

| Parameter | Type | Default | Matches |
|-----------|------|---------|---------|
| `search` | string | none | Partial, case-insensitive match on the English title |
| `level` | int | none | Exact match on the plan's numeric level. A non-integer returns `400` |
| `body_parts` | string[] | none | Body part name, case-insensitive |
| `translation_languages` | string[] | none | Only plans translated into one of these language codes |
| `include_weeks` | bool | `false` | `true` expands each plan with its weeks, days, and per-day workouts |
| `include_shared_library` | bool | `true` | `false` returns only your company's own plans |
| `lang` | string | `en` | Language of the returned text |
| `limit` | int | `10` | Page size, clamped to 100 |
| `offset` | int | `0` | Rows to skip |

Read the `level` field off an unfiltered page to see which levels your library actually uses. There is no `categories` filter here; each plan instead reports its own `category_levels` scores. Personal (AI-generated) plans are always excluded from this list.

```bash
curl "https://data.kinestex.com/api/plans/client?level=2&body_parts=Full%20Body&limit=2" \
  -H "x-api-key: YOUR_API_KEY" \
  -H "Language: en"
```

```json
{
  "data": [
    {
      "id": 45,
      "img_url": "https://firebasestorage.googleapis.com/.../cover.png?alt=media",
      "level": 2,
      "created_at": "2026-01-14T10:00:00Z",
      "updated_at": "2026-02-02T08:30:00Z",
      "translation": {
        "title": "Four Week Reset",
        "description": "A four-week full-body progression."
      },
      "translation_languages": ["en", "es"],
      "weeks_count": 4,
      "workout_count": 20,
      "body_parts": ["Full Body"],
      "category_levels": [
        { "name": "Strength", "score": 7 },
        { "name": "Cardio/Endurance", "score": 4 }
      ],
      "created_by": { "id": 1 }
    }
  ],
  "pagination": {
    "total": 6,
    "limit": 2,
    "offset": 0,
    "total_pages": 3,
    "current_page": 1,
    "has_next": true,
    "has_prev": false
  }
}
```

> **The plan list is keyed differently.** Plans arrive under `data`, not `plans`. Exercises use `exercises` and workouts use `workouts`.

**Plan fields (list view)**

| Field | Type | Description |
|-------|------|-------------|
| `id` | number | Plan identifier |
| `img_url` | string | Cover image |
| `level` | number | Numeric level, the value `?level=` matches against |
| `translation` | object | Localized `title` and `description`, falls back to English |
| `translation_languages` | string[] | Language codes loaded for this plan |
| `weeks_count` | number | Number of weeks |
| `workout_count` | number | Number of workout days across the plan |
| `body_parts` | string[] | Targeted body parts |
| `category_levels` | object[] | `{ "name", "score" }` per training category |
| `created_by` | object | `{ "id": <company id> }` |
| `created_at` / `updated_at` | string | ISO 8601 timestamps |
| `weeks` | array | Present only with `include_weeks=true` |

With `include_weeks=true`, each plan gains a `weeks` array of `{ week_number, title, description, days[] }`, and each day is `{ day_number, title, is_rest, is_done, workout }`. `is_done` is always `false` in list view (no progression lookup is performed).

> Same caveat as exercises: `translation_languages` on this endpoint reports the language you asked for plus English. Use `translation_languages=<code>` as a filter to test coverage accurately.

> **Known quirk:** inside `include_weeks=true`, `days[].workout.total_minutes` currently carries the workout's duration in **seconds**, not minutes. Divide by 60, or read `total_time` and `total_minutes` from `GET /api/workouts/client/{id}`, which are correct.

---

**Get one plan**

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

`{id}` accepts a **numeric ID** (`45`), a legacy Firestore ID, or a **title** (`Four Week Reset`). Titles are normalized before matching. URL-encode spaces as `%20`.

This endpoint takes **no** query parameters. In particular it ignores `?lang=`: set the `Language` or `Accept-Language` header instead. Weeks, days, and the current workout are always included.

```bash
curl "https://data.kinestex.com/api/plans/client/45" \
  -H "x-api-key: YOUR_API_KEY" \
  -H "Language: en"
```

```json
{
  "id": "45",
  "img_url": "https://firebasestorage.googleapis.com/.../cover.png?alt=media",
  "level": "Strength",
  "is_active": true,
  "created_at": "2026-01-14T10:00:00Z",
  "updated_at": "2026-02-02T08:30:00Z",
  "title": "Four Week Reset",
  "description": "A four-week full-body progression.",
  "body_parts": ["Full Body"],
  "category_levels": [
    { "name": "Strength", "score": 7 },
    { "name": "Cardio/Endurance", "score": 4 }
  ],
  "currentWorkout": {
    "id": 88,
    "category": "Strength",
    "calories": 250,
    "dif_level": "Easy",
    "total_time": 1800,
    "total_minutes": 30,
    "translation": { "title": "Upper Body Starter", "language": "en", "description": "A short push-focused session." },
    "workout_sequences": [...]
  },
  "weeks": [
    {
      "id": 30,
      "week_number": 1,
      "title": "Foundation Week",
      "description": "Build your base strength.",
      "intensity": 3,
      "rest_multiplier": 1,
      "isActive": true,
      "isComplete": false,
      "translation": { "id": 100, "language": "en", "title": "Foundation Week", "description": "Build your base strength." },
      "days": [
        {
          "id": 100,
          "day_number": 1,
          "is_rest": false,
          "title": "Day 1",
          "isCompleted": false,
          "isActive": true,
          "translation": { "id": 200, "language": "en", "title": "Day 1" },
          "workout": {
            "id": "88",
            "img_url": "https://firebasestorage.googleapis.com/.../desc.png?alt=media",
            "title": "Upper Body Starter",
            "description": "A short push-focused session.",
            "calories": 250,
            "total_minutes": 30
          }
        },
        {
          "id": 101,
          "day_number": 2,
          "is_rest": true,
          "title": "Rest Day",
          "isCompleted": false,
          "isActive": false,
          "translation": { "id": 201, "language": "en", "title": "Rest Day" },
          "workout": null
        }
      ]
    }
  ],
  "created_by": { "id": 1 }
}
```

**Plan fields (detail view)**

| Field | Type | Description |
|-------|------|-------------|
| `id` | string | Plan identifier, a **string** here and a number in the list |
| `level` | string | The name of the highest-scoring category (`"Strength"`, `"Cardio"`, ...), or the numeric level as a string when no category matches. Not the same shape as the list's numeric `level` |
| `is_active` | bool | Catalog visibility of the plan itself, unrelated to user progress |
| `title` / `description` | string | Localized, at the root rather than under `translation` |
| `currentWorkout` | object or null | The next workout to play, as a **full workout object** with its sequence, identical in shape to `GET /api/workouts/client/{id}` |
| `body_parts` | string[] | Targeted body parts |
| `category_levels` | object[] | `{ "name", "score" }` per training category |
| `weeks` | object[] | Weeks with nested days, see below |

**Week object**

| Field | Type | Description |
|-------|------|-------------|
| `week_number` | number | 1-indexed position |
| `title` / `description` | string | Localized, also mirrored under `translation` |
| `intensity` | number or null | Relative intensity of the week |
| `rest_multiplier` | number or null | Rest-period multiplier applied to this week |
| `isActive` | bool | This is the week the user is on |
| `isComplete` | bool | Every workout day in the week is completed |
| `days` | object[] | Days in ascending `day_number` |

**Day object**

| Field | Type | Description |
|-------|------|-------------|
| `day_number` | number | 1-indexed position within the week |
| `is_rest` | bool | Rest day, `workout` is `null` |
| `title` | string | Localized day title |
| `isCompleted` | bool | The user has completed this day |
| `isActive` | bool | This is the day the user is on |
| `workout` | object or null | Summary: `id`, `title`, `description`, `img_url`, `calories`, `total_minutes` |

> **Progress flags depend on who is asking.** With a user JWT, or an API key plus `x-user-id`, `isActive` / `isComplete` / `isCompleted` reflect that user's real progression. With a bare API key there is no user, so the plan reads as not started: nothing is completed and the first day of week 1 is reported as active. Treat the flags as presentation state, not as a source of truth for your own analytics.

**Not found** returns `404`:

```json
{
  "error": "Plan not found",
  "message": "No plan found matching '4-Week Strength Plan'"
}
```

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