Trainer Chat Lifecycle (REST)
The full Trainer Chat API 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_idis the persistent chat session — store it and send it on subsequent requests.
trainer-chat.ts
React TS (Web) example:
1const BASE_URL = "https://data.kinestex.com/api";
2
3interface SequenceItem {
4 order: number;
5 type: "warmup" | "exercise" | "cooldown" | "rest";
6 exercise_id: number | null;
7 repeats: number | null;
8 rest_countdown: number | null;
9 countdown: number;
10}
11
12interface TrainerChatResponse {
13 message: string;
14 intent: string;
15 action: "WORKOUT_UPDATED" | "NO_CHANGE" | "QUESTION" | "PROFILE_UPDATED" | "INFO";
16 session_id: string;
17 workout_plan?: {
18 step: "EXERCISE_RECOMMENDATION" | "PLAN_REFINEMENT";
19 action: "CONFIRMED" | null;
20 data: {
21 sequences: SequenceItem[];
22 exercises: Array<{
23 id: number;
24 thumbnail_url: string;
25 video_url: string;
26 translations: Array<{ language: string; title: string; description: string }>;
27 }>;
28 can_undo?: boolean;
29 can_redo?: boolean;
30 estimated_duration_seconds?: number;
31 };
32 };
33}
34
35async function chat(
36 body: Record<string, unknown>,
37 jwtToken: string
38): Promise<TrainerChatResponse> {
39 const response = await fetch(`${BASE_URL}/trainer/chat`, {
40 method: "POST",
41 headers: {
42 Authorization: `Bearer ${jwtToken}`,
43 "Content-Type": "application/json",
44 },
45 body: JSON.stringify(body),
46 });
47
48 if (!response.ok) {
49 const error = await response.json();
50 if (response.status === 403 && error.code === "not_subscribed") {
51 throw new Error("SUBSCRIPTION_REQUIRED"); // open your subscription flow
52 }
53 throw new Error(`Trainer API error: ${JSON.stringify(error)}`);
54 }
55 return response.json();
56}
57
58// Render a plan as text
59function formatPlan(resp: TrainerChatResponse, lang = "en"): string {
60 const plan = resp.workout_plan;
61 if (!plan) return "(no plan this turn)";
62
63 const titles = new Map(
64 plan.data.exercises.map((ex) => [
65 ex.id,
66 ex.translations.find((t) => t.language === lang)?.title ??
67 ex.translations.find((t) => t.language === "en")?.title ??
68 `Exercise #${ex.id}`,
69 ])
70 );
71
72 return plan.data.sequences
73 .map((seq) =>
74 seq.type === "rest"
75 ? ` Rest — ${seq.rest_countdown}s`
76 : ` [${seq.type}] ${titles.get(seq.exercise_id!)} × ${seq.repeats}`
77 )
78 .join("\n");
79}
80
81// Full conversation flow
82async function trainerDemo(jwtToken: string) {
83 // 1. Create: profile sent once; preferences live in the message
84 const created = await chat(
85 {
86 stage: "CREATE_WORKOUT",
87 message: "Create a 30-minute dumbbell workout for chest and triceps",
88 profile_data: {
89 age: 28,
90 weight: 75.0,
91 height: 180.0,
92 gender: "MALE",
93 fitness_goals: ["strength"],
94 },
95 },
96 jwtToken
97 );
98 const sessionId = created.session_id; // persist this
99 console.log(`Trainer: ${created.message}\n${formatPlan(created)}`);
100
101 // 2. Refine
102 const refined = await chat(
103 { session_id: sessionId, message: "make it harder, add core work" },
104 jwtToken
105 );
106 console.log(`Trainer: ${refined.message}\n${formatPlan(refined)}`);
107
108 // 3. Confirm — natural language, no magic string required
109 const confirmed = await chat(
110 { session_id: sessionId, message: "looks good, let's start" },
111 jwtToken
112 );
113 console.log(`Confirmed: ${confirmed.workout_plan?.action === "CONFIRMED"}`);
114 console.log(`Final plan:\n${formatPlan(confirmed)}`);
115 return confirmed;
116}