# AI Trainer API

KinesteX's conversational AI trainer can generate personalized workouts, review workout progression, and provide tailored recommendations — all through natural conversation. Beyond the [plug-and-play AI Trainer Chat view](/docs/ai-trainer-chat), the same intelligence is available as a REST API so you can build your own experience on top of it.

**Three integration paths:**

| Path | What it is | Best for |
|------|-----------|----------|
| [Plug-and-Play SDK](/docs/ai-trainer-chat) | Fully white-labeled chat UI embedded via the client SDK | Ship fast — configure, skin, and launch with no backend work |
| [Trainer Chat API](/docs/trainer-api/trainer-api-chat) | REST endpoint that handles session management and workout creation through our agentic pipeline | Custom chat UIs — you own the UX, we handle the intelligence |
| [Semantic Exercise Search](/docs/trainer-api/trainer-api-search) | Vector-indexed exercise search API | Teams building their own AI agents on top of the exercise library |

**Base URL:** `https://data.kinestex.com`

All trainer endpoints authenticate end-users with a JWT Bearer token — see [Authentication](/docs/trainer-api/trainer-api-auth). To get API access, [contact KinesteX](/#contact-form).

## Authentication

Trainer and search endpoints require a **JWT Bearer token** for a company user (an end-user of your app):

```
Authorization: Bearer <jwt_token>
```

**Obtaining a JWT token** — call the verify-api-key endpoint **from your backend** (never embed your company API key in client code):

```
POST https://data.kinestex.com/api/companies/me/verify-api-key/
```

| Header / Field | Required | Description |
|----------------|----------|-------------|
| `x-api-key` header | Yes | Your company's API key |
| `user_id` (JSON body) | Yes | A unique identifier for the end-user in your system (e.g. UUID or database ID) |

```bash
curl -X POST "https://data.kinestex.com/api/companies/me/verify-api-key/" \
  -H "x-api-key: your_api_key_here" \
  -H "Content-Type: application/json" \
  -d '{"user_id": "user-abc-123"}'
```

The response includes a `token` field — use it as the Bearer token in all subsequent API requests. The `user_id` you provide should match the `user_id` used across other KinesteX endpoints and SDK launches so user data stays consistent.

**Session tokens (recommended for client launches):** instead of shipping a raw API key to the client, your backend can mint a short-lived session for SDK launches — see [Session Auth & Managed Subscriptions](/docs/trainer-api/trainer-api-subscriptions).

**Language:** the trainer replies in the user's language. Set either header on any trainer request (defaults to English):

```
Language: es              # custom header, takes precedence
Accept-Language: es-MX    # standard header, used as fallback
```

Locale tags are normalized to their base code (`es-MX` → `es`).

## Trainer Chat Endpoint

The unified conversational endpoint. Your users talk to the trainer in natural language — *"give me a 30-minute dumbbell chest workout"*, *"make it harder"*, *"let's start"* — and the API classifies each message's intent, generates or modifies a workout plan, answers fitness questions, and manages the user's fitness profile.

```
POST https://data.kinestex.com/api/trainer/chat
```

Integration is minimal by design:
- **One required field per message** (`message`). Everything else is optional.
- **The fitness profile is stored server-side.** Send it once and never again.
- **Workout history is stored server-side.** The AI plans around recently worked muscle groups automatically.
- **Workout preferences are extracted from the message.** *"45 minutes, dumbbells, chest and triceps"* needs no separate preferences object.

**Request fields:**

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `message` | string | **Yes** | The user's natural-language message |
| `session_id` | string | No | Chat session ID. Omit to use the user's default session (created automatically). Pass the value from a previous response to target a specific session. |
| `stage` | string | No | Client-declared intent that skips AI classification when you already know it: `"CREATE_WORKOUT"` (first message of a session) or `"RECOMMEND_NEXT"` (first message after a completed workout). Anything else falls back to the classifier. |
| `profile_data` | object | No | The user's fitness profile (see below). Only needed once — it is saved server-side and reused. Sending it again updates the stored profile. |
| `workout_details` | object | No | Explicit workout preferences (see below). If omitted on a create, preferences are extracted from the message itself. |
| `readiness` | object | No | Free-form JSON describing the user's current condition (sleep, soreness, energy…), passed to the AI as context. |

Advanced AI controls (`model`, `enable_thinking`, `thinking_effort`: `"minimal"` / `"low"` / `"medium"` / `"high"`) are also accepted — available model identifiers are provided by your KinesteX contact.

**Profile object** (persists server-side per user, always bound to the authenticated user):

| Field | Type | Values / Unit |
|-------|------|---------------|
| `age` | int | years |
| `weight` | float | kg |
| `height` | float | cm |
| `gender` | string | `"MALE"`, `"FEMALE"`, `"OTHER"` |
| `fitness_goals` | string[] | `"strength"`, `"muscle_gain"`, `"weight_loss"`, `"cardio_endurance"`, `"general_fitness"`, `"wellness_flexibility"` |
| `fitness_level_pushups` | string | e.g. `"0-5"`, `"6-15"`, `"16+"` |
| `fitness_level_cardio` | string | e.g. `"1 mile"`, `"2 mile"`, `"3+ mile"` |
| `fitness_level_squats` | string | e.g. `"0-10"`, `"10-21"`, `"22+"` |
| `injuries` | object[] | `{ "body_part", "preference": "avoid" \| "include", "severity": "severe" \| "moderate" \| "light" }` — `"avoid"` hard-filters exercises targeting that body part; `"include"` keeps them but picks light/rehabilitative variants |
| `health_conditions` | string[] | free text, e.g. `["Hypertension"]` |
| `other_preferences` | string | free text, passed verbatim to the AI |
| `preferred_duration_minutes` | int | default workout length (also learned from explicit duration requests) |
| `training_intensity` | int | 1–10, intensity ceiling for the plan |
| `structured_program` | bool | `true` = has followed a structured weight-training program (heavier low-rep sets) |

Removing an injury restriction via chat (e.g. *"my knee is fine now"*) triggers an are-you-sure confirmation before the profile actually changes.

**Workout details object:**

| Field | Type | Description |
|-------|------|-------------|
| `equipment` | string[] | e.g. `["dumbbells", "kettlebells"]` or `["bodyweight"]` |
| `duration` | int | Minutes. If omitted: explicit in message → previous workout in this conversation → profile preference → 30-minute default |
| `body_parts` | string[] | e.g. `["Chest", "Shoulders", "Triceps"]` |
| `include_warmup` / `include_cooldown` | bool | Prepend warmup / append cooldown exercises |
| `post_workout_feedback` | object | Check-in from the just-completed workout, sent with `stage: "RECOMMEND_NEXT"` — `rpe` (1–10, primary progression signal), `discomfort` (`"no_pain"` / `"mild"` / `"sharp"`), and when sharp: `pain_severity` (1–10), `pain_stopped`, `pain_body_parts` |

**Intents** — every message is classified into one of these (returned in the response so your UI can react). There are no magic strings — *"confirm"*, *"let's go"*, and *"start the workout"* all classify as `START_WORKOUT`:

| Intent | Triggered by | Effect |
|--------|--------------|--------|
| `CREATE_WORKOUT` | "make me a workout…" | Semantic search + AI plan generation |
| `MODIFY_WORKOUT` | "make it harder", "remove the squats" | AI edits the current plan |
| `START_WORKOUT` | "let's start", "confirm", "looks good" | Returns the final plan with `workout_plan.action = "CONFIRMED"` |
| `UNDO` / `REDO` | "undo" / "redo" | Instant plan history navigation (no AI call) |
| `ASK_QUESTION` | "what's a superset?" | Conversational answer |
| `DISCUSS_RESULTS` | "how did I do yesterday?" | Discusses the user's saved workout results |
| `RECOMMEND_NEXT` | first message after a completed workout | Recovery-aware next workout using stored history + feedback |
| `UPDATE_PROFILE` | "I weigh 78kg now", "my knee hurts" | Updates the stored profile |
| `OFF_TOPIC` | anything non-fitness | Polite redirect |

**Response — 200 OK:**

```json
{
  "message": "Here's your 30-minute upper-body dumbbell workout! ...",
  "intent": "CREATE_WORKOUT",
  "action": "WORKOUT_UPDATED",
  "session_id": "9c1e37a2-4b7f-4f6e-9a2d-1f2e3d4c5b6a",
  "workout_plan": {
    "step": "PLAN_REFINEMENT",
    "action": null,
    "data": {
      "turn_action": "WORKOUT_UPDATED",
      "can_undo": false,
      "can_redo": false,
      "estimated_duration_seconds": 1820,
      "sequences": [ ... ],
      "exercises": [ ... ]
    }
  }
}
```

| Field | Description |
|-------|-------------|
| `message` | The trainer's reply — render this in your chat UI |
| `intent` | The classified intent (table above) |
| `action` | What this turn actually **did** — `WORKOUT_UPDATED` (plan content changed), `NO_CHANGE`, `QUESTION` (trainer awaits a reply), `PROFILE_UPDATED` (refresh cached profile data), `INFO` (plain reply) |
| `session_id` | The persistent **chat session ID** — store it and send it on subsequent requests |
| `workout_plan` | Present only on turns that involve a plan. `workout_plan.action` is `"CONFIRMED"` once the user starts/confirms the workout. |

> ⚠️ **Two different IDs:** the top-level `session_id` is the persistent chat session — the one you store and send back. The response may also carry a nested `workout_plan.session_id`; that is internal planning state and you never need to send it.

`workout_plan.data.sequences` is the ordered plan: each item is an `"exercise"`, `"warmup"`, `"cooldown"` (with `exercise_id`, `repeats`) or a `"rest"` (with `rest_countdown` seconds). `workout_plan.data.exercises` holds the full exercise objects referenced by the sequences, including media URLs and per-language `translations` — pick the entry matching your requested language, falling back to `"en"`.

**Error responses:**

| Status | Cause |
|--------|-------|
| `400` | Missing `message`, invalid JSON, unsupported advanced option |
| `401` | Missing or invalid JWT |
| `403` | `{ "error": "Subscription required to generate workouts", "code": "not_subscribed" }` — only for generation intents when your company manages subscriptions (see [Session Auth & Managed Subscriptions](/docs/trainer-api/trainer-api-subscriptions)) |
| `404` | `session_id` doesn't exist or belongs to another user |
| `429` | Rate limit exceeded (see [Sessions, Profile & Limits](/docs/trainer-api/trainer-api-sessions)) |
| `500` | AI call or session storage failure — `{ "error": "Failed to process trainer chat", "details": "..." }` |

**Example — create, refine, confirm:**

```bash
# 1. First message (profile inline, preferences in the message)
curl -X POST "https://data.kinestex.com/api/trainer/chat" \
  -H "Authorization: Bearer <jwt>" -H "Content-Type: application/json" \
  -d '{
    "message": "Create a 30-minute dumbbell workout for chest and triceps",
    "stage": "CREATE_WORKOUT",
    "profile_data": { "age": 28, "weight": 75.0, "height": 180.0, "gender": "MALE", "fitness_goals": ["strength"] }
  }'

# 2. Refine (session_id from the previous response)
curl -X POST "https://data.kinestex.com/api/trainer/chat" \
  -H "Authorization: Bearer <jwt>" -H "Content-Type: application/json" \
  -d '{ "session_id": "<session_id>", "message": "make it harder and add more core work" }'

# 3. Confirm — natural language, no magic string required
curl -X POST "https://data.kinestex.com/api/trainer/chat" \
  -H "Authorization: Bearer <jwt>" -H "Content-Type: application/json" \
  -d '{ "session_id": "<session_id>", "message": "looks good, let'\''s start" }'

# 4. Next workout after completing one (recovery-aware, with post-workout feedback)
curl -X POST "https://data.kinestex.com/api/trainer/chat" \
  -H "Authorization: Bearer <jwt>" -H "Content-Type: application/json" \
  -d '{
    "session_id": "<session_id>",
    "stage": "RECOMMEND_NEXT",
    "message": "what should I do next?",
    "workout_details": {
      "post_workout_feedback": { "rpe": 8, "discomfort": "no_pain" }
    }
  }'
```

See the [full TypeScript lifecycle example](/docs/guides/guide-trainer-rest-lifecycle) in Guides & Examples.

## Sessions, Profile & Limits

Chat sessions are persistent, named, durable records — messages survive restarts and can be reloaded any time. A session belongs to the authenticated user; other users cannot read, modify, or delete it.

**Endpoints:**

| Method | Path | Description |
|--------|------|-------------|
| `GET` | `/api/trainer/history` | Load a conversation's message history |
| `POST` | `/api/trainer/sessions` | Create a named chat session (`{ "title": "Leg day planning" }`, max 255 chars) |
| `GET` | `/api/trainer/sessions` | List the user's sessions, most recently active first |
| `PATCH` | `/api/trainer/sessions/:id` | Rename a session |
| `DELETE` | `/api/trainer/sessions/:id` | Delete a session (also clears its in-progress planning state) |
| `GET` | `/api/trainer/profile` | Get the stored fitness profile (`404` if none yet) |
| `PUT` | `/api/trainer/profile` | Create or update the fitness profile |

**History** (`GET /api/trainer/history?session_id=…&limit=50`, limit capped at 100) — use it to restore chat UI state on app startup. Assistant messages carry `metadata.intent`, `metadata.action`, and — when the turn produced a plan — the full `metadata.workout_plan`, so you can re-render plans from history alone. If the user has no sessions yet the response is `{ "messages": [] }`; it never creates a session.

```json
{
  "conversation_id": "9c1e37a2-…",
  "title": "New Chat",
  "messages": [
    { "role": "user", "content": "make me a chest workout", "created_at": "2026-07-14T10:00:00Z" },
    {
      "role": "assistant",
      "content": "Here's your chest workout! …",
      "metadata": { "intent": "CREATE_WORKOUT", "action": "WORKOUT_UPDATED", "workout_plan": { } },
      "created_at": "2026-07-14T10:00:05Z"
    }
  ]
}
```

**Rate limits:**

| Limit | Scope | On exceed |
|-------|-------|-----------|
| 50 workout generations / day | per user (resets at UTC midnight) | `429` |
| 50 refinements / workout | per workout plan | `429` — confirm the current plan or start a new workout |
| 50 chat sessions | per user | `400` on session create |
| Company-level daily quota | per company | `429` |

Undo/redo do **not** count against the refinement limit. Higher limits are available — [contact KinesteX](/#contact-form).

## Semantic Exercise Search

**Semantic (vector) search** over the exercises accessible to your company. Unlike keyword search, it understands the *meaning* of your query — searching for `"exercises for bad knees"` returns relevant low-impact exercises even if they don't contain those exact words.

The AI Trainer runs this search internally when building workouts — you don't need to call it yourself for that. Use it directly when building your **own** agent, or a search/browse experience on top of the exercise library.

```
GET https://data.kinestex.com/api/exercises/search
```

Requires the same JWT Bearer token as the trainer endpoints (see [Authentication](/docs/trainer-api/trainer-api-auth)).

**Query parameters:**

| Parameter | Type | Required | Default | Description |
|-----------|------|----------|---------|-------------|
| `query` | string | **Yes** | — | Natural-language search query, 1–500 characters |
| `limit` | integer | No | `20` | Max exercises to return (capped at 100) |
| `lang` | string | No | `"en"` | Language code for returned translations (e.g. `"es"`, `"de"`, `"fr"`) |
| `basic_info` | boolean | No | `false` | When `true`, returns a lightweight response with only ID, title, description, and media URLs |
| `categories` | string | No | — | Comma-separated category filter, e.g. `categories=Strength,Cardio` |

**Lightweight response** (`basic_info=true`) — ideal as LLM context:

```json
{
  "exercises": [
    {
      "id": "123",
      "title": "Squat",
      "description": "A fundamental lower-body exercise targeting the quads, glutes, and hamstrings.",
      "thumbnail_url": "https://cdn.kinestex.com/exercises/squat-thumbnail.jpg",
      "video_url": "https://cdn.kinestex.com/exercises/squat.mp4",
      "male_thumbnail_url": "https://cdn.kinestex.com/exercises/squat-male-thumbnail.jpg",
      "male_video_url": "https://cdn.kinestex.com/exercises/squat-male.mp4"
    }
  ],
  "count": 1
}
```

The **full response** (default) additionally includes per exercise: `difficulty_level`, `position`, `calories_per_rep`, `body_parts`, `categories`, `contraindications`, `equipment` (empty array = bodyweight), `repeats`, `countdown`, and a `translation` object for the requested `lang` with `title`, `description`, `tips`, `exercise_steps`, `common_mistakes`, and rest-speech audio URLs.

**Rate limiting:** 50 semantic searches per user per day (resets at UTC midnight) → `429` when exceeded. A company-level daily read quota also applies.

**Errors:** `400` (missing/too-long `query`, invalid `limit`), `401` (missing/invalid JWT), `429`, `500`.

**Examples:**

```bash
# Basic search
curl "https://data.kinestex.com/api/exercises/search?query=core+exercises+for+beginners&limit=10" \
  -H "Authorization: Bearer <jwt>"

# Localized, lightweight, category-filtered
curl "https://data.kinestex.com/api/exercises/search?query=knee+friendly+exercises&lang=es&basic_info=true&categories=Strength,Cardio&limit=5" \
  -H "Authorization: Bearer <jwt>"
```

See [Build a workout-recommendation chatbot](/docs/guides/guide-trainer-chatbot) for a complete example pairing this endpoint with an LLM.

## Workout Sessions API

Every completed workout session is stored server-side, tied to the `userId` the SDK was initialized with (sessions are persisted when the workout is launched with `shouldSendStats: true`). Fetch the history to build progress screens, completed-workout lists, or analytics.

```
GET https://data.kinestex.com/api/workout-sessions
```

**Headers:**

| Header | Description |
|--------|-------------|
| `x-api-key` | Your company API key (server-side only) |
| `x-user-id` | The user's id — **always send it**; it must match the SDK's `userId` |

**Query params (all optional):** `page` (default 1), `limit` (default 20, max 50), `sort` (default `started_at_desc`).

**Response** (trimmed to the most useful fields):

```json
{
  "sessions": [
    {
      "id": 4978,
      "content_title": "Fitness Lite",
      "content_image_url": "https://cdn.kinestex.com/uploads%2F...webp",
      "content_difficulty": "Medium",
      "calories_burned": 1.03,
      "actual_duration_seconds": 12,
      "completion_percentage": 0.67,
      "total_exercises": 10
    }
  ],
  "pagination": { "page": 1, "limit": 20, "total": 1, "total_pages": 1, "has_more": false }
}
```

Each session also carries `planned_exercises[]` (with per-exercise thumbnails and rep/time targets), `accuracy_score`, `efficiency_score`, `total_mistakes`, and timestamps.

**Key rules:**
- The values are **actuals**: `calories_burned` is what the user really burned and `actual_duration_seconds` is real time spent (in seconds — format it; don't show "0 min" for short sessions).
- `completion_percentage` (0–100, can be fractional like `0.67`) is authoritative — never derive completion from exercise counts, since `total_exercises` includes untouched exercises.
- **Errors:** `401` invalid `x-api-key` · `404` `x-user-id` not found for your company · `400` bad query param.

To open a full session summary in your app, pass the session `id` to the SDK's custom component view with route `session/{id}`. The complete card-building walkthrough (Swift + Kotlin) is in [Displaying completed workouts](/docs/guides/guide-completed-workouts).

## Session Auth & Managed Subscriptions

Let users browse KinesteX views and chat with the AI trainer for free, while **workout generation** requires an active subscription that **your backend controls**. KinesteX stores each user's subscription status and enforces it server-side — the client app can't bypass it.

**Fully optional.** If you never set a subscription status, nothing changes for your users.

All management calls are **server-to-server** with your API key (`x-api-key` header). Never ship the API key in your app.

**The flow:**

1. **Mint a session** for the user, declaring their subscription status:

```bash
curl -X POST https://data.kinestex.com/api/sessions \
  -H "x-api-key: YOUR_API_KEY" -H "content-type: application/json" \
  -d '{"user_id": "user-123", "is_subscribed": false}'
# → { "session_id": "ksx_sess_…", "user_id": "user-123", "is_subscribed": false, "expires_at": "…" }
```

2. **Launch the KinesteX view** with that `session_id` as usual — the SDK authenticates server-side and the raw API key never reaches the client. The user can browse, complete onboarding, and chat freely.

3. **At the generation step**, KinesteX posts `{ "type": "open_subscription_flow", "source": "generate_workout" }` to your app instead of generating. Present your paywall on top of the KinesteX view.

4. **After the purchase**, grant the subscription from your backend, then tell the view the flow finished by posting `{ "subscription_result": "purchased" }` (or `"dismissed"`) into the webview. On `"purchased"` the parked generation resumes automatically — no extra tap.

**Managing subscriptions:**

| Action | Call |
|--------|------|
| Grant | `POST /api/user-subscriptions` `{"user_id": "user-123"}` — idempotent; creates the user if they've never launched |
| Revoke | `DELETE /api/user-subscriptions/user-123` — blocks generation from their next attempt |
| List active | `GET /api/user-subscriptions?limit=100&offset=0` |
| Set at session mint | `POST /api/sessions` with `"is_subscribed": true/false` — omit to leave the stored status unchanged |

Status changes take effect on the user's next request — no relaunch needed.

**Rules & enforcement:**
- Status is **tri-state**: never set → nothing is gated (default); `true` → generation allowed; `false` → generation blocked.
- Only generation is gated (creating, modifying, or getting a recommended workout). Q&A, results discussion, and starting an already-generated workout stay available.
- Enforcement is server-side: an unsubscribed generation attempt returns `403 {"code": "not_subscribed"}` and the view re-opens your subscription flow. Reporting `"purchased"` without actually granting on the backend will not unlock generation.
- For users with a backend-managed status, that status **overrides** the `isSubscribed` launch flag. Users without one keep the launch-flag behavior unchanged.

**Web/redirect integrations** (no host app): pass `subscriptionReturnUrl` in the launch config instead. KinesteX redirects there at the generation step; after purchase, send the user back with a fresh session — generation unlocks automatically.

For the client-side handshake (events, edge cases, per-platform code), see the [subscription gating guide](/docs/guides/guide-subscription-gating).

---
Source: https://www.kinestex.com/docs/trainer-api · Index: https://www.kinestex.com/llms.txt
