# Trainer Chat Lifecycle (REST)

The full [Trainer Chat API](/docs/trainer-api/trainer-api-chat) lifecycle in TypeScript: create a plan, refine it, confirm it. Key things this example demonstrates:

- The profile is sent **once** — it persists server-side.
- Preferences (duration, equipment, body parts) live **in the message** — no separate object needed.
- Confirmation is natural language — `workout_plan.action === "CONFIRMED"` signals the final plan.
- The top-level `session_id` is the persistent chat session — store it and send it on subsequent requests.

**trainer-chat.ts**

_React (TypeScript)_
```tsx
const BASE_URL = "https://data.kinestex.com/api";

interface SequenceItem {
  order: number;
  type: "warmup" | "exercise" | "cooldown" | "rest";
  exercise_id: number | null;
  repeats: number | null;
  rest_countdown: number | null;
  countdown: number;
}

interface TrainerChatResponse {
  message: string;
  intent: string;
  action: "WORKOUT_UPDATED" | "NO_CHANGE" | "QUESTION" | "PROFILE_UPDATED" | "INFO";
  session_id: string;
  workout_plan?: {
    step: "EXERCISE_RECOMMENDATION" | "PLAN_REFINEMENT";
    action: "CONFIRMED" | null;
    data: {
      sequences: SequenceItem[];
      exercises: Array<{
        id: number;
        thumbnail_url: string;
        video_url: string;
        translations: Array<{ language: string; title: string; description: string }>;
      }>;
      can_undo?: boolean;
      can_redo?: boolean;
      estimated_duration_seconds?: number;
    };
  };
}

async function chat(
  body: Record<string, unknown>,
  jwtToken: string
): Promise<TrainerChatResponse> {
  const response = await fetch(`${BASE_URL}/trainer/chat`, {
    method: "POST",
    headers: {
      Authorization: `Bearer ${jwtToken}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify(body),
  });

  if (!response.ok) {
    const error = await response.json();
    if (response.status === 403 && error.code === "not_subscribed") {
      throw new Error("SUBSCRIPTION_REQUIRED"); // open your subscription flow
    }
    throw new Error(`Trainer API error: ${JSON.stringify(error)}`);
  }
  return response.json();
}

// Render a plan as text
function formatPlan(resp: TrainerChatResponse, lang = "en"): string {
  const plan = resp.workout_plan;
  if (!plan) return "(no plan this turn)";

  const titles = new Map(
    plan.data.exercises.map((ex) => [
      ex.id,
      ex.translations.find((t) => t.language === lang)?.title ??
        ex.translations.find((t) => t.language === "en")?.title ??
        `Exercise #${ex.id}`,
    ])
  );

  return plan.data.sequences
    .map((seq) =>
      seq.type === "rest"
        ? `  Rest — ${seq.rest_countdown}s`
        : `  [${seq.type}] ${titles.get(seq.exercise_id!)} × ${seq.repeats}`
    )
    .join("\n");
}

// Full conversation flow
async function trainerDemo(jwtToken: string) {
  // 1. Create: profile sent once; preferences live in the message
  const created = await chat(
    {
      stage: "CREATE_WORKOUT",
      message: "Create a 30-minute dumbbell workout for chest and triceps",
      profile_data: {
        age: 28,
        weight: 75.0,
        height: 180.0,
        gender: "MALE",
        fitness_goals: ["strength"],
      },
    },
    jwtToken
  );
  const sessionId = created.session_id; // persist this
  console.log(`Trainer: ${created.message}\n${formatPlan(created)}`);

  // 2. Refine
  const refined = await chat(
    { session_id: sessionId, message: "make it harder, add core work" },
    jwtToken
  );
  console.log(`Trainer: ${refined.message}\n${formatPlan(refined)}`);

  // 3. Confirm — natural language, no magic string required
  const confirmed = await chat(
    { session_id: sessionId, message: "looks good, let's start" },
    jwtToken
  );
  console.log(`Confirmed: ${confirmed.workout_plan?.action === "CONFIRMED"}`);
  console.log(`Final plan:\n${formatPlan(confirmed)}`);
  return confirmed;
}
```

---
Source: https://www.kinestex.com/docs/guides/guide-trainer-rest-lifecycle · Index: https://www.kinestex.com/llms.txt
