# KinesteX SDK Documentation — Full Text
> Complete KinesteX SDK documentation. Source: https://www.kinestex.com/docs
This file concatenates every documentation section as Markdown for AI agents and LLMs. Code samples are included for all supported platforms (Swift, Kotlin, React Native, Flutter, HTML/JavaScript, React TypeScript).
---
## Getting Started
KinesteX offers a powerful motion tracking SDK that seamlessly integrates into your platform. Choose between ready-made workouts, plans, challenges, AI assessments, or create custom experiences with our advanced motion tracking.
### Overview
Our white-label solution supports any camera-enabled device across Android, iOS, and web, with SDKs for React Native, Flutter, Kotlin, Swift, Java, PWA, and JavaScript for quick integration.
KinesteX's real-time AI motion tracking boosts engagement, retention, and revenue while providing valuable data insights to create personalized, growth-driving experiences.
#### Plug and Play Integration
Pre-built experiences ready to integrate. Customize colors, fonts, and certain UI/UX elements while we handle the core workout experience, motion tracking, and user flow.
##### Complete UX (Main)
Full experience with personalized plan selection, user survey, and AI-generated workout schedules.
##### Plan View
Display a specific workout plan with schedule and multiple workouts.
##### Workout View
Launch directly into a specific workout by name or ID.
##### Challenge View
Gamified exercise challenges with leaderboards and rep tracking.
##### Leaderboard View
Display real-time leaderboards for challenge exercises as a standalone view.
##### AI Experiences
Interactive fitness games like Balloon Pop, Color Chase, and Alien Squat Shooter.
##### Personalized Plan
AI-generated workout plans tailored to user biometrics, fitness level, and assessment results.
##### AI Trainer Chat
Conversational AI personal trainer with in-app fitness assessment, personalized workout generation, and next-session scheduling.
Full guide: [AI Trainer Chat](/docs/ai-trainer-chat)
##### Admin Workout Editor
Embedded view for creating and managing workouts and exercises. Receive events for workout/exercise creation, selection, and updates. Flutter and Swift.
#### Custom Integration
Build everything yourself with full control over UI/UX. Use our Content APIs to fetch workout data and the Camera Component for real-time motion analysis.
##### Custom Workout
Create personalized workout sequences with custom exercises, reps, durations, and rest periods. Full control over exercise order and timing.
##### Camera Component
Access raw pose analysis data to build your own custom workout UI. Receive real-time rep counts, form feedback, and body tracking data.
##### Content Fetching APIs
Fetch workout plans, exercises, and content data via REST APIs. Build your own content browsing and workout selection experience.
### Requirements
**Platform Versions:**
• iOS 14.0+
• Android API 26+
• Web browser support (Chrome, Safari, Firefox, Edge)
**Prerequisites:**
• API key from KinesteX
• Camera permissions
• Internet connection
### Get API Key
To get demo access and your API key, fill out the contact form on our website.
[Contact Us](/#contact-form)
---
## Installation
Follow these steps to install the KinesteX SDK in your project.
### Step 1: Add Dependencies
Add the KinesteX SDK to your project using your platform's package manager.
**Install SDK**
_Swift (iOS)_
```swift
1. In Xcode: File > Add Package Dependencies...
2. Enter URL: https://github.com/KinesteX/KinesteX-AI-Kit.git
3. Click "Add Package"
```
_Kotlin (Android)_
```kotlin
// 1. Add JitPack repository in settings.gradle.kts
dependencyResolutionManagement {
repositories {
maven { url = uri("https://jitpack.io") }
}
}
// 2. Add dependency in app/build.gradle.kts
dependencies {
// Use the latest release from https://github.com/KinesteX/KinesteX-SDK-Kotlin/releases
implementation("com.github.KinesteX:KinesteX-SDK-Kotlin:2.0.9")
}
// 3. Sync project with Gradle files
```
_React Native_
```jsx
// 1. Install packages
npm install kinestex-sdk-react-native react-native-webview
// 2. Install iOS dependencies
cd ios && pod install && cd ..
// For Expo projects:
npx expo install react-native-webview
```
_Flutter_
```dart
# 1. Add to pubspec.yaml
dependencies:
kinestex_sdk_flutter: ^1.5.1 # use the latest version from pub.dev
permission_handler: ^11.3.1
# 2. Install packages
flutter pub get
# 3. Install iOS dependencies
cd ios && pod install && cd ..
```
_HTML / JavaScript_
```html
```
_React (TypeScript)_
```tsx
npm i kinestex-sdk-react-ts
```
### Step 2: Configure Permissions
Add the required permissions for camera and motion sensors.
**Required Permissions**
_Swift (iOS)_
```swift
NSCameraUsageDescription
Camera access is required for AI-powered workout tracking
NSMotionUsageDescription
Motion sensors help position your device correctly for workouts
```
_Kotlin (Android)_
```kotlin
```
_React Native_
```jsx
NSCameraUsageDescription
Camera access is required for AI-powered workout tracking
NSMotionUsageDescription
Motion sensors help position your device correctly for workouts
```
_Flutter_
```dart
NSCameraUsageDescription
Camera access is required for AI-powered workout tracking
NSMotionUsageDescription
Motion sensors help position your device correctly for workouts
```
_HTML / JavaScript_
```html
```
_React (TypeScript)_
```tsx
Browser will prompt users for camera access automatically. No additional permissions are needed.
```
### Step 3: Import the SDK
Import the KinesteX SDK in your code to start using it.
**Import Statement**
_Swift (iOS)_
```swift
import KinesteXAIKit
```
_Kotlin (Android)_
```kotlin
import com.kinestex.kinestexsdkkotlin.KinesteXSDK
```
_React Native_
```jsx
import KinestexSDK from 'kinestex-sdk-react-native';
// Types and enums live in the /src/types subpath (the package root has only the default export)
import { IntegrationOption, IPostData, PlanCategory } from 'kinestex-sdk-react-native/src/types';
```
_Flutter_
```dart
import 'package:kinestex_sdk_flutter/kinestex_sdk.dart';
```
_HTML / JavaScript_
```html
// Define the iframe element and create a URL to be later used to load the iframe
const webView = document.getElementById('webView');
const srcURL = 'https://ai.kinestex.com';
```
_React (TypeScript)_
```tsx
import {
IntegrationOption,
KinesteXSDK,
type IPostData,
type KinesteXSDKCamera,
} from "kinestex-sdk-react-ts";
```
**Step 4: iOS 15+ Device Orientation Fix (Required)** — React Native
react-native-webview doesn't support iOS 15+ motion permissions by default. Apply this patch to enable device orientation tracking:
_4.1: Install patch-package_
```bash
npm install patch-package postinstall-postinstall --save-dev
```
_4.2: Add postinstall script to package.json_
```json
{
"scripts": {
"postinstall": "patch-package"
}
}
```
_4.3: Patch RNCWebViewImpl.m_
```objectivec
// Edit: node_modules/react-native-webview/apple/RNCWebViewImpl.m
// Find requestMediaCapturePermissionForOrigin (line ~1312)
// Add this method after it:
- (void)webView:(WKWebView *)webView
requestDeviceOrientationAndMotionPermissionForOrigin:(WKSecurityOrigin *)origin
initiatedByFrame:(WKFrameInfo *)frame
decisionHandler:(void (^)(WKPermissionDecision))decisionHandler {
(void)webView;
(void)origin;
(void)frame;
decisionHandler(WKPermissionDecisionGrant);
}
```
_4.4: Save the patch_
```bash
npx patch-package react-native-webview
```
**Step 5: Expo Android Camera Fix** — React Native
For Expo on Android, request app-level camera permission before displaying KinesteX:
_5.1: Install expo-camera_
```bash
npx expo install expo-camera
```
_5.2: Configure app.json_
```json
{
"expo": {
"android": {
"permissions": ["android.permission.CAMERA"]
},
"ios": {
"infoPlist": {
"NSCameraUsageDescription": "For AI Motion Tracking"
}
}
}
}
```
_5.3: Request permission before showing KinesteX_
```typescript
import { Camera } from 'expo-camera';
import { useState, useEffect } from 'react';
const [hasPermission, setHasPermission] = useState(false);
useEffect(() => {
(async () => {
const { status } = await Camera.requestCameraPermissionsAsync();
setHasPermission(status === 'granted');
})();
}, []);
// Only render KinesteX when hasPermission is true
```
---
## Configuration
Initialize the SDK with your API credentials.
**Step 1: Initialize the SDK**
_Swift (iOS)_
```swift
import KinesteXAIKit
@State var showKinesteX = false // Controls KinesteX SDK visibility
@State var isLoading = false // Optional: Controls custom loading screen
// Initialize with your API key, company name, and unique user ID
let kinestex = KinesteXAIKit(
apiKey: "YOUR_API_KEY",
companyName: "YOUR_COMPANY",
userId: "unique-user-id"
)
// Optional: UserDetails to customize workout intensity
let user = UserDetails(
age: 20,
height: 170,
weight: 70,
gender: .Male,
lifestyle: .Active
)
```
_Kotlin (Android)_
```kotlin
import com.kinestex.kinestexsdkkotlin.KinesteXSDK
// Initialize the SDK in your Application class as early as possible
class MyApplication : Application() {
override fun onCreate() {
super.onCreate()
KinesteXSDK.initialize(
context = this,
apiKey = "YOUR_API_KEY",
companyName = "YOUR_COMPANY",
userId = "unique-user-id"
)
}
}
```
_React Native_
```jsx
// 1. Create a reference to KinesteXSDK component
const kinestexSDKRef = useRef(null);
// 2. Create postData object to initialize KinesteX session
const postData: IPostData = {
key: 'YOUR_API_KEY',
userId: 'YOUR_USER_ID', // unique per user
company: 'YOUR_COMPANY',
style: {
style: 'dark', // 'dark' or 'light'
loadingBackgroundColor: '000000', // hex without #
},
customParameters: {
// language: 'es' // customize language
},
// Optional: UserDetails for workout intensity
age: 25,
height: 180, // cm
weight: 75, // kg
gender: 'Male',
lifestyle: Lifestyle.Active,
};
// 3. Handle real-time messages from KinesteX
const handleMessage = (type: string, data: { [key: string]: any }) => {
switch (type) {
case 'exit_kinestex':
console.log('User exited');
setShowKinesteX(false);
break;
case 'plan_unlocked':
console.log('Plan unlocked:', data);
break;
default:
console.log('Message:', type, data);
break;
}
};
```
_Flutter_
```dart
import 'package:kinestex_sdk_flutter/kinestex_sdk.dart';
// Initialize the SDK in your main function as early as possible.
await KinesteXAIFramework.initialize(
apiKey: "YOUR_API_KEY",
companyName: "YOUR_COMPANY",
userId: "unique-user-id",
);
```
_HTML / JavaScript_
```html
// 1. Define the config data to be sent to the iframe
const postData = {
userId: "YOUR_USER_ID", // Unique identifier for the user
company: "YOUR_COMPANY", // Your organization's name from kinestex admin dashboard
key: "YOUR_API_KEY", // Your API Key from kinestex admin dashboard
// Optional parameters for customization
style: "dark", // dark or light theme
// user related customization
age: 30, // User's age (optional)
height: 175, // User's height in cm (optional)
weight: 70, // User's weight in kg (optional)
gender: "Female", // Gender (optional)
};
// 2. Sending the messages to the iframe will be done through postMessage API
function sendMessage() {
if (webView.contentWindow) {
webView.contentWindow.postMessage(postData, srcURL); // post initial data to start session
} else {
setTimeout(() => {
try {
webView.contentWindow.postMessage(postData, srcURL); // post initial data to start session
} catch {
webView.contentWindow.postMessage(postData, srcURL); // retry sending message
}
}, 100);
}
}
// 3. Whenever you want to display the view:
webView.src = srcURL; // Update this URL for specific features
webView.onload = () => {
sendMessage();
};
// 4. Handle the loading confirmation received from the iframe to send the initial data once more (iOS specific verification)
window.addEventListener("message", (event) => {
if (event.origin !== "https://ai.kinestex.com") return; // prevent listening for messages from other sources for security
const message = JSON.parse(event.data);
if (message.type === 'kinestex_loaded') {
sendMessage();
}
});
```
_React (TypeScript)_
```tsx
// 1. Create a reference to KinesteXSDK component
const kinestexSDKRef = useRef(null);
// 2. Create postData object to initialize KinesteX session
const postData: IPostData = {
key: 'YOUR_API_KEY',
userId: 'YOUR_USER_ID', // unique per user
company: 'YOUR_COMPANY',
style: {
style: 'dark', // 'dark' or 'light'
loadingBackgroundColor: '000000', // hex without #
},
customParameters: {
// language: 'es' // customize language
},
// Optional: UserDetails for workout intensity
age: 25,
height: 180, // cm
weight: 75, // kg
gender: 'Male',
lifestyle: Lifestyle.Active,
};
// 3. Handle real-time messages from KinesteX
const handleMessage = (type: string, data: { [key: string]: any }) => {
switch (type) {
case 'exit_kinestex':
console.log('User exited');
setShowKinesteX(false);
break;
case 'plan_unlocked':
console.log('Plan unlocked:', data);
break;
default:
console.log('Message:', type, data);
break;
}
};
```
**Step 2: Register Application Class** — Kotlin (Android)
Register your custom Application class in AndroidManifest.xml:
_AndroidManifest.xml_
```xml
```
**Step 3: Request Camera Permission** — Kotlin (Android)
Before a user starts a workout, request camera permission at the app level. Your activity or fragment must implement the PermissionHandler interface:
_MainActivity with PermissionHandler_
```kotlin
class MainActivity : AppCompatActivity(), PermissionHandler {
// Initialize the webview
private var kinesteXWebView: GenericWebView? = null
// Optional: Pass user details to adjust exercises and estimate calories
// Note: User details are only used on the device for customization during the session
private var userDetails = UserDetails(
age = 20,
height = 170,
weight = 180,
gender = Gender.MALE,
lifestyle = Lifestyle.ACTIVE
)
// Register permission launcher
private val requestPermissionLauncher = registerForActivityResult(
ActivityResultContracts.RequestPermission()
) { isGranted: Boolean ->
// Pass permission result to KinesteX webview
kinesteXWebView?.handlePermissionResult(isGranted)
}
// Override to display system dialog for camera access
override fun requestCameraPermission() {
requestPermissionLauncher.launch(Manifest.permission.CAMERA)
}
}
```
**Step 4: Handle SDK Messages** — Kotlin (Android)
Implement a callback function to handle messages from the SDK:
_Message Handler_
```kotlin
private fun handleWebViewMessage(message: WebViewMessage) {
when (message) {
is WebViewMessage.ExitKinestex -> {
// Dismiss KinesteX view when user clicks exit button
}
// Handle other messages
else -> {
Log.d("KinesteX", message.toString())
}
}
}
```
**Step 2: Request Camera Permission** — Flutter
Request camera permission before launching KinesteX. Ensure you've added the necessary permissions in AndroidManifest.xml and Info.plist. Add the following to your iOS Podfile:
_2.1: ios/Podfile_
```ruby
post_install do |installer|
installer.pods_project.targets.each do |target|
flutter_additional_ios_build_settings(target)
target.build_configurations.each do |config|
config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] ||= [
'$(inherited)',
## dart: PermissionGroup.camera
'PERMISSION_CAMERA=1',
]
end
end
end
```
_2.2: Request Permission in Dart_
```dart
void _checkCameraPermission() async {
if (await Permission.camera.request() != PermissionStatus.granted) {
_showCameraAccessDeniedAlert();
}
}
void _showCameraAccessDeniedAlert() {
showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: const Text("Camera Permission Denied"),
content: const Text("Camera access is required for this app to function properly."),
actions: [
TextButton(
child: const Text("OK"),
onPressed: () {
Navigator.of(context).pop();
},
),
],
);
},
);
}
```
**Step 3: Initialize on App Launch** — Flutter
Initialize KinesteX in your main function for WebView warmup:
_main.dart_
```dart
Future main() async {
await KinesteXAIFramework.initialize(
apiKey: YOUR_API_KEY,
companyName: YOUR_COMPANY_NAME,
userId: YOUR_USER_ID,
);
runApp(
const MaterialApp(
home: MyHomePage(),
),
);
}
```
**Step 4: Deinitialize on App Closure** — Flutter
Call dispose when closing the app:
_Dispose Method_
```dart
@override
void dispose() {
disposeKinesteXAIFramework();
super.dispose();
}
```
**Step 5: Setup Recommendations** — Flutter
Use a ValueNotifier to manage KinesteX presentation and handle callback messages:
_State Management & Message Handler_
```dart
import 'package:kinestex_sdk_flutter/kinestex_sdk.dart';
// Add a ValueNotifier to manage presentation
ValueNotifier showKinesteX = ValueNotifier(false);
// Handle callback messages from KinesteX
void handleWebViewMessage(WebViewMessage message) {
if (message is ExitKinestex) {
setState(() {
showKinesteX.value = false;
});
} else {
print("Message received: ${message.data}");
}
}
```
---
## Integration Options
Choose the integration option that best fits your use case. Each option provides a different level of UI customization.
Looking for a guided, conversational coaching experience? See the [AI Trainer Chat](/docs/ai-trainer-chat) integration guide — a chat-based AI personal trainer with workout launching and next-session scheduling.
### Plug and Play Integration
Pre-built experiences ready to integrate. Customize colors, fonts, and certain UI/UX elements while we handle the core workout experience, motion tracking, and user flow.
#### Complete UX (Main)
Display the full KinesteX experience with personalized workout plan selection based on category. Includes user survey, assessment, and personalized schedule generation. This is the easiest integration option.
**Available Plan Categories:**
| Plan Category | Key |
|---------------|-----|
| Strength | Strength |
| Cardio | Cardio |
| Weight Management | Weight Management |
| Rehabilitation | Rehabilitation |
| Custom | Custom |
**Define Plan Category**
First, define the plan category for personalized fitness goals:
_Swift (iOS)_
```swift
// Plan category for personalized fitness goals
@State private var planCategory: PlanCategory = .Cardio
```
_Kotlin (Android)_
```kotlin
// Plan category for personalized fitness goals
private var planCategory = PlanCategory.Cardio
```
_React Native_
```jsx
// Plan category for personalized fitness goals
const planCategory = PlanCategory.Cardio; // Strength, Cardio, etc.;
```
_Flutter_
```dart
// Plan category for personalized fitness goals
PlanCategory planCategory = PlanCategory.Cardio;
```
_HTML / JavaScript_
```html
// Plan category for personalized fitness goals
const planCategory = "Cardio";
```
_React (TypeScript)_
```tsx
// Plan category for personalized fitness goals
const planCategory = PlanCategory.Cardio; // Strength, Cardio, etc.
```
**Display Category View**
Display the category-based view with real-time message handling:
_Swift (iOS)_
```swift
kinestex.createCategoryView(
planCategory: planCategory,
user: user, // optional: can be nil
isLoading: $isLoading,
customParams: ["style": "dark"], // dark or light theme
onMessageReceived: { message in
switch message {
case .kinestex_launched(let data):
print("KinesteX Launched: \(data)")
case .finished_workout(let data):
print("Workout Finished: \(data)")
case .exit_kinestex(let data):
showKinesteX = false // Dismiss the view
default:
print("Received \(message)")
break
}
}
)
// OPTIONAL: Display loading screen during view initialization
.overlay(
Group {
if showAnimation {
Text("Aifying workouts...")
.foregroundColor(.black)
.font(.caption)
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color.white)
}
}
)
.onChange(of: isLoading) { newValue in
withAnimation(.easeInOut(duration: 2.5)) {
showAnimation = !newValue
}
}
```
_Kotlin (Android)_
```kotlin
KinesteXSDK.createMainView(
context = this,
planCategory = planCategory,
user = userDetails, // optional user details
customParams = mutableMapOf("style" to "dark"),
isLoading = viewModel.isLoading,
onMessageReceived = { message ->
when (message) {
is WebViewMessage.KinestexLaunched ->
println("KinesteX Launched")
is WebViewMessage.FinishedWorkout ->
println("Workout Finished: ${message.data}")
is WebViewMessage.ExitKinestex -> finish()
else -> println("Received: $message")
}
},
permissionHandler = this
)
```
_React Native_
```jsx
// postData structure
const postData: IPostData = {
key: 'YOUR_API_KEY',
userId: 'YOUR_USER_ID',
company: 'YOUR_COMPANY_NAME',
planCategory: planCategory,
style: {
style: 'dark', // or 'light'
},
};
{
switch (type) {
case 'kinestex_launched':
console.log('KinesteX Launched:', data);
break;
case 'finished_workout':
console.log('Workout Finished:', data);
break;
case 'exit_kinestex':
setShowKinesteX(false);
break;
}
}}
/>
```
_Flutter_
```dart
KinesteXAIFramework.createMainView(
isShowKinestex: showKinesteX,
planCategory: planCategory,
customParams: {"style": "dark"},
isLoading: ValueNotifier(false),
onMessageReceived: (message) {
if (message is KinestexLaunched) {
print('KinesteX Launched');
} else if (message is FinishedWorkout) {
print('Workout Finished: ${message.data}');
} else if (message is ExitKinestex) {
setState(() {
showKinesteX.value = false;
});
}
},
)
```
_HTML / JavaScript_
```html
// Add planCategory to postData
const postData = {
// ... all initial fields
planCategory: planCategory, // "Strength", "Cardio", etc.
};
const srcURL = "https://ai.kinestex.com";
webView.src = srcURL;
webView.onload = () => {
sendMessage();
};
// Handle messages
window.addEventListener('message', (event) => {
const { type, data } = event.data;
switch (type) {
case 'kinestex_launched':
console.log('KinesteX Launched:', data);
break;
case 'exit_kinestex':
// Hide the iframe
break;
}
});
```
_React (TypeScript)_
```tsx
import { IntegrationOption, KinesteXSDK, PlanCategory, type IPostData } from 'kinestex-sdk-react-ts';
// postData structure
const postData: IPostData = {
key: 'YOUR_API_KEY',
userId: 'YOUR_USER_ID',
company: 'YOUR_COMPANY_NAME',
planCategory: PlanCategory.Cardio,
style: {
style: 'dark',
},
};
{
switch (type) {
case 'kinestex_launched':
console.log('KinesteX Launched:', data);
break;
case 'finished_workout':
console.log('Workout Finished:', data);
break;
case 'exit_kinestex':
setShowKinesteX(false);
break;
}
}}
/>
```
**Complete Example**
Full implementation example with all required setup:
_Swift (iOS)_
```swift
import SwiftUI
import KinesteXAIKit
struct MainViewIntegration: View {
@State private var showKinesteX = false
@State private var isLoading = false
// Replace with your KinesteX credentials
let kinesteXKit = KinesteXAIKit(
apiKey: "YOUR API KEY",
companyName: "YOUR COMPANY NAME",
userId: "YOUR USER ID"
)
// Plan category for personalized fitness goals
@State private var planCategory: PlanCategory = .Cardio
var body: some View {
VStack {
Text("KinesteX Main View")
.font(.title)
.padding()
Spacer()
Button(action: {
showKinesteX.toggle()
}) {
Text("Open Main View")
.font(.title3)
.foregroundColor(.white)
.bold()
.padding()
.frame(maxWidth: .infinity)
.background(Color.green.cornerRadius(10))
.padding(.horizontal)
}
Spacer()
}
.fullScreenCover(isPresented: $showKinesteX) {
kinestex.createCategoryView(
planCategory: planCategory,
user: nil,
isLoading: $isLoading,
customParams: ["style": "light"],
onMessageReceived: { message in
switch message {
case .exit_kinestex(_):
showKinesteX = false
default:
print("Message received: \(message)")
}
}
)
}
}
}
#Preview {
MainViewIntegration()
}
```
_Kotlin (Android)_
```kotlin
import android.os.Bundle
import android.widget.Toast
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.runtime.mutableStateOf
import com.kinestex.kinestexsdkkotlin.GenericWebView
import com.kinestex.kinestexsdkkotlin.KinesteXSDK
import com.kinestex.kinestexsdkkotlin.PlanCategory
import com.kinestex.kinestexsdkkotlin.PermissionHandler
import com.kinestex.kinestexsdkkotlin.WebViewMessage
import kotlinx.coroutines.flow.MutableStateFlow
class MainViewActivity : ComponentActivity(), PermissionHandler {
private val viewModel = MainViewModel()
// OPTIONAL: UserDetails to customize workout intensity and calorie estimation
// Note: User details are only used on-device during the session
private val userDetails = UserDetails(
age = 30,
height = 180,
weight = 75,
gender = Gender.MALE,
lifestyle = Lifestyle.ACTIVE
)
// Custom data for the WebView
private val data = mutableMapOf()
// Store reference to the KinesteX WebView
private var kinesteXWebView: GenericWebView? = null
// Register permission launcher
private val requestPermissionLauncher = registerForActivityResult(
ActivityResultContracts.RequestPermission()
) { isGranted: Boolean ->
// Pass permission result to KinesteX webview
kinesteXWebView?.handlePermissionResult(isGranted)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
data["style"] = "light"
setContent {
val webView = KinesteXSDK.createMainView(
context = this,
planCategory = PlanCategory.Cardio,
user = userDetails,
customParams = data,
isLoading = viewModel.isLoading,
onMessageReceived = ::handleWebViewMessage,
permissionHandler = this
) as GenericWebView
kinesteXWebView = webView
webView.Render()
}
}
private fun handleWebViewMessage(message: WebViewMessage) {
when (message) {
is WebViewMessage.ExitKinestex -> finish()
is WebViewMessage.KinestexLaunched -> viewModel.isLoading.value = false
else -> Toast.makeText(this, "Received: $message", Toast.LENGTH_SHORT).show()
}
}
// When request is sent, display system dialog for camera access
override fun requestCameraPermission() {
requestPermissionLauncher.launch(Manifest.permission.CAMERA)
}
}
class MainViewModel {
val isLoading = MutableStateFlow(true)
}
```
_React Native_
```jsx
import React, { useState } from 'react';
import { View, Button, StyleSheet } from 'react-native';
import KinestexSDK from 'kinestex-sdk-react-native';
import { IntegrationOption, IPostData, PlanCategory } from 'kinestex-sdk-react-native/src/types';
const MainViewIntegration = () => {
const [showKinesteX, setShowKinesteX] = useState(false);
const postData: IPostData = {
key: 'YOUR_API_KEY',
userId: 'YOUR_USER_ID',
company: 'YOUR_COMPANY_NAME',
planCategory: PlanCategory.Cardio,
style: {
style: 'dark',
},
};
return (
{showKinesteX ? (
{
if (type === 'exit_kinestex') {
setShowKinesteX(false);
}
}}
/>
) : (
setShowKinesteX(true)}
/>
)}
);
};
const styles = StyleSheet.create({
container: { flex: 1 }
});
export default MainViewIntegration;
```
_Flutter_
```dart
import 'package:flutter/material.dart';
import 'package:kinestex_sdk_flutter/kinestex_sdk.dart';
import 'package:permission_handler/permission_handler.dart';
Future main() async {
WidgetsFlutterBinding.ensureInitialized();
await KinesteXAIFramework.initialize(
apiKey: "your_api_key",
companyName: "your_company_name",
userId: "your_user_id",
);
runApp(const MyApp());
}
class MyApp extends StatefulWidget {
const MyApp({super.key});
@override
State createState() => _MyAppState();
}
class _MyAppState extends State {
@override
void dispose() {
disposeKinesteXAIFramework();
super.dispose();
}
Future disposeKinesteXAIFramework() async {
await KinesteXAIFramework.dispose();
}
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'KinesteX Main View',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const MyHomePage(),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key});
@override
State createState() => _MyHomePageState();
}
class _MyHomePageState extends State {
ValueNotifier showKinesteX = ValueNotifier(false);
@override
void initState() {
super.initState();
_checkCameraPermission();
}
void _checkCameraPermission() async {
if (await Permission.camera.request() != PermissionStatus.granted) {
_showCameraAccessDeniedAlert();
}
}
void _showCameraAccessDeniedAlert() {
showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: const Text("Camera Permission Denied"),
content: const Text("Camera access is required for this app to function properly."),
actions: [
TextButton(
child: const Text("OK"),
onPressed: () => Navigator.of(context).pop(),
),
],
);
},
);
}
void handleWebViewMessage(WebViewMessage message) {
if (message is ExitKinestex) {
setState(() {
showKinesteX.value = false;
});
}
}
Widget createMainView() {
return Center(
child: KinesteXAIFramework.createMainView(
isShowKinestex: showKinesteX,
planCategory: PlanCategory.Cardio,
customParams: {
"style": "dark",
},
isLoading: ValueNotifier(false),
onMessageReceived: handleWebViewMessage,
),
);
}
@override
Widget build(BuildContext context) {
return ValueListenableBuilder(
valueListenable: showKinesteX,
builder: (context, isShowKinesteX, child) {
return isShowKinesteX
? SafeArea(
child: createMainView(),
)
: Scaffold(
body: Center(
child: ElevatedButton(
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(
horizontal: 40,
vertical: 20,
),
backgroundColor: Colors.green,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
),
onPressed: () {
showKinesteX.value = true;
},
child: const Text(
'Start Main View',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
),
),
);
},
);
}
}
```
_HTML / JavaScript_
```html
KinesteX: Complete User Experience
Start KinesteX
```
_React (TypeScript)_
```tsx
import React, { useState } from 'react';
import {
IntegrationOption,
KinesteXSDK,
PlanCategory,
type IPostData,
} from 'kinestex-sdk-react-ts';
const MainViewIntegration: React.FC = () => {
const [showKinesteX, setShowKinesteX] = useState(false);
const postData: IPostData = {
key: 'YOUR_API_KEY',
userId: 'YOUR_USER_ID',
company: 'YOUR_COMPANY_NAME',
planCategory: PlanCategory.Cardio,
style: {
style: 'dark',
},
};
return (
{showKinesteX ? (
{
if (type === 'exit_kinestex') {
setShowKinesteX(false);
}
}}
/>
) : (
setShowKinesteX(true)}>
Open Main View
)}
);
};
export default MainViewIntegration;
```
#### Workout View
Personalized Workouts: Anytime, Anywhere.
- **Tailored for All Levels**: Workouts for strength, flexibility, or relaxation
- **Time-Saving**: Quick, efficient sessions with zero hassle
- **Engaging**: Keep users motivated with fresh, personalized routines
- **Easy Integration**: Add workouts seamlessly with minimal effort
You can find workouts in our [workout library](https://workout-view.kinestex.com/?tab=workouts), or create your own workouts in our [admin portal](https://admin.kinestex.com).
**Workout Integration**
Display a workout by name or ID:
_Swift (iOS)_
```swift
kinestex.createWorkoutView(
workout: selectedWorkout, // workout name or ID
user: nil,
isLoading: $isLoading,
customParams: ["style": "dark", "language": "en"], // dark or light theme
onMessageReceived: { message in
switch message {
case .exit_kinestex(_):
showKinesteX = false // dismiss the view
default:
print("Received \(message)")
break
}
}
)
```
_Kotlin (Android)_
```kotlin
KinesteXSDK.createWorkoutView(
context = this,
workoutName = selectedWorkout, // workout name or ID
user = userDetails, // optional user details
customParams = mutableMapOf("style" to "dark", "language" to "en"),
isLoading = viewModel.isLoading,
onMessageReceived = { message ->
when (message) {
is WebViewMessage.ExitKinestex -> finish()
else -> println("Received: $message")
}
},
permissionHandler = this
)
```
_React Native_
```jsx
// postData structure
const postData: IPostData = {
key: 'YOUR_API_KEY',
userId: 'YOUR_USER_ID',
company: 'YOUR_COMPANY_NAME',
style: {
style: 'dark',
},
};
{
if (type === 'exit_kinestex') {
setShowKinesteX(false);
}
}}
/>
```
_Flutter_
```dart
KinesteXAIFramework.createWorkoutView(
isShowKinestex: showKinesteX,
workoutName: selectedWorkout, // workout name or ID
customParams: {"style": "dark", "language": "en"},
isLoading: ValueNotifier(false),
onMessageReceived: (message) {
if (message is ExitKinestex) {
setState(() => showKinesteX.value = false);
}
},
)
```
_HTML / JavaScript_
```html
// Specify workout ID in the URL
const srcURL = "https://ai.kinestex.com/workout/YOUR_WORKOUT_ID";
webView.src = srcURL;
```
_React (TypeScript)_
```tsx
import { IntegrationOption, KinesteXSDK, type IPostData } from 'kinestex-sdk-react-ts';
// postData structure
const postData: IPostData = {
key: 'YOUR_API_KEY',
userId: 'YOUR_USER_ID',
company: 'YOUR_COMPANY_NAME',
style: {
style: 'dark',
},
};
{
if (type === 'exit_kinestex') {
setShowKinesteX(false);
}
}}
/>
```
**Complete Example**
Full implementation example with workout selection:
_Swift (iOS)_
```swift
import SwiftUI
import KinesteXAIKit
struct WorkoutIntegrationView: View {
@State private var showKinesteX = false
@State private var isLoading = false
// Initialize KinesteXAIKit
// Replace with your KinesteX credentials
let kinesteXKit = KinesteXAIKit(
apiKey: "YOUR API KEY",
companyName: "YOUR COMPANY NAME",
userId: "YOUR USER ID"
)
// Replace with the name or ID of the workout
let workoutName = "Fitness Lite"
var body: some View {
VStack {
Text("KinesteX Workout Integration")
.font(.title)
.padding()
Spacer()
Button(action: {
showKinesteX.toggle()
}) {
Text("Start \(workoutName) Workout")
.font(.title3)
.foregroundColor(.white)
.bold()
.padding()
.frame(maxWidth: .infinity)
.background(Color.green.cornerRadius(10))
.padding(.horizontal)
}
.padding()
Spacer()
}
.fullScreenCover(isPresented: $showKinesteX) {
kinesteXKit.createWorkoutView(
workout: workoutName,
user: nil,
isLoading: $isLoading,
customParams: ["style": "dark"],
onMessageReceived: { message in
switch message {
case .exit_kinestex(_):
showKinesteX = false
default:
print("Message received: \(message)")
}
}
)
}
}
}
#Preview {
WorkoutIntegrationView()
}
```
_Kotlin (Android)_
```kotlin
import android.os.Bundle
import android.widget.Toast
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import com.kinestex.kinestexsdkkotlin.GenericWebView
import com.kinestex.kinestexsdkkotlin.KinesteXSDK
import com.kinestex.kinestexsdkkotlin.PermissionHandler
import com.kinestex.kinestexsdkkotlin.WebViewMessage
import kotlinx.coroutines.flow.MutableStateFlow
class WorkoutViewActivity : ComponentActivity(), PermissionHandler {
private val viewModel = WorkoutViewModel()
// OPTIONAL: UserDetails to customize workout intensity and calorie estimation
// Note: User details are only used on-device during the session
private val userDetails = UserDetails(
age = 30,
height = 180,
weight = 75,
gender = Gender.MALE,
lifestyle = Lifestyle.ACTIVE
)
// Custom data for the WebView
private val data = mutableMapOf()
// Store reference to the KinesteX WebView
private var kinesteXWebView: GenericWebView? = null
// Register permission launcher
private val requestPermissionLauncher = registerForActivityResult(
ActivityResultContracts.RequestPermission()
) { isGranted: Boolean ->
// Pass permission result to KinesteX webview
kinesteXWebView?.handlePermissionResult(isGranted)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
data["style"] = "dark"
setContent {
val webView = KinesteXSDK.createWorkoutView(
context = this,
workoutName = "Fitness Lite",
user = userDetails,
customParams = data,
isLoading = viewModel.isLoading,
onMessageReceived = ::handleWebViewMessage,
permissionHandler = this
) as GenericWebView
kinesteXWebView = webView
webView.Render()
}
}
private fun handleWebViewMessage(message: WebViewMessage) {
when (message) {
is WebViewMessage.ExitKinestex -> finish()
is WebViewMessage.KinestexLaunched -> viewModel.isLoading.value = false
else -> Toast.makeText(this, "Received: $message", Toast.LENGTH_SHORT).show()
}
}
// When request is sent, display system dialog for camera access
override fun requestCameraPermission() {
requestPermissionLauncher.launch(Manifest.permission.CAMERA)
}
}
class WorkoutViewModel {
val isLoading = MutableStateFlow(true)
}
```
_React Native_
```jsx
import React, { useState } from 'react';
import { View, Button, StyleSheet } from 'react-native';
import KinestexSDK from 'kinestex-sdk-react-native';
import { IntegrationOption, IPostData } from 'kinestex-sdk-react-native/src/types';
const WorkoutIntegration = () => {
const [showKinesteX, setShowKinesteX] = useState(false);
const selectedWorkout = "Fitness Lite";
const postData: IPostData = {
key: 'YOUR_API_KEY',
userId: 'YOUR_USER_ID',
company: 'YOUR_COMPANY_NAME',
style: {
style: 'dark',
},
};
return (
{showKinesteX ? (
{
if (type === 'exit_kinestex') {
setShowKinesteX(false);
}
}}
/>
) : (
setShowKinesteX(true)}
/>
)}
);
};
const styles = StyleSheet.create({
container: { flex: 1 }
});
export default WorkoutIntegration;
```
_Flutter_
```dart
import 'package:flutter/material.dart';
import 'package:kinestex_sdk_flutter/kinestex_sdk.dart';
import 'package:permission_handler/permission_handler.dart';
Future main() async {
WidgetsFlutterBinding.ensureInitialized();
await KinesteXAIFramework.initialize(
apiKey: "your_api_key",
companyName: "your_company_name",
userId: "your_user_id",
);
runApp(const MyApp());
}
class MyApp extends StatefulWidget {
const MyApp({super.key});
@override
State createState() => _MyAppState();
}
class _MyAppState extends State {
@override
void dispose() {
disposeKinesteXAIFramework();
super.dispose();
}
Future disposeKinesteXAIFramework() async {
await KinesteXAIFramework.dispose();
}
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'KinesteX Workout',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const MyHomePage(),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key});
@override
State createState() => _MyHomePageState();
}
class _MyHomePageState extends State {
ValueNotifier showKinesteX = ValueNotifier(false);
@override
void initState() {
super.initState();
_checkCameraPermission();
}
void _checkCameraPermission() async {
if (await Permission.camera.request() != PermissionStatus.granted) {
_showCameraAccessDeniedAlert();
}
}
void _showCameraAccessDeniedAlert() {
showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: const Text("Camera Permission Denied"),
content: const Text("Camera access is required for this app to function properly."),
actions: [
TextButton(
child: const Text("OK"),
onPressed: () => Navigator.of(context).pop(),
),
],
);
},
);
}
void handleWebViewMessage(WebViewMessage message) {
if (message is ExitKinestex) {
setState(() {
showKinesteX.value = false;
});
}
}
Widget createWorkoutView() {
return Center(
child: KinesteXAIFramework.createWorkoutView(
isShowKinestex: showKinesteX,
workoutName: "Fitness Lite",
customParams: {
"style": "dark",
},
isLoading: ValueNotifier(false),
onMessageReceived: handleWebViewMessage,
),
);
}
@override
Widget build(BuildContext context) {
return ValueListenableBuilder(
valueListenable: showKinesteX,
builder: (context, isShowKinesteX, child) {
return isShowKinesteX
? SafeArea(
child: createWorkoutView(),
)
: Scaffold(
body: Center(
child: ElevatedButton(
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(
horizontal: 40,
vertical: 20,
),
backgroundColor: Colors.green,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
),
onPressed: () {
showKinesteX.value = true;
},
child: const Text(
'Start Workout',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
),
),
);
},
);
}
}
```
_HTML / JavaScript_
```html
KinesteX: Workout
Start KinesteX
```
_React (TypeScript)_
```tsx
import React, { useState } from 'react';
import {
IntegrationOption,
KinesteXSDK,
type IPostData,
} from 'kinestex-sdk-react-ts';
const WorkoutIntegration: React.FC = () => {
const [showKinesteX, setShowKinesteX] = useState(false);
const selectedWorkout = "Fitness Lite";
const postData: IPostData = {
key: 'YOUR_API_KEY',
userId: 'YOUR_USER_ID',
company: 'YOUR_COMPANY_NAME',
style: {
style: 'dark',
},
};
return (
{showKinesteX ? (
{
if (type === 'exit_kinestex') {
setShowKinesteX(false);
}
}}
/>
) : (
setShowKinesteX(true)}>
Start {selectedWorkout}
)}
);
};
export default WorkoutIntegration;
```
#### Plan View
Display a specific workout plan with multiple workouts. Shows plan overview, schedule, and individual workout access.
**Key Features of Our Workout Plans:**
- **Goal-Oriented**: Supports strength, flexibility, and wellness goals
- **Seamless Experience**: From recommendations to real-time feedback
- **Customizable**: Brand-aligned app design
- **Quick Integration**: Easy setup for advanced fitness solutions
You can find plans in our [workout library](https://workout-view.kinestex.com/?tab=plans), or create your own plans in our [admin portal](https://admin.kinestex.com).
**Plan Integration**
Display a workout plan by name or ID:
_Swift (iOS)_
```swift
kinestex.createPlanView(
plan: selectedPlan, // name or ID of the plan (string)
user: nil, // OPTIONAL: provide user details
isLoading: $isLoading,
customParams: ["style": "dark"], // dark or light theme
onMessageReceived: { message in
switch message {
case .exit_kinestex(_):
showKinesteX = false // dismiss the view
default:
print("Received \(message)")
break
}
}
)
```
_Kotlin (Android)_
```kotlin
KinesteXSDK.createPlanView(
context = this,
planName = selectedPlan, // name or ID of the plan
user = userDetails, // optional user details
customParams = mutableMapOf("style" to "dark"),
isLoading = viewModel.isLoading,
onMessageReceived = { message ->
when (message) {
is WebViewMessage.ExitKinestex -> finish()
else -> println("Received: $message")
}
},
permissionHandler = this
)
```
_React Native_
```jsx
// postData structure
const postData: IPostData = {
key: 'YOUR_API_KEY',
userId: 'YOUR_USER_ID',
company: 'YOUR_COMPANY_NAME',
style: {
style: 'dark',
},
};
{
if (type === 'exit_kinestex') {
setShowKinesteX(false);
}
}}
/>
```
_Flutter_
```dart
KinesteXAIFramework.createPlanView(
isShowKinestex: showKinesteX,
planName: selectedPlan, // name or ID of the plan
customParams: {"style": "dark"},
isLoading: ValueNotifier(false),
onMessageReceived: (message) {
if (message is ExitKinestex) {
setState(() => showKinesteX.value = false);
}
},
)
```
_HTML / JavaScript_
```html
// Specify plan ID in the URL
const srcURL = "https://ai.kinestex.com/plan/YOUR_PLAN_ID";
webView.src = srcURL;
```
_React (TypeScript)_
```tsx
import { IntegrationOption, KinesteXSDK, type IPostData } from 'kinestex-sdk-react-ts';
// postData structure
const postData: IPostData = {
key: 'YOUR_API_KEY',
userId: 'YOUR_USER_ID',
company: 'YOUR_COMPANY_NAME',
style: {
style: 'dark',
},
};
{
if (type === 'exit_kinestex') {
setShowKinesteX(false);
}
}}
/>
```
**Complete Example**
Full implementation example with plan selection:
_Swift (iOS)_
```swift
import SwiftUI
import KinesteXAIKit
struct PlanViewIntegration: View {
@State private var showKinesteX = false
@State private var isLoading = false
@State private var selectedPlan = "livggb4P6zoD94VsTBB6"
let kinestex = KinesteXAIKit(
apiKey: "YOUR API KEY",
companyName: "YOUR COMPANY NAME",
userId: "YOUR USER ID"
)
var body: some View {
VStack {
Text("Select a Plan")
.font(.title)
.padding()
Spacer()
Button(action: {
showKinesteX.toggle()
}) {
Text("Start \(selectedPlan)")
.font(.title3)
.foregroundColor(.white)
.bold()
.padding()
.frame(maxWidth: .infinity)
.background(Color.green.cornerRadius(10))
.padding(.horizontal)
}
Spacer()
}
.fullScreenCover(isPresented: $showKinesteX) {
kinestex.createPlanView(
plan: selectedPlan,
user: nil,
isLoading: $isLoading,
customParams: ["style": "light"],
onMessageReceived: { message in
switch message {
case .exit_kinestex(_):
showKinesteX = false
default:
print("Message: \(message)")
}
}
)
}
}
}
#Preview {
PlanViewIntegration()
}
```
_Kotlin (Android)_
```kotlin
import android.os.Bundle
import android.widget.Toast
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import com.kinestex.kinestexsdkkotlin.GenericWebView
import com.kinestex.kinestexsdkkotlin.KinesteXSDK
import com.kinestex.kinestexsdkkotlin.PermissionHandler
import com.kinestex.kinestexsdkkotlin.WebViewMessage
import kotlinx.coroutines.flow.MutableStateFlow
class PlanViewActivity : ComponentActivity(), PermissionHandler {
private val viewModel = PlanViewModel()
// OPTIONAL: UserDetails to customize workout intensity and calorie estimation
// Note: User details are only used on-device during the session
private val userDetails = UserDetails(
age = 30,
height = 180,
weight = 75,
gender = Gender.MALE,
lifestyle = Lifestyle.ACTIVE
)
// Custom data for the WebView
private val data = mutableMapOf()
// Store reference to the KinesteX WebView
private var kinesteXWebView: GenericWebView? = null
// Register permission launcher
private val requestPermissionLauncher = registerForActivityResult(
ActivityResultContracts.RequestPermission()
) { isGranted: Boolean ->
// Pass permission result to KinesteX webview
kinesteXWebView?.handlePermissionResult(isGranted)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
data["style"] = "light"
setContent {
val webView = KinesteXSDK.createPlanView(
context = this,
planName = "livggb4P6zoD94VsTBB6", // name or ID of the plan
user = userDetails,
customParams = data,
isLoading = viewModel.isLoading,
onMessageReceived = ::handleWebViewMessage,
permissionHandler = this
) as GenericWebView
kinesteXWebView = webView
webView.Render()
}
}
private fun handleWebViewMessage(message: WebViewMessage) {
when (message) {
is WebViewMessage.ExitKinestex -> finish()
is WebViewMessage.KinestexLaunched -> viewModel.isLoading.value = false
else -> Toast.makeText(this, "Received: $message", Toast.LENGTH_SHORT).show()
}
}
// When request is sent, display system dialog for camera access
override fun requestCameraPermission() {
requestPermissionLauncher.launch(Manifest.permission.CAMERA)
}
}
class PlanViewModel {
val isLoading = MutableStateFlow(true)
}
```
_React Native_
```jsx
import React, { useState } from 'react';
import { View, Button, StyleSheet } from 'react-native';
import KinestexSDK from 'kinestex-sdk-react-native';
import { IntegrationOption, IPostData } from 'kinestex-sdk-react-native/src/types';
const PlanViewIntegration = () => {
const [showKinesteX, setShowKinesteX] = useState(false);
const selectedPlan = "livggb4P6zoD94VsTBB6"; // plan ID
const postData: IPostData = {
key: 'YOUR_API_KEY',
userId: 'YOUR_USER_ID',
company: 'YOUR_COMPANY_NAME',
style: {
style: 'dark',
},
};
return (
{showKinesteX ? (
{
if (type === 'exit_kinestex') {
setShowKinesteX(false);
}
}}
/>
) : (
setShowKinesteX(true)}
/>
)}
);
};
const styles = StyleSheet.create({
container: { flex: 1 }
});
export default PlanViewIntegration;
```
_Flutter_
```dart
import 'package:flutter/material.dart';
import 'package:kinestex_sdk_flutter/kinestex_sdk.dart';
import 'package:permission_handler/permission_handler.dart';
Future main() async {
WidgetsFlutterBinding.ensureInitialized();
await KinesteXAIFramework.initialize(
apiKey: "your_api_key",
companyName: "your_company_name",
userId: "your_user_id",
);
runApp(const MyApp());
}
class MyApp extends StatefulWidget {
const MyApp({super.key});
@override
State createState() => _MyAppState();
}
class _MyAppState extends State {
@override
void dispose() {
disposeKinesteXAIFramework();
super.dispose();
}
Future disposeKinesteXAIFramework() async {
await KinesteXAIFramework.dispose();
}
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'KinesteX Plan',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const MyHomePage(),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key});
@override
State createState() => _MyHomePageState();
}
class _MyHomePageState extends State {
ValueNotifier showKinesteX = ValueNotifier(false);
@override
void initState() {
super.initState();
_checkCameraPermission();
}
void _checkCameraPermission() async {
if (await Permission.camera.request() != PermissionStatus.granted) {
_showCameraAccessDeniedAlert();
}
}
void _showCameraAccessDeniedAlert() {
showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: const Text("Camera Permission Denied"),
content: const Text("Camera access is required for this app to function properly."),
actions: [
TextButton(
child: const Text("OK"),
onPressed: () => Navigator.of(context).pop(),
),
],
);
},
);
}
void handleWebViewMessage(WebViewMessage message) {
if (message is ExitKinestex) {
setState(() {
showKinesteX.value = false;
});
}
}
Widget createPlanView() {
return Center(
child: KinesteXAIFramework.createPlanView(
isShowKinestex: showKinesteX,
planName: "Full Body Fitness",
customParams: {
"style": "dark",
},
isLoading: ValueNotifier(false),
onMessageReceived: handleWebViewMessage,
),
);
}
@override
Widget build(BuildContext context) {
return ValueListenableBuilder(
valueListenable: showKinesteX,
builder: (context, isShowKinesteX, child) {
return isShowKinesteX
? SafeArea(
child: createPlanView(),
)
: Scaffold(
body: Center(
child: ElevatedButton(
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(
horizontal: 40,
vertical: 20,
),
backgroundColor: Colors.green,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
),
onPressed: () {
showKinesteX.value = true;
},
child: const Text(
'Start Plan',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
),
),
);
},
);
}
}
```
_HTML / JavaScript_
```html
KinesteX: Plan
Start KinesteX
```
_React (TypeScript)_
```tsx
import React, { useState } from 'react';
import {
IntegrationOption,
KinesteXSDK,
type IPostData,
} from 'kinestex-sdk-react-ts';
const PlanViewIntegration: React.FC = () => {
const [showKinesteX, setShowKinesteX] = useState(false);
const selectedPlan = "Full Body Fitness";
const postData: IPostData = {
key: 'YOUR_API_KEY',
userId: 'YOUR_USER_ID',
company: 'YOUR_COMPANY_NAME',
style: {
style: 'dark',
},
};
return (
{showKinesteX ? (
{
if (type === 'exit_kinestex') {
setShowKinesteX(false);
}
}}
/>
) : (
setShowKinesteX(true)}>
Start {selectedPlan}
)}
);
};
export default PlanViewIntegration;
```
#### Challenge View
Exciting Challenges: Drive Engagement and Motivation.
- **Fun and Competitive**: Quick challenges with leaderboards for friendly competition
- **Boost Activity**: Keep fitness exciting and rewarding for users
- **Easy Integration**: Add dynamic challenges effortlessly to your app
You can find exercises in our [exercise library](https://workout-view.kinestex.com/?tab=exercises), or create your own exercises in our [admin portal](https://admin.kinestex.com).
**Challenge Integration**
Display a challenge by exercise name or ID:
_Swift (iOS)_
```swift
kinestex.createChallengeView(
exercise: challengeExercise, // exercise name or ID
duration: 100, // duration of challenge in seconds
user: nil, // Optionally pass user details
showLeaderboard: true, // showLeaderboard prompts a user to enter a challenge at the end (true by default)
isLoading: $isLoading,
customParams: ["style": "dark"], // dark or light theme
onMessageReceived: { message in
switch message {
case .exit_kinestex(let data):
showKinesteX = false // dismiss the view
default:
print("Received \(message)")
break
}
}
)
```
_Kotlin (Android)_
```kotlin
KinesteXSDK.createChallengeView(
context = this,
exercise = challengeExercise, // exercise name or ID
countdown = 100, // duration of challenge in seconds
user = userDetails, // optional user details
customParams = mutableMapOf("style" to "dark"),
showLeaderboard = true, // show leaderboard at end (default true)
isLoading = viewModel.isLoading,
onMessageReceived = { message ->
when (message) {
is WebViewMessage.ExitKinestex -> finish()
else -> println("Received: $message")
}
},
permissionHandler = this
)
```
_React Native_
```jsx
// postData structure with challenge fields
const postData: IPostData = {
key: 'YOUR_API_KEY',
userId: 'YOUR_USER_ID',
company: 'YOUR_COMPANY_NAME',
exercise: 'Squats', // exercise name or ID
countdown: 100, // duration in seconds
showLeaderboard: true, // show leaderboard at end
style: {
style: 'dark',
},
};
{
if (type === 'exit_kinestex') {
setShowKinesteX(false);
}
}}
/>
```
_Flutter_
```dart
KinesteXAIFramework.createChallengeView(
isShowKinestex: showKinesteX,
exercise: challengeExercise, // exercise name or ID
countdown: 100, // seconds
showLeaderboard: true,
customParams: {"style": "dark"},
isLoading: ValueNotifier(false),
onMessageReceived: (message) {
if (message is ExitKinestex) {
setState(() => showKinesteX.value = false);
}
},
)
```
_HTML / JavaScript_
```html
// Add challenge params to postData
const postData = {
// ... all initial fields
exercise: challengeExercise, // exercise name or ID
showLeaderboard: true, // show leaderboard at end
countdown: 100, // duration in seconds
};
const srcURL = "https://ai.kinestex.com/challenge";
webView.src = srcURL;
webView.onload = () => {
sendMessage();
};
```
_React (TypeScript)_
```tsx
import { IntegrationOption, KinesteXSDK, type IPostData } from 'kinestex-sdk-react-ts';
// postData structure with challenge fields
const postData: IPostData = {
key: 'YOUR_API_KEY',
userId: 'YOUR_USER_ID',
company: 'YOUR_COMPANY_NAME',
exercise: 'Squats', // exercise name or ID
countdown: 100, // duration in seconds
showLeaderboard: true, // show leaderboard at end
style: {
style: 'dark',
},
};
{
if (type === 'exit_kinestex') {
setShowKinesteX(false);
}
}}
/>
```
**Complete Example**
Full implementation example with challenge setup:
_Swift (iOS)_
```swift
import SwiftUI
import KinesteXAIKit
struct ChallengeIntegrationView: View {
@State private var showKinesteX = false
@State private var isLoading = false
// Initialize KinesteXAIKit
// Replace with your KinesteX credentials
let kinesteXKit = KinesteXAIKit(
apiKey: "YOUR API KEY",
companyName: "YOUR COMPANY NAME",
userId: "YOUR USER ID"
)
// Challenge parameters
let challengeExercise = "Squats"
let challengeDuration = 100 // Duration in seconds
let showLeaderboardAfterChallenge = true
var body: some View {
VStack {
Text("KinesteX Challenge Integration")
.font(.title)
.padding()
Spacer()
Button(action: {
showKinesteX.toggle()
}) {
Text("Start \(challengeExercise) Challenge (\(challengeDuration)s)")
.font(.title3)
.foregroundColor(.white)
.bold()
.padding()
.frame(maxWidth: .infinity)
.background(Color.red.cornerRadius(10))
.padding(.horizontal)
}
.padding()
Spacer()
}
.fullScreenCover(isPresented: $showKinesteX) {
kinesteXKit.createChallengeView(
exercise: challengeExercise,
duration: challengeDuration,
showLeaderboard: showLeaderboardAfterChallenge,
user: nil,
isLoading: $isLoading,
customParams: ["style": "dark"],
onMessageReceived: { message in
switch message {
case .exit_kinestex(_):
showKinesteX = false
default:
print("Message received: \(message)")
}
}
)
}
}
}
#Preview {
ChallengeIntegrationView()
}
```
_Kotlin (Android)_
```kotlin
import android.os.Bundle
import android.widget.Toast
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import com.kinestex.kinestexsdkkotlin.GenericWebView
import com.kinestex.kinestexsdkkotlin.KinesteXSDK
import com.kinestex.kinestexsdkkotlin.PermissionHandler
import com.kinestex.kinestexsdkkotlin.WebViewMessage
import kotlinx.coroutines.flow.MutableStateFlow
class ChallengeViewActivity : ComponentActivity(), PermissionHandler {
private val viewModel = ChallengeViewModel()
// OPTIONAL: UserDetails to customize workout intensity and calorie estimation
// Note: User details are only used on-device during the session
private val userDetails = UserDetails(
age = 30,
height = 180,
weight = 75,
gender = Gender.MALE,
lifestyle = Lifestyle.ACTIVE
)
// Custom data for the WebView
private val data = mutableMapOf()
// Store reference to the KinesteX WebView
private var kinesteXWebView: GenericWebView? = null
// Register permission launcher
private val requestPermissionLauncher = registerForActivityResult(
ActivityResultContracts.RequestPermission()
) { isGranted: Boolean ->
// Pass permission result to KinesteX webview
kinesteXWebView?.handlePermissionResult(isGranted)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
data["style"] = "dark"
setContent {
val webView = KinesteXSDK.createChallengeView(
context = this,
exercise = "Squats",
countdown = 60,
user = userDetails,
customParams = data,
showLeaderboard = true,
isLoading = viewModel.isLoading,
onMessageReceived = ::handleWebViewMessage,
permissionHandler = this
) as GenericWebView
kinesteXWebView = webView
webView.Render()
}
}
private fun handleWebViewMessage(message: WebViewMessage) {
when (message) {
is WebViewMessage.ExitKinestex -> finish()
is WebViewMessage.KinestexLaunched -> viewModel.isLoading.value = false
else -> Toast.makeText(this, "Received: $message", Toast.LENGTH_SHORT).show()
}
}
// When request is sent, display system dialog for camera access
override fun requestCameraPermission() {
requestPermissionLauncher.launch(Manifest.permission.CAMERA)
}
}
class ChallengeViewModel {
val isLoading = MutableStateFlow(true)
}
```
_React Native_
```jsx
import React, { useState } from 'react';
import { View, Button, StyleSheet } from 'react-native';
import KinestexSDK from 'kinestex-sdk-react-native';
import { IntegrationOption, IPostData } from 'kinestex-sdk-react-native/src/types';
const ChallengeIntegration = () => {
const [showKinesteX, setShowKinesteX] = useState(false);
const challengeExercise = "Squats";
const challengeDuration = 100;
// Include challenge fields in postData
const postData: IPostData = {
key: 'YOUR_API_KEY',
userId: 'YOUR_USER_ID',
company: 'YOUR_COMPANY_NAME',
exercise: challengeExercise, // exercise name or ID
countdown: challengeDuration, // duration in seconds
showLeaderboard: true, // show leaderboard at end
style: {
style: 'dark',
},
};
return (
{showKinesteX ? (
{
if (type === 'exit_kinestex') {
setShowKinesteX(false);
}
}}
/>
) : (
setShowKinesteX(true)}
/>
)}
);
};
const styles = StyleSheet.create({
container: { flex: 1 }
});
export default ChallengeIntegration;
```
_Flutter_
```dart
import 'package:flutter/material.dart';
import 'package:kinestex_sdk_flutter/kinestex_sdk.dart';
import 'package:permission_handler/permission_handler.dart';
Future main() async {
WidgetsFlutterBinding.ensureInitialized();
await KinesteXAIFramework.initialize(
apiKey: "your_api_key",
companyName: "your_company_name",
userId: "your_user_id",
);
runApp(const MyApp());
}
class MyApp extends StatefulWidget {
const MyApp({super.key});
@override
State createState() => _MyAppState();
}
class _MyAppState extends State {
@override
void dispose() {
disposeKinesteXAIFramework();
super.dispose();
}
Future disposeKinesteXAIFramework() async {
await KinesteXAIFramework.dispose();
}
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'KinesteX Challenge',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const MyHomePage(),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key});
@override
State createState() => _MyHomePageState();
}
class _MyHomePageState extends State {
ValueNotifier showKinesteX = ValueNotifier(false);
String challengeExercise = "Squats";
int challengeDuration = 100;
@override
void initState() {
super.initState();
_checkCameraPermission();
}
void _checkCameraPermission() async {
if (await Permission.camera.request() != PermissionStatus.granted) {
_showCameraAccessDeniedAlert();
}
}
void _showCameraAccessDeniedAlert() {
showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: const Text("Camera Permission Denied"),
content: const Text("Camera access is required for this app to function properly."),
actions: [
TextButton(
child: const Text("OK"),
onPressed: () => Navigator.of(context).pop(),
),
],
);
},
);
}
void handleWebViewMessage(WebViewMessage message) {
if (message is ExitKinestex) {
setState(() {
showKinesteX.value = false;
});
}
}
Widget createChallengeView() {
return Center(
child: KinesteXAIFramework.createChallengeView(
isShowKinestex: showKinesteX,
exercise: challengeExercise,
countdown: challengeDuration,
showLeaderboard: true,
customParams: {
"style": "dark",
},
isLoading: ValueNotifier(false),
onMessageReceived: handleWebViewMessage,
),
);
}
@override
Widget build(BuildContext context) {
return ValueListenableBuilder(
valueListenable: showKinesteX,
builder: (context, isShowKinesteX, child) {
return isShowKinesteX
? SafeArea(
child: createChallengeView(),
)
: Scaffold(
body: Center(
child: ElevatedButton(
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(
horizontal: 40,
vertical: 20,
),
backgroundColor: Colors.green,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
),
onPressed: () {
showKinesteX.value = true;
},
child: Text(
'Start $challengeExercise Challenge (${challengeDuration}s)',
style: const TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
),
),
);
},
);
}
}
```
_HTML / JavaScript_
```html
KinesteX: Challenge
Start KinesteX
```
_React (TypeScript)_
```tsx
import React, { useState } from 'react';
import {
IntegrationOption,
KinesteXSDK,
type IPostData,
} from 'kinestex-sdk-react-ts';
const ChallengeIntegration: React.FC = () => {
const [showKinesteX, setShowKinesteX] = useState(false);
const challengeExercise = "Squats";
const challengeDuration = 100;
// Include challenge fields in postData
const postData: IPostData = {
key: 'YOUR_API_KEY',
userId: 'YOUR_USER_ID',
company: 'YOUR_COMPANY_NAME',
exercise: challengeExercise, // exercise name or ID
countdown: challengeDuration, // duration in seconds
showLeaderboard: true, // show leaderboard at end
style: {
style: 'dark',
},
};
return (
{showKinesteX ? (
{
if (type === 'exit_kinestex') {
setShowKinesteX(false);
}
}}
/>
) : (
setShowKinesteX(true)}>
Start {challengeExercise} Challenge ({challengeDuration}s)
)}
);
};
export default ChallengeIntegration;
```
#### Leaderboard View (Challenge)
Ready-made Leaderboard: Boost User Engagement and Motivation.
- **Adaptive Design**: The leaderboard automatically adapts to your KinesteX UI and can be fully customized in the admin dashboard
- **Real-time Updates**: Whenever a new ranking is available, the leaderboard automatically refreshes to show the latest standings
**Leaderboard Integration**
Display the leaderboard for a specific exercise:
_Swift (iOS)_
```swift
kinestex.createLeaderboardView(
exercise: "Squats", // Specify the exercise id or title
username: "", // if you know the username: highlight the user by specifying their username
isLoading: $isLoading,
customParams: [
"style": "dark", // light or dark theme (default is dark)
"isHideHeaderMain": true // OPTIONAL: hide the exit button from the leaderboard
],
onMessageReceived: { message in
switch message {
case .exit_kinestex(_):
showKinesteX = false
default:
break
}
}
)
```
_Kotlin (Android)_
```kotlin
KinesteXSDK.createLeaderboardView(
context = this,
exercise = "Squats", // exercise name or ID
username = "John", // highlight username in leaderboard if known
customParams = mutableMapOf(
"style" to "dark",
"isHideHeaderMain" to true // hide exit button
),
isLoading = viewModel.isLoading,
onMessageReceived = { message ->
when (message) {
is WebViewMessage.ExitKinestex -> finish()
else -> println("Received: $message")
}
},
permissionHandler = this
)
```
_React Native_
```jsx
// postData structure
const postData: IPostData = {
key: 'YOUR_API_KEY',
userId: 'YOUR_USER_ID',
company: 'YOUR_COMPANY_NAME',
exercise: 'Squats', // exercise name or ID
style: {
style: 'dark',
},
};
{
if (type === 'exit_kinestex') {
setShowKinesteX(false);
}
}}
/>
```
_Flutter_
```dart
KinesteXAIFramework.createLeaderboardView(
isShowKinestex: showKinesteX,
exercise: "Squats",
username: "", // highlight user if known
customParams: {
"style": "dark",
"isHideHeaderMain": true, // hide exit button
},
isLoading: ValueNotifier(false),
onMessageReceived: (message) {
if (message is ExitKinestex) {
setState(() => showKinesteX.value = false);
}
},
)
```
_HTML / JavaScript_
```html
// Add to postData
const postData = {
// ... all initial fields
exercise: "Squats", // exercise name or ID
};
const userId = "unique-user-id"; // OPTIONAL: userId to highlight in leaderboard
const srcURL = `https://ai.kinestex.com/leaderboard/?userId=${userId}`;
webView.src = srcURL;
webView.onload = () => {
sendMessage();
};
```
_React (TypeScript)_
```tsx
import { IntegrationOption, KinesteXSDK, type IPostData } from 'kinestex-sdk-react-ts';
// postData structure
const postData: IPostData = {
key: 'YOUR_API_KEY',
userId: 'YOUR_USER_ID',
company: 'YOUR_COMPANY_NAME',
exercise: 'Squats', // exercise ID or name
style: {
style: 'dark',
},
};
{
if (type === 'exit_kinestex') {
setShowKinesteX(false);
}
}}
/>
```
#### AI Experiences
AI-powered movement-based games and clinical assessments with real-time motion tracking.
**AI Games:**
| Game | Exercise ID |
|------|-------------|
| Balloon Pop | balloonpop |
| Color Chase | colorchase |
| Alien Squat Shooter | aliensquatshooter |
**Clinical Assessments:**
| Assessment | Exercise ID |
|------------|-------------|
| Timed Up and Go (TUG) | tug |
| Gait Speed Test | gaitspeedtest |
| Sit-to-Stand | sittostand |
| Functional Reach Test | functionalreachtest |
| Single Leg Stance Test | singlelegstancetest |
| Five Times Sit-to-Stand | fivetimessts |
| Side-by-Side Stand | sidebysidestand |
| Semi-Tandem Stand | semitandemstand |
| Full Tandem Stand | fulltandem |
| Shoulder Range of Motion | romshoulder |
**Experience View**
_Swift (iOS)_
```swift
// Launch an AI game or balance assessment
// Pass the exercise ID from the tables above
kinestex.createExperienceView(
experience: "assessment", // experience type
exercise: "balloonpop", // exercise ID from table
user: nil,
isLoading: $isLoading,
customParams: ["style": "dark"],
onMessageReceived: { message in
switch message {
case .exit_kinestex(_):
showKinesteX = false
default:
break
}
}
)
```
_Kotlin (Android)_
```kotlin
// Launch an AI game or balance assessment
val data = mutableMapOf()
data["style"] = "dark"
data["exercise"] = "balloonpop" // exercise ID from table
KinesteXSDK.createExperiencesView(
context = this,
experienceName = "assessment",
user = userDetails, // optional user details
customParams = data,
isLoading = viewModel.isLoading,
onMessageReceived = { message ->
if (message is WebViewMessage.ExitKinestex) finish()
},
permissionHandler = this
)
```
_React Native_
```jsx
// postData structure
const postData: IPostData = {
key: 'YOUR_API_KEY',
userId: 'YOUR_USER_ID',
company: 'YOUR_COMPANY_NAME',
exercise: 'balloonpop', // exercise ID from table
style: {
style: 'dark',
},
};
```
_Flutter_
```dart
KinesteXAIFramework.createExperienceView(
isShowKinestex: showKinesteX,
experience: "assessment",
customParams: {
"style": "dark",
"exercise": "balloonpop", // exercise ID from table
},
isLoading: ValueNotifier(false),
onMessageReceived: (message) {
handleWebViewMessage(message);
},
)
```
_HTML / JavaScript_
```html
const postData = {
// ... all initial fields
exercise: "balloonpop", // exercise ID from table
};
const srcURL = "https://ai.kinestex.com/experiences/assessment";
webView.src = srcURL;
webView.onload = () => {
sendMessage();
};
```
_React (TypeScript)_
```tsx
import { IntegrationOption, KinesteXSDK, type IPostData } from 'kinestex-sdk-react-ts';
// postData structure
const postData: IPostData = {
key: 'YOUR_API_KEY',
userId: 'YOUR_USER_ID',
company: 'YOUR_COMPANY_NAME',
exercise: 'balloonpop', // exercise ID from table
style: {
style: 'dark',
},
};
```
**Complete Example**
_Swift (iOS)_
```swift
import SwiftUI
import KinesteXAIKit
struct ExperienceIntegrationView: View {
@State private var showKinesteX = false
@State private var isLoading = false
// Initialize KinesteXAIKit
// Replace with your KinesteX credentials
let kinesteXKit = KinesteXAIKit(
apiKey: "YOUR API KEY",
companyName: "YOUR COMPANY NAME",
userId: "YOUR USER ID"
)
// Parameters for the experience
let experienceName = "assessment" // Name of the AI experience
let experienceExercise = "balloonpop" // Exercise ID from table
var body: some View {
VStack {
Text("KinesteX Experience Integration")
.font(.title)
.padding()
Spacer()
Button(action: {
showKinesteX.toggle()
}) {
Text("Start '\(experienceName.capitalized)' Experience")
.font(.title3)
.foregroundColor(.white)
.bold()
.padding()
.frame(maxWidth: .infinity)
.background(Color.teal.cornerRadius(10))
.padding(.horizontal)
}
.padding()
Spacer()
}
.fullScreenCover(isPresented: $showKinesteX) {
kinesteXKit.createExperienceView(
experience: experienceName,
exercise: experienceExercise,
user: nil,
isLoading: $isLoading,
customParams: ["style": "dark"],
onMessageReceived: { message in
switch message {
case .exit_kinestex(_):
showKinesteX = false
default:
print("Message received: \(message)")
}
}
)
}
}
}
#Preview {
ExperienceIntegrationView()
}
```
_Kotlin (Android)_
```kotlin
import android.os.Bundle
import android.widget.Toast
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import com.kinestex.kinestexsdkkotlin.GenericWebView
import com.kinestex.kinestexsdkkotlin.KinesteXSDK
import com.kinestex.kinestexsdkkotlin.PermissionHandler
import com.kinestex.kinestexsdkkotlin.WebViewMessage
import kotlinx.coroutines.flow.MutableStateFlow
class ExperienceActivity : ComponentActivity(), PermissionHandler {
private val viewModel = ExperienceViewModel()
// OPTIONAL: UserDetails to customize workout intensity and calorie estimation
// Note: User details are only used on-device during the session
private val userDetails = UserDetails(
age = 30,
height = 180,
weight = 75,
gender = Gender.MALE,
lifestyle = Lifestyle.ACTIVE
)
// Custom data for the WebView
private val data = mutableMapOf()
// Store reference to the KinesteX WebView
private var kinesteXWebView: GenericWebView? = null
// Register permission launcher
private val requestPermissionLauncher = registerForActivityResult(
ActivityResultContracts.RequestPermission()
) { isGranted: Boolean ->
// Pass permission result to KinesteX webview
kinesteXWebView?.handlePermissionResult(isGranted)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
data["style"] = "dark"
data["exercise"] = "balloonpop" // AI game or balance assessment ID
setContent {
val webView = KinesteXSDK.createExperiencesView(
context = this,
experienceName = "assessment",
user = userDetails,
customParams = data,
isLoading = viewModel.isLoading,
onMessageReceived = ::handleWebViewMessage,
permissionHandler = this
) as GenericWebView
kinesteXWebView = webView
webView.Render()
}
}
private fun handleWebViewMessage(message: WebViewMessage) {
when (message) {
is WebViewMessage.ExitKinestex -> finish()
is WebViewMessage.KinestexLaunched -> viewModel.isLoading.value = false
else -> Toast.makeText(this, "Received: $message", Toast.LENGTH_SHORT).show()
}
}
// When request is sent, display system dialog for camera access
override fun requestCameraPermission() {
requestPermissionLauncher.launch(Manifest.permission.CAMERA)
}
}
class ExperienceViewModel {
val isLoading = MutableStateFlow(true)
}
```
_React Native_
```jsx
import React, { useState } from 'react';
import { View, Button } from 'react-native';
import KinestexSDK from 'kinestex-sdk-react-native';
import { IntegrationOption, IPostData } from 'kinestex-sdk-react-native/src/types';
export default function ExperienceScreen() {
const [showKinesteX, setShowKinesteX] = useState(false);
const postData: IPostData = {
key: 'YOUR_API_KEY',
userId: 'YOUR_USER_ID',
company: 'YOUR_COMPANY_NAME',
exercise: 'balloonpop', // AI game or balance assessment ID
style: {
style: 'dark',
},
};
const handleMessage = (type: string, data: any) => {
if (type === 'exit_kinestex') {
setShowKinesteX(false);
}
};
if (showKinesteX) {
return (
);
}
return (
setShowKinesteX(true)} />
);
}
```
_Flutter_
```dart
import 'package:flutter/material.dart';
import 'package:kinestex_sdk_flutter/kinestex_sdk.dart';
import 'package:permission_handler/permission_handler.dart';
Future main() async {
WidgetsFlutterBinding.ensureInitialized();
await KinesteXAIFramework.initialize(
apiKey: "your_api_key",
companyName: "your_company_name",
userId: "your_user_id",
);
runApp(const MyApp());
}
class MyApp extends StatefulWidget {
const MyApp({super.key});
@override
State createState() => _MyAppState();
}
class _MyAppState extends State {
@override
void dispose() {
disposeKinesteXAIFramework();
super.dispose();
}
Future disposeKinesteXAIFramework() async {
await KinesteXAIFramework.dispose();
}
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'KinesteX Experience',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const MyHomePage(),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key});
@override
State createState() => _MyHomePageState();
}
class _MyHomePageState extends State {
ValueNotifier showKinesteX = ValueNotifier(false);
@override
void initState() {
super.initState();
_checkCameraPermission();
}
void _checkCameraPermission() async {
if (await Permission.camera.request() != PermissionStatus.granted) {
_showCameraAccessDeniedAlert();
}
}
void _showCameraAccessDeniedAlert() {
showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: const Text("Camera Permission Denied"),
content: const Text("Camera access is required for this app to function properly."),
actions: [
TextButton(
child: const Text("OK"),
onPressed: () => Navigator.of(context).pop(),
),
],
);
},
);
}
void handleWebViewMessage(WebViewMessage message) {
if (message is ExitKinestex) {
setState(() {
showKinesteX.value = false;
});
}
}
Widget createExperienceView() {
return Center(
child: KinesteXAIFramework.createExperienceView(
isShowKinestex: showKinesteX,
experience: "assessment",
customParams: {
"style": "dark",
"exercise": "balloonpop",
},
isLoading: ValueNotifier(false),
onMessageReceived: handleWebViewMessage,
),
);
}
@override
Widget build(BuildContext context) {
return ValueListenableBuilder(
valueListenable: showKinesteX,
builder: (context, isShowKinesteX, child) {
return isShowKinesteX
? SafeArea(
child: createExperienceView(),
)
: Scaffold(
body: Center(
child: ElevatedButton(
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(
horizontal: 40,
vertical: 20,
),
backgroundColor: Colors.green,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
),
onPressed: () {
showKinesteX.value = true;
},
child: const Text(
'Start AI Experience',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
),
),
);
},
);
}
}
```
_HTML / JavaScript_
```html
KinesteX: AI Experience
Start KinesteX
```
_React (TypeScript)_
```tsx
import React, { useState } from 'react';
import {
IntegrationOption,
KinesteXSDK,
type IPostData,
} from 'kinestex-sdk-react-ts';
export default function ExperienceScreen() {
const [showKinesteX, setShowKinesteX] = useState(false);
const postData: IPostData = {
key: 'YOUR_API_KEY',
userId: 'YOUR_USER_ID',
company: 'YOUR_COMPANY_NAME',
exercise: 'balloonpop', // AI game or balance assessment ID
style: {
style: 'dark',
},
};
const handleMessage = (type: string, data: Record) => {
if (type === 'exit_kinestex') {
setShowKinesteX(false);
}
};
if (showKinesteX) {
return (
);
}
return setShowKinesteX(true)}>Start AI Experience ;
}
```
#### Personalized Plan View
AI-Generated Personalized Workout Plans.
- **Personalized**: Tailored to height, weight, age, gender, activity level, and fitness assessment results
- **Goal-Oriented**: Supports strength, flexibility, and wellness goals
- **Seamless Experience**: From recommendations to real-time feedback
- **Customizable**: Brand-aligned app design
- **Quick Integration**: Easy setup for advanced fitness solutions
**Personalized Plan Integration**
_Swift (iOS)_
```swift
kinestex.createPersonalizedPlanView(
user: nil, // OPTIONAL: provide user details
isLoading: $isLoading,
customParams: ["style": "dark"], // dark or light theme (customizable in admin portal)
onMessageReceived: { message in
switch message {
case .exit_kinestex(_):
showKinesteX = false // dismiss the view
default:
print("Received \(message)")
break
}
}
)
```
_Kotlin (Android)_
```kotlin
KinesteXSDK.createPersonalizedPlanView(
context = this,
user = userDetails, // optional user details
customParams = mutableMapOf("style" to "dark"), // customizable in admin portal
isLoading = viewModel.isLoading,
onMessageReceived = { message ->
when (message) {
is WebViewMessage.ExitKinestex -> finish()
else -> println("Received: $message")
}
},
permissionHandler = this
)
```
_React Native_
```jsx
// postData structure
const postData: IPostData = {
key: 'YOUR_API_KEY',
userId: 'YOUR_USER_ID',
company: 'YOUR_COMPANY_NAME',
style: {
style: 'dark',
},
};
```
_Flutter_
```dart
KinesteXAIFramework.createPersonalizedPlanView(
isShowKinestex: showKinesteX,
customParams: {"style": "dark"},
isLoading: ValueNotifier(false),
onMessageReceived: (message) {
handleWebViewMessage(message);
},
)
```
_HTML / JavaScript_
```html
KinesteX: Personalized Plan
Start Personalized Plan
```
_React (TypeScript)_
```tsx
import { IntegrationOption, KinesteXSDK, type IPostData } from 'kinestex-sdk-react-ts';
// postData structure
const postData: IPostData = {
key: 'YOUR_API_KEY',
userId: 'YOUR_USER_ID',
company: 'YOUR_COMPANY_NAME',
style: {
style: 'dark',
},
};
```
#### AI Trainer Chat
Conversational AI personal trainer — onboarding with an in-app fitness assessment, personalized workout generation, post-workout check-ins, and next-session scheduling in a fully white-labeled chat UI.
Full guide: [AI Trainer Chat](/docs/ai-trainer-chat)
#### Admin Workout Editor
Embedded view for creating and managing workouts and exercises. As users interact with the editor, your application will receive events that you can handle to trigger custom logic.
**Available for Flutter and Swift (iOS).**
**Custom Query Options (Flutter):**
| Option | Description |
|--------|-------------|
| hidePlansTab | Hide the plans tab in the dashboard |
| tab | Default tab: "workouts", "exercises", or "plans" |
| isSelectableMenu | Show select button on cards, triggers `_selected` events |
**Available Events:**
*General Events:*
- `kinestex_loaded` - Application fully loaded
- `kinestex_launched` - Successful authentication
- `error_occurred` - Authentication error
*Exercise Events:*
- `exercise_opened` - Exercise detail page opened
- `exercise_selection_opened` - Exercise list page opened
- `exercise_selected` - Exercise selected from menu
- `exercise_saved` - Exercise created/updated
- `exercise_removed` - Exercise removed from workout
*Workout Events:*
- `workout_opened` - Workout detail page opened
- `workout_selection_opened` - Workout list page opened
- `workout_selected` - Workout selected from menu
- `workout_saved` - Workout created/updated
*Plan Events:*
- `plan_opened` - Plan detail page opened
- `plan_selection_opened` - Plan list page opened
- `plan_selected` - Plan selected from menu
- `plan_saved` - Plan created/updated
**Swift Parameters:**
| Parameter | Description |
|-----------|-------------|
| organization (required) | Your organization identifier |
| contentType (optional) | `.workout`, `.plan`, or `.exercise` |
| contentId (optional) | Specific content ID to edit |
| customQueries (optional) | Additional query parameters |
**Admin Workout Editor Integration**
Create the admin workout editor view for managing workouts and exercises:
_Swift (iOS)_
```swift
// Open main admin dashboard
kinestex.createAdminWorkoutEditor(
organization: "YourOrg",
isLoading: $isLoading,
onMessageReceived: { message in
switch message {
case .exit_kinestex(_):
showEditor = false
default:
print("Message received: \(message)")
}
}
)
// Open specific workout for editing
kinestex.createAdminWorkoutEditor(
organization: "YourOrg",
contentType: .workout,
contentId: "workout123",
isLoading: $isLoading,
onMessageReceived: { message in /* handle messages */ }
)
// Open specific exercise for editing
kinestex.createAdminWorkoutEditor(
organization: "YourOrg",
contentType: .exercise,
contentId: "exercise456",
customQueries: ["language": "en"],
isLoading: $isLoading,
onMessageReceived: { message in /* handle messages */ }
)
```
_Flutter_
```dart
KinesteXAIFramework.createAdminWorkoutEditor(
// Use an organization name to differentiate between different orgs.
// If you don't plan to use multiple orgs, you can use your company name.
organization: "your_organization_name",
isShowKinestex: showKinesteX,
// OPTIONAL: show/hide content on the admin dashboard
customQueries: {
"hidePlansTab": true, // will hide the plans tabs in the dashboard
"tab": "workouts", // will default to workouts tab. Options: "exercises", "plans"
"isSelectableMenu": true // show select Button, triggers _selected events
},
isLoading: ValueNotifier(false),
onMessageReceived: (message) {
handleWebViewMessage(message);
},
)
```
**Complete Example**
Full implementation with event handling:
_Swift (iOS)_
```swift
import SwiftUI
import KinesteXAIKit
struct AdminEditorView: View {
@State private var showEditor = false
@State private var isLoading = false
// Initialize KinesteXAIKit with your credentials
let kinestex = KinesteXAIKit(
apiKey: "YOUR_API_KEY",
companyName: "YOUR_COMPANY_NAME",
userId: "YOUR_USER_ID"
)
var body: some View {
VStack {
Text("Admin Editor")
.font(.title)
.padding()
Spacer()
Button(action: {
showEditor.toggle()
}) {
Text("Open Admin Editor")
.font(.title3)
.foregroundColor(.white)
.bold()
.padding()
.frame(maxWidth: .infinity)
.background(Color.blue.cornerRadius(10))
.padding(.horizontal)
}
Spacer()
}
.fullScreenCover(isPresented: $showEditor) {
kinestex.createAdminWorkoutEditor(
organization: "YourOrg",
contentType: nil,
contentId: nil,
customQueries: nil,
isLoading: $isLoading,
onMessageReceived: { message in
switch message {
case .exit_kinestex(_):
showEditor = false
case .error_occurred(let data):
print("Error: \(data)")
default:
print("Message received: \(message)")
}
}
)
}
}
}
#Preview {
AdminEditorView()
}
```
_Flutter_
```dart
import 'package:flutter/material.dart';
import 'package:kinestex_sdk_flutter/kinestex_sdk.dart';
import 'package:permission_handler/permission_handler.dart';
Future main() async {
WidgetsFlutterBinding.ensureInitialized();
await KinesteXAIFramework.initialize(
apiKey: "your_api_key",
companyName: "your_company_name",
userId: "your_user_id",
);
runApp(const MyApp());
}
class MyApp extends StatefulWidget {
const MyApp({super.key});
@override
State createState() => _MyAppState();
}
class _MyAppState extends State {
@override
void dispose() {
disposeKinesteXAIFramework();
super.dispose();
}
Future disposeKinesteXAIFramework() async {
await KinesteXAIFramework.dispose();
}
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'KinesteX Admin Editor',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const MyHomePage(),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key});
@override
State createState() => _MyHomePageState();
}
class _MyHomePageState extends State {
ValueNotifier showKinesteX = ValueNotifier(false);
@override
void initState() {
super.initState();
_checkCameraPermission();
}
void _checkCameraPermission() async {
if (await Permission.camera.request() != PermissionStatus.granted) {
_showCameraAccessDeniedAlert();
}
}
void _showCameraAccessDeniedAlert() {
showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: const Text("Camera Permission Denied"),
content: const Text("Camera access is required for this app to function properly."),
actions: [
TextButton(
child: const Text("OK"),
onPressed: () => Navigator.of(context).pop(),
),
],
);
},
);
}
void handleWebViewMessage(WebViewMessage message) {
if (message is ExitKinestex) {
setState(() {
showKinesteX.value = false;
});
} else if (message is ErrorOccurred) {
print('Error: ${message.data["error_message"]}');
} else {
// Admin editor events arrive as generic messages — check data['type']
switch (message.data['type']) {
case 'workout_saved':
print('Workout saved: ${message.data["workout_id"]}');
break;
case 'exercise_saved':
print('Exercise saved: ${message.data["exercise_id"]}');
break;
case 'workout_selected':
print('Workout selected: ${message.data["workout_title"]}');
break;
case 'exercise_selected':
print('Exercise selected: ${message.data["exercise_title"]}');
break;
case 'plan_saved':
print('Plan saved: ${message.data["plan_id"]}');
break;
}
}
}
Widget createAdminWorkoutEditorView() {
return Center(
child: KinesteXAIFramework.createAdminWorkoutEditor(
organization: "your_organization_name",
isShowKinestex: showKinesteX,
customQueries: {
"hidePlansTab": false,
"tab": "workouts",
"isSelectableMenu": true,
},
isLoading: ValueNotifier(false),
onMessageReceived: handleWebViewMessage,
),
);
}
@override
Widget build(BuildContext context) {
return ValueListenableBuilder(
valueListenable: showKinesteX,
builder: (context, isShowKinesteX, child) {
return isShowKinesteX
? SafeArea(
child: createAdminWorkoutEditorView(),
)
: Scaffold(
body: Center(
child: ElevatedButton(
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(
horizontal: 40,
vertical: 20,
),
backgroundColor: Colors.green,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
),
onPressed: () {
showKinesteX.value = true;
},
child: const Text(
'Open Admin Workout Editor',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
),
),
);
},
);
}
}
```
### Custom Integration
Build everything yourself with full control over UI/UX. Use our Camera Component for real-time motion analysis and create custom workout sequences.
#### Custom Workout
Create and execute personalized workout sequences with custom exercises, repetitions, durations, and rest periods. Define your own workout flow with full control over exercise order and timing.
**How it works:**
1. Initialize the SDK with custom workout integration option
2. Pass your custom workout exercises array
3. Wait for `all_resources_loaded` message
4. Send `workout_activity_action: start` to begin
**WorkoutSequenceExercise Parameters:**
- `exerciseId` - Exercise ID from KinesteX API or admin panel
- `reps` - Number of repetitions
- `duration` - Duration in seconds (null = unlimited time for reps)
- `includeRestPeriod` - Include rest period before exercise
- `restDuration` - Rest duration in seconds
**Tip:** To create sets, duplicate the same exercise in the array multiple times.
**Custom Workout Setup**
_Swift (iOS)_
```swift
@State var workoutAction: [String: Any]? = nil
let exercises = [
WorkoutSequenceExercise(
exerciseId: "jz73VFlUyZ9nyd64OjRb",
reps: 15,
duration: nil,
includeRestPeriod: true,
restDuration: 20
),
WorkoutSequenceExercise(
exerciseId: "ZVMeLsaXQ9Tzr5JYXg29",
reps: 10,
duration: 30,
includeRestPeriod: true,
restDuration: 15
),
WorkoutSequenceExercise(
exerciseId: "gJGOiZhCvJrhEP7sTy78",
reps: 20,
duration: nil,
includeRestPeriod: false,
restDuration: 0
)
]
kinestex.createCustomWorkoutView(
exercises: exercises,
user: userDetails,
style: nil,
isLoading: $isLoading,
workoutAction: $workoutAction,
onMessageReceived: { message in
if case .custom_type(let value) = message,
let type = value["type"] as? String,
type == "all_resources_loaded" {
// Start workout when resources are ready
workoutAction = ["workout_activity_action": "start"]
}
}
)
```
_Kotlin (Android)_
```kotlin
// Define custom workout exercises
val customExercises = listOf(
WorkoutSequenceExercise(
exerciseId = "jz73VFlUyZ9nyd64OjRb",
reps = 15,
duration = null,
includeRestPeriod = true,
restDuration = 20
),
WorkoutSequenceExercise(
exerciseId = "ZVMeLsaXQ9Tzr5JYXg29",
reps = 10,
duration = 30,
includeRestPeriod = true,
restDuration = 15
)
)
KinesteXSDK.createCustomWorkoutView(
context = this,
customWorkouts = customExercises,
user = userDetails, // optional user details
isLoading = viewModel.isLoading,
customParams = mutableMapOf("style" to "dark"),
onMessageReceived = { message ->
if (message is WebViewMessage.AllResourcesLoaded) {
// Start workout when ready
}
},
permissionHandler = this
)
```
_React Native_
```jsx
// Step 1: Define custom workout exercises
const customWorkoutExercises: WorkoutSequenceExercise[] = [
{
exerciseId: "jz73VFlUyZ9nyd64OjRb", // exercise id from kinestex api
reps: 15, // number of reps
duration: null, // null = unlimited time for reps
includeRestPeriod: true, // include rest before exercise
restDuration: 20, // rest duration in seconds
},
{
exerciseId: "ZVMeLsaXQ9Tzr5JYXg29",
reps: 10,
duration: 30,
includeRestPeriod: true,
restDuration: 15,
},
// Duplicate exercise to create a set
{
exerciseId: "ZVMeLsaXQ9Tzr5JYXg29",
reps: 10,
duration: 30,
includeRestPeriod: true,
restDuration: 15,
},
];
// Step 2: Configure postData with customWorkoutExercises
const postData: IPostData = {
key: 'YOUR_API_KEY',
userId: 'YOUR_USER_ID',
company: 'YOUR_COMPANY_NAME',
customWorkoutExercises: customWorkoutExercises, // pass exercises in postData
style: {
style: 'dark',
},
};
// Step 3: Handle SDK messages
const handleMessage = (type: string, data: any) => {
switch (type) {
case 'all_resources_loaded':
// SDK is ready - show the view and start workout
setAllResourcesLoaded(true);
kinestexSDKRef.current?.sendAction("workout_activity_action", "start");
break;
case 'workout_exit_request':
setAllResourcesLoaded(false);
setShowKinestex(false);
break;
case 'exit_kinestex':
setAllResourcesLoaded(false);
break;
}
};
// Step 4: Render with conditional visibility
{showKinestex && (
)}
```
_Flutter_
```dart
// Define custom workout exercises
final customWorkoutExercises = [
WorkoutSequenceExercise(
exerciseId: "jz73VFlUyZ9nyd64OjRb",
reps: 15,
duration: null,
includeRestPeriod: true,
restDuration: 20,
),
WorkoutSequenceExercise(
exerciseId: "ZVMeLsaXQ9Tzr5JYXg29",
reps: 10,
duration: 30,
includeRestPeriod: true,
restDuration: 15,
),
];
KinesteXAIFramework.createCustomWorkoutView(
isShowKinestex: showKinesteX,
customWorkouts: customWorkoutExercises,
customParams: {"style": "dark"},
isLoading: ValueNotifier(false),
onMessageReceived: (message) {
// Custom events arrive as CustomType; the event name is in data['type']
if (message is CustomType && message.data['type'] == 'all_resources_loaded') {
// Start workout when ready
}
},
)
```
_HTML / JavaScript_
```html
// Define custom workout exercises
const customWorkoutExercises = [
{
exerciseId: "jz73VFlUyZ9nyd64OjRb",
reps: 15,
duration: null,
includeRestPeriod: true,
restDuration: 20,
},
{
exerciseId: "ZVMeLsaXQ9Tzr5JYXg29",
reps: 10,
duration: 30,
includeRestPeriod: true,
restDuration: 15,
},
];
const config = {
...postData,
customWorkoutExercises: customWorkoutExercises,
};
const srcURL = "https://ai.kinestex.com/custom-workout";
webView.src = srcURL;
webView.onload = () => {
sendMessage();
};
// Listen for ready signal
window.addEventListener("message", (e) => {
const msg = JSON.parse(e.data);
if (msg.type === 'all_resources_loaded') {
// Start workout
webView.contentWindow.postMessage(
{ action: "workout_activity_action", value: "start" },
srcURL
);
}
});
```
_React (TypeScript)_
```tsx
import { useRef, useState } from 'react';
import {
IntegrationOption,
KinesteXSDK,
type IPostData,
type KinesteXSDKCamera,
} from 'kinestex-sdk-react-ts';
// Define custom workout exercises
interface WorkoutSequenceExercise {
exerciseId: string;
reps: number | null;
duration: number | null;
includeRestPeriod: boolean;
restDuration: number;
}
const customWorkoutExercises: WorkoutSequenceExercise[] = [
{
exerciseId: "jz73VFlUyZ9nyd64OjRb",
reps: 15,
duration: null,
includeRestPeriod: true,
restDuration: 20,
},
];
// postData structure with customWorkoutExercises
const postData: IPostData = {
key: 'YOUR_API_KEY',
userId: 'YOUR_USER_ID',
company: 'YOUR_COMPANY_NAME',
customWorkoutExercises: customWorkoutExercises, // pass exercises in postData
style: {
style: 'dark',
},
};
const ref = useRef(null);
{
if (type === 'all_resources_loaded') {
ref.current?.sendAction("workout_activity_action", "start");
}
}}
/>
```
##### Complete Example
Full implementation with state management, loading indicators, and proper message handling.
**Complete Implementation**
_Swift (iOS)_
```swift
import SwiftUI
import KinesteXAIKit
struct CustomWorkoutView: View {
@State private var showKinesteX = false
@State private var isLoading = false
@State private var allResourcesLoaded = false
@State var workoutAction: [String: Any]? = nil
// Initialize KinesteXAIKit with your credentials
let kinestex = KinesteXAIKit(
apiKey: "YOUR_API_KEY",
companyName: "YOUR_COMPANY_NAME",
userId: "YOUR_USER_ID"
)
// Define custom workout exercises
let exercises = [
WorkoutSequenceExercise(
exerciseId: "jz73VFlUyZ9nyd64OjRb",
reps: 15,
duration: nil,
includeRestPeriod: true,
restDuration: 20
),
WorkoutSequenceExercise(
exerciseId: "ZVMeLsaXQ9Tzr5JYXg29",
reps: 10,
duration: 30,
includeRestPeriod: true,
restDuration: 15
),
WorkoutSequenceExercise(
exerciseId: "gJGOiZhCvJrhEP7sTy78",
reps: 20,
duration: nil,
includeRestPeriod: false,
restDuration: 0
)
]
var body: some View {
VStack {
Text("Custom Workout")
.font(.title)
.padding()
Spacer()
Button(action: {
showKinesteX.toggle()
}) {
Text("Start Custom Workout")
.font(.title3)
.foregroundColor(.white)
.bold()
.padding()
.frame(maxWidth: .infinity)
.background(Color.green.cornerRadius(10))
.padding(.horizontal)
}
Spacer()
}
.fullScreenCover(isPresented: $showKinesteX) {
kinestex.createCustomWorkoutView(
exercises: exercises,
user: nil,
style: nil,
isLoading: $isLoading,
workoutAction: $workoutAction,
onMessageReceived: { message in
switch message {
case .custom_type(let value):
guard let type = value["type"] as? String else { return }
if type == "all_resources_loaded" {
allResourcesLoaded = true
// Start workout when resources are ready
workoutAction = ["workout_activity_action": "start"]
}
case .exit_kinestex(_):
showKinesteX = false
allResourcesLoaded = false
case .workout_overview(let data):
print("Workout overview: \(data)")
case .error_occurred(let data):
print("Error: \(data)")
default:
print("Message received: \(message)")
}
}
)
}
}
}
#Preview {
CustomWorkoutView()
}
```
_React Native_
```jsx
import { StyleSheet, View, Button, Text } from "react-native";
import { useEffect, useRef, useState } from "react";
import { Camera } from "expo-camera";
import { SafeAreaView } from "react-native-safe-area-context";
import KinestexSDK from "kinestex-sdk-react-native";
import {
KinesteXSDKCamera,
WorkoutSequenceExercise,
IPostData,
IntegrationOption
} from "kinestex-sdk-react-native/src/types";
export default function CustomWorkoutScreen() {
const kinestexSDKRef = useRef(null);
const [permission, setPermission] = useState(false);
const [allResourcesLoaded, setAllResourcesLoaded] = useState(false);
const [showKinestex, setShowKinestex] = useState(true);
// Define workout sequence of exercises
const customWorkoutExercises: WorkoutSequenceExercise[] = [
{
exerciseId: "jz73VFlUyZ9nyd64OjRb",
reps: 15,
duration: null, // unlimited time to complete reps
includeRestPeriod: true,
restDuration: 20,
},
{
exerciseId: "ZVMeLsaXQ9Tzr5JYXg29",
reps: 10,
duration: 30,
includeRestPeriod: true,
restDuration: 15,
},
// Duplicate to create a set
{
exerciseId: "ZVMeLsaXQ9Tzr5JYXg29",
reps: 10,
duration: 30,
includeRestPeriod: true,
restDuration: 15,
},
{
exerciseId: "gJGOiZhCvJrhEP7sTy78",
reps: 20,
duration: null,
includeRestPeriod: false,
restDuration: 0,
},
];
// Configuration data with customWorkoutExercises
const postData: IPostData = {
key: 'YOUR_API_KEY',
company: 'YOUR_COMPANY_NAME',
userId: "user-123",
customWorkoutExercises: customWorkoutExercises, // pass exercises in postData
style: {
style: "dark",
},
};
// Request camera permission
useEffect(() => {
(async () => {
const { status } = await Camera.requestCameraPermissionsAsync();
if (status === "granted") {
setPermission(true);
}
})();
}, []);
// Handle messages from SDK
const handleMessage = (type: string, data: { [key: string]: any }) => {
switch (type) {
case "exit_kinestex":
console.log("User wishes to exit");
setAllResourcesLoaded(false);
break;
case "workout_exit_request":
console.log("Workout exit request:", data);
setAllResourcesLoaded(false);
setShowKinestex(false);
break;
case "all_resources_loaded":
console.log("All resources loaded");
setAllResourcesLoaded(true);
// Start the workout
kinestexSDKRef.current?.sendAction("workout_activity_action", "start");
break;
case "workout_overview":
console.log("Workout overview:", data);
break;
case "error_occurred":
console.log("Error:", data);
break;
default:
console.log("Message:", type, data);
break;
}
};
if (!permission) {
return (
Camera permission required
);
}
return (
{!showKinestex
? "KinesteX is not activated"
: allResourcesLoaded
? "All resources loaded"
: "KinesteX is loading in background"}
{showKinestex ? (
) : (
setShowKinestex(true)}
/>
)}
);
}
const styles = StyleSheet.create({
safeArea: {
flex: 1,
backgroundColor: "black",
},
statusText: {
padding: 8,
textAlign: "center",
color: "white",
},
launchContainer: {
flex: 1,
alignItems: "center",
justifyContent: "center",
backgroundColor: "black",
},
sdkContainer: {
flex: 1,
},
hiddenSdkContainer: {
width: 0,
height: 0,
overflow: "hidden",
},
});
```
_React (TypeScript)_
```tsx
import React, { useRef, useState } from 'react';
import {
IntegrationOption,
KinesteXSDK,
type IPostData,
type KinesteXSDKCamera,
} from 'kinestex-sdk-react-ts';
// Define workout sequence exercise type
interface WorkoutSequenceExercise {
exerciseId: string;
reps: number | null;
duration: number | null;
includeRestPeriod: boolean;
restDuration: number;
}
const CustomWorkoutScreen: React.FC = () => {
const ref = useRef(null);
const [allResourcesLoaded, setAllResourcesLoaded] = useState(false);
const [showKinestex, setShowKinestex] = useState(true);
const customWorkoutExercises: WorkoutSequenceExercise[] = [
{
exerciseId: "jz73VFlUyZ9nyd64OjRb",
reps: 15,
duration: null,
includeRestPeriod: true,
restDuration: 20,
},
{
exerciseId: "ZVMeLsaXQ9Tzr5JYXg29",
reps: 10,
duration: 30,
includeRestPeriod: true,
restDuration: 15,
},
];
// Include customWorkoutExercises in postData
const postData: IPostData = {
key: 'YOUR_API_KEY',
company: 'YOUR_COMPANY_NAME',
userId: "user-123",
customWorkoutExercises: customWorkoutExercises,
style: { style: "dark" },
};
const handleMessage = (type: string, data: Record) => {
switch (type) {
case "all_resources_loaded":
setAllResourcesLoaded(true);
ref.current?.sendAction("workout_activity_action", "start");
break;
case "workout_exit_request":
case "exit_kinestex":
setAllResourcesLoaded(false);
setShowKinestex(false);
break;
}
};
return (
{showKinestex ? (
) : (
setShowKinestex(true)}>
Show Kinestex Again
)}
);
};
export default CustomWorkoutScreen;
```
_Flutter_
```dart
import 'package:flutter/material.dart';
import 'package:kinestex_sdk_flutter/kinestex_sdk.dart';
import 'package:permission_handler/permission_handler.dart';
Future main() async {
WidgetsFlutterBinding.ensureInitialized();
await KinesteXAIFramework.initialize(
apiKey: "your_api_key",
companyName: "your_company_name",
userId: "your_user_id",
);
runApp(const MyApp());
}
class MyApp extends StatefulWidget {
const MyApp({super.key});
@override
State createState() => _MyAppState();
}
class _MyAppState extends State {
@override
void dispose() {
disposeKinesteXAIFramework();
super.dispose();
}
Future disposeKinesteXAIFramework() async {
await KinesteXAIFramework.dispose();
}
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'KinesteX Custom Workout',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const MyHomePage(),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key});
@override
State createState() => _MyHomePageState();
}
class _MyHomePageState extends State {
// The KinesteX view is ALWAYS mounted. We only toggle visibility.
final ValueNotifier showKinesteX = ValueNotifier(false);
// Signals when SDK reports all resources loaded.
final ValueNotifier allResourcesLoaded = ValueNotifier(false);
// Optional: SDK loading notifier.
final ValueNotifier sdkLoading = ValueNotifier(false);
bool permissionGranted = false;
late final List customWorkoutExercises;
@override
void initState() {
super.initState();
_checkCameraPermission();
customWorkoutExercises = const [
WorkoutSequenceExercise(
exerciseId: "jz73VFlUyZ9nyd64OjRb",
reps: 15,
duration: null,
includeRestPeriod: true,
restDuration: 20,
),
WorkoutSequenceExercise(
exerciseId: "ZVMeLsaXQ9Tzr5JYXg29",
reps: 10,
duration: 30,
includeRestPeriod: true,
restDuration: 15,
),
// Duplicate to create a set
WorkoutSequenceExercise(
exerciseId: "ZVMeLsaXQ9Tzr5JYXg29",
reps: 10,
duration: 30,
includeRestPeriod: true,
restDuration: 15,
),
WorkoutSequenceExercise(
exerciseId: "gJGOiZhCvJrhEP7sTy78",
reps: 20,
duration: null,
includeRestPeriod: false,
restDuration: 0,
),
];
}
void _checkCameraPermission() async {
if (await Permission.camera.request() != PermissionStatus.granted) {
setState(() => permissionGranted = false);
_showCameraAccessDeniedAlert();
} else {
setState(() => permissionGranted = true);
}
}
void _showCameraAccessDeniedAlert() {
WidgetsBinding.instance.addPostFrameCallback((_) {
showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: const Text("Camera Permission Denied"),
content: const Text(
"Camera access is required for this app to function properly.",
),
actions: [
TextButton(
child: const Text("OK"),
onPressed: () => Navigator.of(context).pop(),
),
],
);
},
);
});
}
void handleWebViewMessage(WebViewMessage message) {
if (message is ExitKinestex) {
setState(() {
allResourcesLoaded.value = false;
showKinesteX.value = false;
});
return;
}
try {
final data = message.data;
final type = data['type'];
switch (type) {
case 'all_resources_loaded':
allResourcesLoaded.value = true;
showKinesteX.value = true;
KinesteXAIFramework.sendAction(
"workout_activity_action",
"start",
);
break;
case 'workout_exit_request':
allResourcesLoaded.value = false;
showKinesteX.value = false;
break;
case 'error_occurred':
final errorMsg = data['message']?.toString() ?? 'Unknown error';
print('Error from KinesteX SDK: $errorMsg');
break;
default:
break;
}
} catch (_) {
allResourcesLoaded.value = false;
showKinesteX.value = false;
}
}
Widget createCustomWorkoutView() {
return Center(
child: KinesteXAIFramework.createCustomWorkoutView(
customWorkouts: customWorkoutExercises,
isShowKinestex: showKinesteX,
isLoading: sdkLoading,
onMessageReceived: handleWebViewMessage,
),
);
}
Widget buildCornerIndicator() {
return SafeArea(
child: Padding(
padding: const EdgeInsets.only(top: 8, right: 8),
child: ValueListenableBuilder(
valueListenable: allResourcesLoaded,
builder: (context, loaded, _) {
return ValueListenableBuilder(
valueListenable: showKinesteX,
builder: (context, visible, __) {
Color bg = Colors.black.withOpacity(0.75);
IconData icon = Icons.hourglass_bottom;
String label = 'KinesteX loading...';
if (!permissionGranted) {
bg = Colors.red.withOpacity(0.85);
icon = Icons.videocam_off;
label = 'Camera permission required';
} else if (!loaded) {
bg = Colors.orange.withOpacity(0.85);
icon = Icons.downloading;
label = 'Loading in background';
} else if (loaded && visible) {
bg = Colors.green.withOpacity(0.85);
icon = Icons.check_circle;
label = 'All resources loaded';
} else if (loaded && !visible) {
bg = Colors.grey.withOpacity(0.85);
icon = Icons.visibility_off;
label = 'KinesteX hidden';
}
return Container(
constraints: const BoxConstraints(maxWidth: 260),
padding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 10,
),
decoration: BoxDecoration(
color: bg,
borderRadius: BorderRadius.circular(12),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(icon, color: Colors.white, size: 18),
const SizedBox(width: 8),
Expanded(
child: Text(
label,
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
color: Colors.white,
fontSize: 13,
fontWeight: FontWeight.w500,
),
),
),
if (!permissionGranted)
TextButton(
onPressed: _checkCameraPermission,
child: const Text(
'Grant',
style: TextStyle(color: Colors.white),
),
)
else if (loaded && !visible)
TextButton(
onPressed: () {
showKinesteX.value = true;
KinesteXAIFramework.sendAction(
"workout_activity_action",
"start",
);
},
child: const Text(
'Show',
style: TextStyle(color: Colors.white),
),
)
else if (!loaded)
const SizedBox(
height: 16,
width: 16,
child: CircularProgressIndicator(
strokeWidth: 2,
color: Colors.white,
),
),
],
),
);
},
);
},
),
),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Stack(
children: [
ValueListenableBuilder(
valueListenable: showKinesteX,
builder: (context, isVisible, _) {
return IgnorePointer(
ignoring: !isVisible,
child: AnimatedOpacity(
duration: const Duration(milliseconds: 250),
opacity: isVisible ? 1.0 : 0.0,
child: createCustomWorkoutView(),
),
);
},
),
Positioned(top: 0, right: 0, child: buildCornerIndicator()),
],
),
);
}
}
```
_HTML / JavaScript_
```html
KinesteX: Custom Workout
Click button to start custom workout
Start Custom Workout
```
#### Camera Component
KinesteX Motion Recognition: Real-Time Engagement.
- **Interactive Tracking**: Advanced motion recognition for immersive fitness experiences
- **Real-Time Feedback**: Instantly track reps, spot mistakes, and calculate calories burned
- **Boost Motivation**: Keep users engaged with detailed exercise feedback
- **Custom Integration**: Adapt camera placement to fit your app's design
**Important — what to pass for `currentExercise` and `exercises`:** we **recommend exercise IDs** — they're stable, human-readable, and you already have them when listing exercises from the Content API. Set `exerciseFetchType: "exercise_id"` to use them. The Camera Component also accepts **model IDs** (the default, kept for backward compatibility — but they require an extra round-trip through the Content API to look up) and **exercise titles** (case-sensitive — convenient for prototyping, but title matching depends on locale-normalization and can mismatch similar exercises, so prefer IDs in production). See the **Fetching Exercises** section below.
**Before showing the camera UI**, wait for both `model_warmedup` and `models_loaded` events to fire. See the **Preloading & Events** section below.
**Quick Start**
_Swift (iOS)_
```swift
// Model IDs come from the Content API or admin dashboard.
// See "Camera: Model IDs" subsection.
@State var currentExercise = "3"
kinestex.createCameraView(
exercises: ["3"], // preload every model ID you may switch to
currentExercise: $currentExercise,
user: nil,
isLoading: $isLoading,
onMessageReceived: { message in
switch message {
case .reps(let value):
reps = value["value"] as? Int ?? 0
case .mistake(let value):
mistake = value["value"] as? String ?? "--"
default:
break
}
}
)
```
_Kotlin (Android)_
```kotlin
// Model IDs come from the Content API or admin dashboard.
// See "Camera: Model IDs" subsection.
val cameraView = KinesteXSDK.createCameraComponent(
context = this,
currentExercise = "3",
exercises = listOf("3"),
user = userDetails,
isLoading = viewModel.isLoading,
onMessageReceived = { message ->
when (message) {
is WebViewMessage.Reps ->
(message.data["value"] as? Int)?.let { reps = it }
is WebViewMessage.Mistake ->
(message.data["value"] as? String)?.let { mistake = it }
else -> {}
}
},
permissionHandler = this
)
```
_React Native_
```jsx
// Model IDs come from the Content API or admin dashboard.
// See "Camera: Model IDs" subsection.
// postData seeds the SDK with INITIAL values. To change them at runtime,
// call methods on kinestexSDKRef (e.g. changeExercise) — see "Camera: Controls".
const postData: IPostData = {
key: 'YOUR_API_KEY',
userId: 'YOUR_USER_ID',
company: 'YOUR_COMPANY_NAME',
currentExercise: '3',
exercises: ['3'], // preload every model ID you may switch to
style: { style: 'dark' },
};
{
if (type === 'successful_repeat') setReps(data.value);
if (type === 'mistake') setMistake(data.value);
}}
/>
```
_Flutter_
```dart
// Model IDs come from the Content API or admin dashboard.
// See "Camera: Model IDs" subsection.
KinesteXAIFramework.createCameraComponent(
isShowKinestex: showKinesteX,
exercises: ["3"],
currentExercise: "3",
isLoading: ValueNotifier(false),
onMessageReceived: (message) {
if (message is Reps) {
setState(() => reps = message.data['value']);
}
if (message is Mistake) {
setState(() => mistake = message.data['value']);
}
},
)
```
_HTML / JavaScript_
```html
// Model IDs come from the Content API or admin dashboard.
// See "Camera: Model IDs" subsection.
const postData = {
// ... your initial fields (key, userId, company, etc.)
currentExercise: "3",
exercises: ["3"],
};
const srcURL = "https://ai.kinestex.com/camera";
webView.src = srcURL;
webView.onload = () => sendMessage(postData);
window.addEventListener("message", (event) => {
if (event.origin !== "https://ai.kinestex.com") return;
const msg = JSON.parse(event.data);
if (msg.type === 'successful_repeat') console.log('Rep:', msg.value);
if (msg.type === 'mistake') console.log('Mistake:', msg.data?.value);
});
```
_React (TypeScript)_
```tsx
// Model IDs come from the Content API or admin dashboard.
// See "Camera: Model IDs" subsection.
import { useRef } from 'react';
import {
IntegrationOption,
KinesteXSDK,
type IPostData,
type KinesteXSDKCamera,
} from 'kinestex-sdk-react-ts';
const ref = useRef(null);
// postData seeds the SDK with INITIAL values. To change them at runtime,
// call methods on the ref (e.g. ref.current?.changeExercise) — see "Camera: Controls".
const postData: IPostData = {
key: 'YOUR_API_KEY',
userId: 'YOUR_USER_ID',
company: 'YOUR_COMPANY_NAME',
currentExercise: '3',
exercises: ['3'],
style: { style: 'dark' },
};
{
if (type === 'successful_repeat') setReps(data.value as number);
if (type === 'mistake') setMistake(data.value as string);
}}
/>
```
##### Model IDs
> **Recommendation:** prefer **exercise IDs** — they're easier to get (you already have them when listing exercises from the Content API) and don't require an extra round-trip just to look up a numeric model ID. Set `exerciseFetchType: "exercise_id"` and pass `exercise.id` directly. See the [Fetching Exercises](#camera-component-fetching-exercises) section.
Model IDs are still supported (it's the default when `exerciseFetchType` is omitted) and useful when you already have one handy — e.g. from a stored `WorkoutModel.sequence` or the admin dashboard. They are numeric values like `"3"` for Squats or `"394"` for Jumping Jack.
**Three ways to get a model ID:**
1. **Content API** — call `fetchExercises()` (Swift / Kotlin), `fetchContent(contentType: ContentType.exercise)` (Flutter), or `GET /api/v1/exercises` (REST). Each `ExerciseModel` has a `model_id` field. See the [Content API](/docs/content-api) section for full details. *(Note: this is the extra round-trip that exercise IDs let you skip.)*
2. **Admin dashboard** — open the exercise in [admin.kinestex.com](https://admin.kinestex.com); the model ID is shown at the top of the page header.
3. **Workout sequences** — when iterating `WorkoutModel.sequence`, each `ExerciseModel` entry exposes its own `model_id`.
**Fetch a Model ID**
_Swift (iOS)_
```swift
Task {
let result = await kinestex.fetchExercises(limit: 10)
if case .success(let response) = result,
let exercise = response.exercises.first {
// exercise.modelId is what the Camera Component expects
currentExercise = exercise.modelId
}
}
```
_Kotlin (Android)_
```kotlin
lifecycleScope.launch {
val result = withContext(Dispatchers.IO) {
KinesteXSDK.api.fetchAPIContentData(
contentType = ContentType.EXERCISE,
limit = 10
)
}
if (result is APIContentResult.Exercises) {
// exercise.modelId is what the Camera Component expects
currentExercise = result.exercises.firstOrNull()?.modelId ?: ""
}
}
```
_React Native_
```jsx
const res = await fetch(
'https://admin.kinestex.com/api/v1/exercises?limit=10',
{ headers: { 'x-api-key': API_KEY, 'x-company-name': COMPANY_NAME } },
);
const { exercises } = await res.json();
// exercise.model_id is what the Camera Component expects.
// Pass it as initial value, or switch later via ref.current?.changeExercise(...).
const modelId = exercises[0].model_id;
```
_Flutter_
```dart
final result = await KinesteXAIFramework.apiService.fetchContent(
contentType: ContentType.exercise,
limit: 10,
);
// exercise.modelId is what the Camera Component expects
if (result is ExercisesResult) {
final modelId = result.response.exercises.first.modelId;
}
```
_HTML / JavaScript_
```html
const res = await fetch(
'https://admin.kinestex.com/api/v1/exercises?limit=10',
{ headers: { 'x-api-key': API_KEY, 'x-company-name': COMPANY_NAME } },
);
const { exercises } = await res.json();
// exercise.model_id is what the Camera Component expects
const modelId = exercises[0].model_id;
```
_React (TypeScript)_
```tsx
const res = await fetch(
'https://admin.kinestex.com/api/v1/exercises?limit=10',
{ headers: { 'x-api-key': API_KEY, 'x-company-name': COMPANY_NAME } },
);
const { exercises } = await res.json() as { exercises: ExerciseModel[] };
// exercise.model_id is what the Camera Component expects
const modelId = exercises[0].model_id;
```
##### Fetching Exercises (by ID or Title)
Besides model IDs, the Camera Component can fetch exercises by **exercise ID** (recommended) or **exercise title**. Set the `exerciseFetchType` parameter to choose the form:
| `exerciseFetchType` | Meaning | What goes in `exercises` / `currentExercise` |
|---|---|---|
| `"exercise_id"` ✅ **recommended** | Exercise IDs from the Content API — easiest to use, no extra round-trip | e.g. `"squats_v2"` |
| `"model_id"` *(default)* | Numeric model IDs — kept for backward compatibility; requires a Content API lookup to obtain | `"3"`, `"394"` |
| `"exercise_title"` | Exercise titles (case-sensitive) — handy for prototyping, but title matching depends on locale-normalization and can mismatch similar exercises | `"Squats"`, `"Jumping Jack"` |
Omitting `exerciseFetchType` keeps the default model-ID behavior — no migration needed for existing integrations.
**Where to pass it:**
- **React Native (SDK v1.3.1+):** directly in `postData`, alongside `exercises` and `currentExercise`.
- **Swift, Kotlin, Flutter, HTML/JS, React (TypeScript):** inside `customParams` / `customParameters` along with `exercises` and `currentExercise`.
**Keep one form per session:** use the same form for both `exercises` and `currentExercise`, and for any later switches.
**Fetch by Exercise Title**
_Swift (iOS)_
```swift
// exerciseFetchType goes inside customParams
kinestex.createCameraView(
exercises: ["Squats", "Jumping Jack"],
currentExercise: $currentExercise, // e.g. "Squats"
customParams: [
"exerciseFetchType": "exercise_title" // "model_id" (default) | "exercise_id" | "exercise_title"
]
)
```
_Kotlin (Android)_
```kotlin
// exerciseFetchType goes inside customParams
KinesteXSDK.createCameraComponent(
context = this,
currentExercise = "Squats",
exercises = listOf("Squats", "Jumping Jack"),
customParams = mutableMapOf(
"exerciseFetchType" to "exercise_title" // "model_id" (default) | "exercise_id" | "exercise_title"
),
permissionHandler = this
)
```
_React Native_
```jsx
// React Native v1.3.1+: exerciseFetchType is direct in postData
const postData: IPostData = {
key: 'YOUR_API_KEY',
userId: 'YOUR_USER_ID',
company: 'YOUR_COMPANY_NAME',
currentExercise: 'Squats',
exercises: ['Squats', 'Jumping Jack'],
exerciseFetchType: 'exercise_title', // "model_id" (default) | "exercise_id" | "exercise_title"
};
```
_Flutter_
```dart
// exerciseFetchType goes inside customParams
KinesteXAIFramework.createCameraComponent(
isShowKinestex: showKinesteX,
exercises: ["Squats", "Jumping Jack"],
currentExercise: "Squats",
customParams: {
"exerciseFetchType": "exercise_title", // "model_id" (default) | "exercise_id" | "exercise_title"
},
isLoading: ValueNotifier(false),
onMessageReceived: handleWebViewMessage,
)
```
_HTML / JavaScript_
```html
// exerciseFetchType goes inside customParams
const postData = {
// ... your initial fields (key, userId, company, etc.)
currentExercise: "Squats",
exercises: ["Squats", "Jumping Jack"],
customParams: {
exerciseFetchType: "exercise_title", // "model_id" (default) | "exercise_id" | "exercise_title"
},
};
```
_React (TypeScript)_
```tsx
// exerciseFetchType goes inside customParameters
const postData: IPostData = {
key: 'YOUR_API_KEY',
userId: 'YOUR_USER_ID',
company: 'YOUR_COMPANY_NAME',
currentExercise: 'Squats',
exercises: ['Squats', 'Jumping Jack'],
customParameters: {
exerciseFetchType: 'exercise_title', // "model_id" (default) | "exercise_id" | "exercise_title"
},
};
```
##### Loading More Exercises at Runtime (React Native v1.3.1+)
**Availability:** React Native SDK `v1.3.1+` only. Other platforms must pass the full `exercises` list at initialization.
In React Native you can fetch and cache **additional** exercise models **after the session has started**, without re-mounting the camera. This is done by calling `sendAction` with the `"load_models"` action and an `extras` object containing the new identifiers.
**Three-step flow:**
1. Send the `load_models` action with the identifiers you want to add.
2. Wait for the `models_loaded` event — `data.modelIds` echoes the identifiers that resolved.
3. Switch the active exercise with `changeExercise(...)` (do **not** repeat `exerciseFetchType`).
If `exercises` is missing or empty, the SDK posts back `{ "type": "error_occurred", "message": "load_models: no exercises provided" }`. Per-identifier failures arrive as separate `error_occurred` messages; any identifiers that did resolve are still listed in `modelIds` and are switchable.
**Load More Models, Then Switch**
_React Native_
```jsx
// Step 1 — fetch additional models at runtime
sdkRef.current?.sendAction(
"workout_activity_action",
"load_models",
{
exercises: ["Jumping Jack", "Lunges"],
exerciseFetchType: "exercise_title", // match the form you used initially
}
);
// Step 2 — wait for models_loaded, then switch
const handleMessage = (type: string, data: { [key: string]: any }) => {
if (type === "models_loaded" && data.modelIds?.includes("Jumping Jack")) {
// Step 3 — switch the active exercise. Do NOT repeat exerciseFetchType here.
sdkRef.current?.changeExercise("Jumping Jack");
}
if (type === "error_occurred") {
console.warn("KinesteX error:", data.message);
}
};
```
##### Preloading & Events
Two events fire as the component initializes. Wait for **both** before revealing the camera UI to the user.
| Event | Meaning |
|---|---|
| `model_warmedup` | Pose-tracking (MediaPipe) model is ready |
| `models_loaded` | All exercise models in `exercises` have downloaded |
**Pattern:** mount the camera hidden (e.g. `opacity: 0`) with a loader on top. Reveal once both events have fired.
**Wait for Both Events**
_Swift (iOS)_
```swift
@State private var modelWarmedUp = false
@State private var modelsLoaded = false
var isReady: Bool { modelWarmedUp && modelsLoaded }
kinestex.createCameraView(
exercises: ["3"],
currentExercise: $currentExercise,
user: nil,
isLoading: $isLoading,
onMessageReceived: { message in
if case .custom_type(let value) = message,
let type = value["type"] as? String {
if type == "model_warmedup" { modelWarmedUp = true }
if type == "models_loaded" { modelsLoaded = true }
}
}
)
.opacity(isReady ? 1 : 0)
```
_Kotlin (Android)_
```kotlin
var modelWarmedUp = false
var modelsLoaded = false
val cameraView = KinesteXSDK.createCameraComponent(
context = this,
currentExercise = "3",
exercises = listOf("3"),
user = null,
isLoading = viewModel.isLoading,
onMessageReceived = { message ->
if (message is WebViewMessage.CustomType) {
when (message.data["type"] as? String) {
"model_warmedup" -> modelWarmedUp = true
"models_loaded" -> modelsLoaded = true
}
cameraView.alpha = if (modelWarmedUp && modelsLoaded) 1f else 0f
}
},
permissionHandler = this
)
```
_React Native_
```jsx
const [modelWarmedUp, setModelWarmedUp] = useState(false);
const [modelsLoaded, setModelsLoaded] = useState(false);
const isReady = modelWarmedUp && modelsLoaded;
{
if (type === 'model_warmedup') setModelWarmedUp(true);
if (type === 'models_loaded') setModelsLoaded(true);
}}
/>
```
_Flutter_
```dart
bool modelWarmedUp = false;
bool modelsLoaded = false;
KinesteXAIFramework.createCameraComponent(
isShowKinestex: showKinesteX,
exercises: ["3"],
currentExercise: "3",
isLoading: ValueNotifier(false),
onMessageReceived: (message) {
// Model-readiness events arrive as CustomType; the name is in data['type']
if (message is CustomType) {
if (message.data['type'] == 'model_warmedup') {
setState(() => modelWarmedUp = true);
}
if (message.data['type'] == 'models_loaded') {
setState(() => modelsLoaded = true);
}
}
},
)
```
_HTML / JavaScript_
```html
let modelWarmedUp = false, modelsLoaded = false;
const reveal = () => {
if (modelWarmedUp && modelsLoaded) iframe.style.opacity = '1';
};
iframe.style.opacity = '0';
window.addEventListener("message", (event) => {
if (event.origin !== "https://ai.kinestex.com") return;
const msg = JSON.parse(event.data);
if (msg.type === 'model_warmedup') { modelWarmedUp = true; reveal(); }
if (msg.type === 'models_loaded') { modelsLoaded = true; reveal(); }
});
```
_React (TypeScript)_
```tsx
const [modelWarmedUp, setModelWarmedUp] = useState(false);
const [modelsLoaded, setModelsLoaded] = useState(false);
const isReady = modelWarmedUp && modelsLoaded;
{
if (type === 'model_warmedup') setModelWarmedUp(true);
if (type === 'models_loaded') setModelsLoaded(true);
}}
/>
```
##### Controls
**Switching exercises in real time.** Set `currentExercise` to any model ID from the `exercises` array — the camera component swaps tracking immediately.
**Control commands.** Send any of these strings as `currentExercise` to control the session:
| Command | Effect |
|---|---|
| `"Pause Exercise"` | Pauses motion tracking; rep counter freezes |
| `"Pause Audio"` | Mutes voice feedback |
| `"Resume Audio"` | Re-enables voice feedback |
| `"Workout Overview"` | Triggers a summary snapshot for the current session |
| `"Stop Camera"` | ⚠️ **Destructive** — releases camera + models, fires `stop_camera`. Not recoverable without re-mounting the component (see warning below) |
To resume tracking after a pause, set `currentExercise` back to a real model ID from `exercises`.
> ⚠️ **`"Stop Camera"` is destructive — avoid it for normal flows.** It tears down the camera, MediaPipe, and all loaded exercise models, then fires the `stop_camera` event. **You cannot recover from it without unmounting and re-creating the entire camera component.** Only send it when the user is permanently leaving the camera screen. For temporary pauses, use `"Pause Exercise"` instead.
**Switch Exercise and Send Control Commands**
_Swift (iOS)_
```swift
// Switch exercise (Swift uses two-way binding via @State)
currentExercise = "394"
// Pause / resume tracking
currentExercise = "Pause Exercise"
currentExercise = "3"
```
_Kotlin (Android)_
```kotlin
// Switch exercise
KinesteXSDK.updateCurrentExercise("394")
// Pause / resume tracking
KinesteXSDK.updateCurrentExercise("Pause Exercise")
KinesteXSDK.updateCurrentExercise("3")
```
_React Native_
```jsx
// Switch exercise
kinestexSDKRef.current?.changeExercise("394");
// Pause / resume tracking
kinestexSDKRef.current?.changeExercise("Pause Exercise");
kinestexSDKRef.current?.changeExercise("3");
```
_Flutter_
```dart
// All controls go through your updateExercise ValueNotifier
updateExercise.value = "394";
updateExercise.value = "Pause Exercise";
updateExercise.value = "3";
```
_HTML / JavaScript_
```html
// Switch exercise
webView.contentWindow.postMessage(
{ currentExercise: "394" },
srcURL
);
// Pause / resume tracking
webView.contentWindow.postMessage({ currentExercise: "Pause Exercise" }, srcURL);
webView.contentWindow.postMessage({ currentExercise: "3" }, srcURL);
```
_React (TypeScript)_
```tsx
// Switch exercise
ref.current?.changeExercise("394");
// Pause / resume tracking
ref.current?.changeExercise("Pause Exercise");
ref.current?.changeExercise("3");
```
##### Customization
Pass any of these fields in `customParams` (Swift / Kotlin / Flutter) or directly in `postData` (HTML/JS, React Native, React TS) at initialization.
| Field | Type | Description |
|---|---|---|
| `restSpeeches` | `string[]` | Audio phrases to preload (from `ExerciseModel.rest_speech`) |
| `videoURL` | `string` | Use a video file instead of the live camera — the full tracking pipeline (reps, accuracy) runs on the video. Ideal for testing without physically performing exercises; see [Testing & Simulation](/docs/customization-parameters/testing-simulation) |
| `landmarkColor` | `string` | Pose overlay color in hex with `#` (default `#14FF00`) |
| `showSilhouette` | `boolean` | Show "get into frame" silhouette (default `true`) |
| `includeRealtimeAccuracy` | `boolean` | (Beta) Stream live position-confidence alongside reps |
| `includePoseData` | `string[]` | Any of `"angles"`, `"poseLandmarks"`, `"worldLandmarks"`. **Performance impact** — only enable for custom calculations |
##### Event Reference
All events the Camera Component emits to the host app:
| Event | Payload | When |
|---|---|---|
| `model_warmedup` | `{ message }` | Pose model is ready |
| `models_loaded` | `{ message }` | All exercise models in `exercises` finished downloading |
| `person_in_frame` | `{ message }` | User entered the silhouette frame |
| `successful_repeat` | `{ exercise, value, accuracy }` | A rep was counted (`value` = total reps so far) |
| `mistake` | `{ value }` | Form mistake detected |
| `correct_position_accuracy` | `{ accuracy }` | (Beta) Live position confidence — only when `includeRealtimeAccuracy: true` |
| `pose_landmarks` | `{ poseLandmarks }` | Per-frame screen-space landmarks — only when `includePoseData` includes `"poseLandmarks"` |
| `world_landmarks` | `{ worldLandmarks }` | Per-frame world-space landmarks — only when `includePoseData` includes `"worldLandmarks"` |
| `speech_fetch_complete` | `{ successCount, failureCount }` | All `restSpeeches` finished loading |
| `error_occurred` | `{ message }` or `{ data, error }` | Any error (model fetch fail, phrase fail, etc.) |
| `warning` | `{ data }` | Non-fatal config issue (e.g. no model IDs provided) |
| `stop_camera` | `{ message }` | Confirms the `"Stop Camera"` command finished |
##### Pose Data
When `includePoseData` contains `"poseLandmarks"` or `"worldLandmarks"`, the camera emits per-frame events with raw skeleton data. **Only enable this if you're doing custom calculations — there is a performance cost.**
**Two coordinate spaces:**
- `poseLandmarks` — values 0–1, normalized to the camera frame.
- `worldLandmarks` — meters, relative to the hips (best Z accuracy).
Each landmark has `{ x, y, z, visibility }` (all 0–1).
**Available landmarks** (same names in both spaces): `nose`, `leftEyeInner`, `leftEye`, `leftEyeOuter`, `rightEyeInner`, `rightEye`, `rightEyeOuter`, `leftEar`, `rightEar`, `mouthLeft`, `mouthRight`, `leftShoulder`, `rightShoulder`, `leftElbow`, `rightElbow`, `leftWrist`, `rightWrist`, `leftPinky`, `rightPinky`, `leftIndex`, `rightIndex`, `leftThumb`, `rightThumb`, `leftHip`, `rightHip`, `leftKnee`, `rightKnee`, `leftAnkle`, `rightAnkle`, `leftHeel`, `rightHeel`, `leftFootIndex`, `rightFootIndex`.
**Available angles** (when `"angles"` is included — both 2D and 3D versions are emitted): `leftKneeAngle`, `rightKneeAngle`, `leftHipAngle`, `rightHipAngle`, `leftShoulderAngle`, `rightShoulderAngle`, `leftElbowAngle`, `rightElbowAngle`, `leftWristAngle`, `rightWristAngle`, `leftAnkleAngle`, `rightAnkleAngle`, `leftArmpitAngle`, `rightArmpitAngle`.
##### Complete Example
Minimal working example with **Next** / **Previous** buttons that cycle between exercises and a live rep counter in the UI.
**Complete Implementation**
_Swift (iOS)_
```swift
import SwiftUI
import KinesteXAIKit
struct CameraScreen: View {
let kinestex = KinesteXAIKit(
apiKey: "YOUR_API_KEY",
companyName: "YOUR_COMPANY_NAME",
userId: "YOUR_USER_ID"
)
// 3 = Squats, 394 = Jumping Jack
let exerciseIds = ["3", "394"]
@State private var index = 0
@State private var currentExercise = "3"
@State private var reps = 0
@State private var isLoading = false
var body: some View {
VStack(spacing: 16) {
Text("Reps: \(reps)")
.font(.title)
.padding(.top)
kinestex.createCameraView(
exercises: exerciseIds,
currentExercise: $currentExercise,
user: nil,
isLoading: $isLoading,
onMessageReceived: { message in
if case .reps(let value) = message {
reps = value["value"] as? Int ?? 0
}
}
)
HStack(spacing: 24) {
Button("Previous") { switchTo(index - 1) }
Button("Next") { switchTo(index + 1) }
}
.padding(.bottom)
}
}
private func switchTo(_ newIndex: Int) {
index = (newIndex + exerciseIds.count) % exerciseIds.count
currentExercise = exerciseIds[index]
reps = 0
}
}
```
_Kotlin (Android)_
```kotlin
import android.Manifest
import android.os.Bundle
import android.widget.Button
import android.widget.LinearLayout
import android.widget.TextView
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity
import com.kinestex.kinestexsdkkotlin.GenericWebView
import com.kinestex.kinestexsdkkotlin.KinesteXSDK
import com.kinestex.kinestexsdkkotlin.PermissionHandler
import com.kinestex.kinestexsdkkotlin.WebViewMessage
import kotlinx.coroutines.flow.MutableStateFlow
class CameraActivity : AppCompatActivity(), PermissionHandler {
// 3 = Squats, 394 = Jumping Jack
private val exerciseIds = listOf("3", "394")
private var index = 0
private val isLoading = MutableStateFlow(false)
private lateinit var camera: GenericWebView
private lateinit var tvReps: TextView
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_camera)
tvReps = findViewById(R.id.tvReps)
camera = KinesteXSDK.createCameraComponent(
context = this,
currentExercise = exerciseIds[0],
exercises = exerciseIds,
user = null,
isLoading = isLoading,
onMessageReceived = { msg ->
if (msg is WebViewMessage.Reps) {
val v = msg.data["value"] as? Int ?: 0
runOnUiThread { tvReps.text = "Reps: $v" }
}
},
permissionHandler = this
) as GenericWebView
findViewById(R.id.cameraContainer).addView(camera)
findViewById(R.id.btnPrev).setOnClickListener { switchTo(index - 1) }
findViewById(R.id.btnNext).setOnClickListener { switchTo(index + 1) }
}
private fun switchTo(newIndex: Int) {
index = (newIndex + exerciseIds.size) % exerciseIds.size
KinesteXSDK.updateCurrentExercise(exerciseIds[index])
runOnUiThread { tvReps.text = "Reps: 0" }
}
override fun requestCameraPermission() {
registerForActivityResult(
ActivityResultContracts.RequestPermission()
) { granted -> camera.handlePermissionResult(granted) }
.launch(Manifest.permission.CAMERA)
}
}
```
_React Native_
```jsx
import { useRef, useState } from 'react';
import { View, Text, Button } from 'react-native';
import KinestexSDK from 'kinestex-sdk-react-native';
import {
IntegrationOption,
KinesteXSDKCamera,
IPostData,
} from 'kinestex-sdk-react-native/src/types';
// 3 = Squats, 394 = Jumping Jack
const exerciseIds = ['3', '394'];
export default function CameraScreen() {
const ref = useRef(null);
const [index, setIndex] = useState(0);
const [reps, setReps] = useState(0);
const postData: IPostData = {
key: 'YOUR_API_KEY',
userId: 'YOUR_USER_ID',
company: 'YOUR_COMPANY_NAME',
currentExercise: exerciseIds[0],
exercises: exerciseIds,
style: { style: 'dark' },
};
const switchTo = (newIndex: number) => {
const wrapped = (newIndex + exerciseIds.length) % exerciseIds.length;
setIndex(wrapped);
setReps(0);
ref.current?.changeExercise(exerciseIds[wrapped]);
};
return (
Reps: {reps}
{
if (type === 'successful_repeat') setReps(data.value);
}}
/>
switchTo(index - 1)} />
switchTo(index + 1)} />
);
}
```
_Flutter_
```dart
import 'package:flutter/material.dart';
import 'package:kinestex_sdk_flutter/kinestex_sdk.dart';
class CameraScreen extends StatefulWidget {
const CameraScreen({super.key});
@override
State createState() => _CameraScreenState();
}
class _CameraScreenState extends State {
// 3 = Squats, 394 = Jumping Jack
final exerciseIds = const ['3', '394'];
int index = 0;
int reps = 0;
final showKinesteX = ValueNotifier(true);
final updateExercise = ValueNotifier('3');
void switchTo(int newIndex) {
final wrapped = (newIndex + exerciseIds.length) % exerciseIds.length;
setState(() {
index = wrapped;
reps = 0;
});
updateExercise.value = exerciseIds[wrapped];
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Column(
children: [
Padding(
padding: const EdgeInsets.all(16),
child: Text('Reps: $reps', style: const TextStyle(fontSize: 24)),
),
Expanded(
child: ValueListenableBuilder(
valueListenable: updateExercise,
builder: (context, value, _) {
return KinesteXAIFramework.createCameraComponent(
isShowKinestex: showKinesteX,
exercises: exerciseIds,
currentExercise: value ?? exerciseIds[0],
updatedExercise: value,
isLoading: ValueNotifier(false),
onMessageReceived: (m) {
if (m is Reps) {
setState(() => reps = m.data['value'] ?? 0);
}
},
);
},
),
),
Padding(
padding: const EdgeInsets.all(16),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
ElevatedButton(
onPressed: () => switchTo(index - 1),
child: const Text('Previous'),
),
ElevatedButton(
onPressed: () => switchTo(index + 1),
child: const Text('Next'),
),
],
),
),
],
),
);
}
}
```
_HTML / JavaScript_
```html
Reps: 0
Previous
Next
```
_React (TypeScript)_
```tsx
import { useRef, useState } from 'react';
import {
IntegrationOption,
KinesteXSDK,
type IPostData,
type KinesteXSDKCamera,
} from 'kinestex-sdk-react-ts';
// 3 = Squats, 394 = Jumping Jack
const exerciseIds = ['3', '394'];
export default function CameraScreen() {
const ref = useRef(null);
const [index, setIndex] = useState(0);
const [reps, setReps] = useState(0);
const postData: IPostData = {
key: 'YOUR_API_KEY',
userId: 'YOUR_USER_ID',
company: 'YOUR_COMPANY_NAME',
currentExercise: exerciseIds[0],
exercises: exerciseIds,
style: { style: 'dark' },
};
const switchTo = (newIndex: number) => {
const wrapped = (newIndex + exerciseIds.length) % exerciseIds.length;
setIndex(wrapped);
setReps(0);
ref.current?.changeExercise(exerciseIds[wrapped]);
};
return (
Reps: {reps}
{
if (type === 'successful_repeat') setReps(data.value as number);
}}
/>
switchTo(index - 1)}>Previous
switchTo(index + 1)}>Next
);
}
```
---
## Data Points
PostMessage events sent from KinesteX SDK for integration with native apps and external systems. Events are sent in real time, work safely offline, and provide comprehensive tracking for workouts, exercises, and health assessments.
### Receiving Data
Each platform has a specific pattern for receiving data from KinesteX.
**SDK Platforms (Swift, Kotlin, React Native, React-TS, Flutter):** Use the callback function provided by the SDK with typed message enums.
**HTML/JS:** Set up a message event listener manually since there's no SDK wrapper.
**Platform-Specific Data Handlers**
_Flutter_
```dart
void handleWebViewMessage(WebViewMessage message) {
if (message is KinestexLaunched) {
print("KinesteX launched at: ${message}");
} else if (message is ExitKinestex) {
print("Exited KinesteX at: ${message} seconds.");
} else if (message is PlanUnlocked) {
print("Plan Unlocked: ${message}");
} else if (message is WorkoutOpened) {
print("Workout Opened: ${message}");
} else if (message is WorkoutStarted) {
print("Workout Started: ${message}");
} else if (message is ExerciseCompleted) {
print("Exercise: ${message}");
} else if (message.data['type'] == 'total_active_seconds') {
// No dedicated class — arrives as a generic message
print("Active seconds: ${message.data}");
} else if (message is LeftCameraFrame) {
print("User left camera frame at time: ${message}");
} else if (message is ReturnedCameraFrame) {
print("User returned to camera frame at time: ${message}");
} else if (message is WorkoutOverview) {
print("Workout Overview: ${message}");
} else if (message is ExerciseOverview) {
print("Exercise Overview: ${message}");
} else if (message is WorkoutCompleted) {
print("Workout Completed: ${message}");
} else {
print("Other data points: ${message.data}");
}
}
```
_React Native_
```jsx
const handleMessage = (type: string, data: { [key: string]: any }) => {
switch (type) {
case "kinestex_launched":
console.log('Launched at:', data);
break;
case "exit_kinestex":
console.log('Exited, time spent:', data.time_spent);
break;
case "workout_overview":
console.log('Workout stats:', data);
break;
case "exercise_completed":
console.log('Exercise done:', data.exercise_title);
break;
case "workout_completed":
console.log('Workout finished:', data);
break;
case "error_occurred":
console.error('Error:', data);
break;
default:
console.log('Other message type:', type, data);
break;
}
};
```
_React (TypeScript)_
```tsx
const handleMessage = (type: string, data: { [key: string]: any }) => {
switch (type) {
case "kinestex_launched":
console.log('Launched at:', data);
break;
case "exit_kinestex":
console.log('Exited, time spent:', data.time_spent);
break;
case "workout_overview":
console.log('Workout stats:', data);
break;
case "exercise_completed":
console.log('Exercise done:', data.exercise_title);
break;
case "workout_completed":
console.log('Workout finished:', data);
break;
case "error_occurred":
console.error('Error:', data);
break;
default:
console.log('Other message type:', type, data);
break;
}
};
```
_Kotlin (Android)_
```kotlin
private fun handleWebViewMessage(message: WebViewMessage) {
when (message) {
is WebViewMessage.KinestexLaunched -> {
println("KinesteX launched at: $message")
}
is WebViewMessage.FinishedWorkout -> {
println("Finished Workout: $message")
}
is WebViewMessage.ErrorOccurred -> {
println("Error Occurred: $message")
}
is WebViewMessage.ExerciseCompleted -> {
println("Exercise Completed: $message")
}
is WebViewMessage.ExitKinestex -> {
println("Exited KinesteX at: $message")
}
is WebViewMessage.WorkoutOpened -> {
println("Workout Opened: $message")
}
is WebViewMessage.WorkoutStarted -> {
println("Workout Started: $message")
}
is WebViewMessage.PlanUnlocked -> {
println("Plan Unlocked: $message")
}
is WebViewMessage.WorkoutOverview -> {
println("Workout Overview: $message")
}
is WebViewMessage.ExerciseOverview -> {
println("Exercise Overview: $message")
}
is WebViewMessage.WorkoutCompleted -> {
println("Workout Completed: $message")
}
// Camera Component Specific
is WebViewMessage.Reps -> {
println("Reps: $message")
}
is WebViewMessage.Mistake -> {
println("Mistake: $message")
}
is WebViewMessage.CustomType -> {
println("Any other message: $message")
}
}
}
```
_Swift (iOS)_
```swift
// onMessageReceived callback passes WebViewMessage enum
// Available message types:
// kinestex_launched([String: Any]) - KinesteX View launched
// finished_workout([String: Any]) - Workout completed
// error_occurred([String: Any]) - Errors (e.g., missing camera)
// exercise_completed([String: Any]) - Exercise finished
// exit_kinestex([String: Any]) - User exits KinesteX view
// workout_opened([String: Any]) - Workout description viewed
// workout_started([String: Any]) - Workout begins
// plan_unlocked([String: Any]) - Workout plan unlocked
// custom_type([String: Any]) - Unrecognized messages
// reps([String: Any]) - Successful repetitions
// mistake([String: Any]) - Detected mistakes
// left_camera_frame([String: Any]) - User left camera frame
// returned_camera_frame([String: Any]) - User returned to frame
// workout_overview([String: Any]) - Workout summary
// exercise_overview([String: Any]) - Exercise summary
// workout_completed([String: Any]) - Workout done, overview exited
```
_HTML / JavaScript_
```html
// HTML/JS requires manual event listener setup
window.addEventListener("message", (event) => {
// Security: only accept messages from KinesteX
if (event.origin !== "https://ai.kinestex.com") return;
try {
const message = JSON.parse(event.data);
switch (message.type) {
case "kinestex_launched":
console.log("Launched:", message.data);
break;
case "exit_kinestex":
console.log("Exited, time spent:", message.time_spent);
break;
case "workout_overview":
console.log("Workout stats:", message.data);
break;
case "exercise_completed":
console.log("Exercise done:", message.data);
break;
case "error_occurred":
console.error("Error:", message.data || message.message);
break;
default:
console.log("Message:", message.type, message);
}
} catch (e) {
console.error("Failed to parse message:", e);
}
});
```
### Application Lifecycle
Events for app startup, loading, and exit.
| Event | Data Fields | Description |
|-------|-------------|-------------|
| kinestex_launched | data: string ("dd mm yyyy hh:mm:ss") | KinesteX application is launched |
| kinestex_loaded | date: string (ISO format) | KinesteX fully loaded and ready |
| exit_kinestex | date: Date, time_spent: string ("hh:mm:ss") | User exits with total time spent |
| main_page_opened | date: string (ISO format) | Main/home page is opened |
| home_page_opened | date: string (ISO format) | Home Page integration screen opened. Fires exactly once per mount |
| streak_extended | data: object | User's daily streak was extended by completing a qualifying activity (workout, challenge, plan day, assessment) |
**streak_extended Data Structure:**
```
{
current_streak: number, // Current streak count (days)
longest_streak: number, // User's longest streak ever
last_activity_date: string // ISO date of the activity that extended the streak
}
```
### Workout Events
Events for workout lifecycle and statistics.
| Event | Data Fields | Description |
|-------|-------------|-------------|
| workout_opened | title: string, id: string, date: string | Workout details page opened |
| workout_started | id: string, date: string | Workout session started |
| workout_started (alt) | workoutId: string | Alternative workout start format |
| workout_completed | workout: string, date: string | Workout finished, user exits overview |
| workout_ended | id: string, exit_type: string, date: string | Workout session ended (see exit_type values below) |
| workout_overview | data: object | Complete workout summary statistics |
**workout_ended exit_type values:**
| Value | Meaning |
|-------|---------|
| complete | User finished the entire workout including outro |
| exit | User abandoned the workout mid-session |
| outro | User exited from the outro/cooldown screen after completing all exercises |
**workout_overview Data Structure:**
```
{
workout_title: string, // Workout name
workout_id: string, // Unique workout ID
target_duration_seconds: number, // Target workout duration (seconds)
workout_duration_seconds: number, // Total wall-clock session time
// (includes rest, transitions, pauses).
// For challenges/assessments this equals
// total_time_spent (no wall-clock concept)
total_time_spent: number, // Active exercise time only (seconds)
completed_reps_count: number, // Total completed reps
target_reps_count: number, // Total target reps
calories_burned: number, // Calories (2 decimal places)
completion_percentage: number, // Completion % (2 decimals)
total_mistakes: number, // Total mistake count
accuracy_score: number, // Overall accuracy (0-100)
efficiency_score: number, // Efficiency metric (0-100)
total_exercise: number, // Number of exercises
actual_hold_time_seconds: number, // Time in correct position
target_hold_time_seconds: number // Target hold time
}
```
**Note:** Use `workout_duration_seconds` to display or log the full session time (including rest periods). Use `total_time_spent` if you only need active exercise time.
**Handling Workout Overview**
_Swift (iOS)_
```swift
case .workout_overview(let data):
if let calories = data["calories_burned"] as? Double,
let completion = data["completion_percentage"] as? Double {
print("Burned \(calories) cal, \(completion)% complete")
}
```
_Kotlin (Android)_
```kotlin
is WebViewMessage.WorkoutOverview -> {
// Payload fields arrive in message.data (Map)
val calories = message.data["calories_burned"]
val completion = message.data["completion_percentage"]
Log.d("Workout", "Burned $calories cal, $completion% complete")
}
```
_React Native_
```jsx
case "workout_overview":
const { calories_burned, completion_percentage, accuracy_score } = data;
console.log(`Workout: ${completion_percentage}% complete`);
console.log(`Calories: ${calories_burned}, Accuracy: ${accuracy_score}`);
break;
```
_Flutter_
```dart
if (message is WorkoutOverview) {
// Payload fields arrive in message.data (Map)
print("Calories: ${message.data['calories_burned']}");
print("Completion: ${message.data['completion_percentage']}%");
print("Accuracy: ${message.data['accuracy_score']}");
}
```
_HTML / JavaScript_
```html
case "workout_overview":
const stats = message.data;
console.log("Workout:", stats.workout_title);
console.log("Calories:", stats.calories_burned);
console.log("Accuracy:", stats.accuracy_score);
break;
```
_React (TypeScript)_
```tsx
case "workout_overview":
const { calories_burned, completion_percentage, accuracy_score } = data;
console.log(`Workout: ${completion_percentage}% complete`);
console.log(`Calories: ${calories_burned}, Accuracy: ${accuracy_score}`);
break;
```
### Exercise Events
Events for individual exercise tracking.
| Event | Description |
|-------|-------------|
| exercise_completed | Individual exercise completed |
| exercise_overview | All exercises summary (array) |
**exercise_completed Data Structure:**
```
{
exercise_title: string, // Exercise name
time_spent: number, // Seconds spent
repeats: number, // Reps completed
total_reps: number, // Required reps
total_duration: number, // Countdown time
perfect_hold_position: number, // Time in perfect hold position (seconds).
// 0 for non-hold exercises
calories: number, // Calories burned
exercise_id: string, // Exercise ID
exercise_index: number, // 1-based position of the completed exercise
total_exercises: number, // Total number of exercises in the workout
mistakes: Array<{ // Mistakes made
mistake: string,
count: number
}>,
average_accuracy?: number // Average accuracy (0-1, optional)
}
```
**exercise_overview Item Structure:**
```
{
exercise_title: string, // Exercise name
exercise_id: string, // Unique exercise ID
time_spent: number, // Time on exercise (seconds)
perfect_hold_position: number, // Time in correct position (timer-based)
repeats: number, // Reps completed
total_required_reps: number, // Target reps
total_required_time: number, // Target time (seconds)
calories: number, // Calories (2 decimal places)
mistakes: Array<{ // Detailed mistake breakdown
mistake: string,
count: number
}>,
mistake_count: number, // Total mistakes for exercise
accuracy_reps?: number[], // Per-rep accuracy scores (optional)
average_accuracy?: number // Average accuracy 0-100 (optional)
}
```
### Camera & Frame Events
Events for camera tracking and frame detection.
| Event | Data Fields | Description |
|-------|-------------|-------------|
| left_camera_frame | date: string ("dd mm yyyy hh:mm:ss") | User left camera view |
| returned_camera_frame | date: string ("dd mm yyyy hh:mm:ss") | User returned to camera view |
| check_frame_completed | message: string ("Person stepped into frame") | Frame check completed |
| camera_selector_opened | message: array (available cameras) | Camera selector opened |
| camera_selected | id: string, label: string, isMirrorCamera: boolean | Camera selected by user |
### Plans & Programs
Events for workout plans and programs.
| Event | Data Fields | Description |
|-------|-------------|-------------|
| plan_unlocked | id: string, img: string, title: string, date: string | Plan unlocked/selected |
| plan_opened | id: string | Plan result screen rendered. Fires for both goal-based and personalized plans |
| plan_onboarding_plan_created | data: { plan_id: string, plan_type: string } | A new plan was created from onboarding/assessment. Does NOT fire on revisits to an existing personalized plan |
| plan_progression_saved | data: { planType: string, weekNumber: number, dayNumber: number, source?: string } | Plan day progression was saved successfully after a plan workout finished. `planType` is `"goal-based"` or `"personalized"`. `source: "offline_outbox"` is present only when the save was recovered after a lost connection — live saves omit it |
| plan_progression_failed | data: object | Plan progression save failed (network/server error) |
| personalized_plan_exit | workout: string, date: string | Exit from personalized plan |
| remind_me_later_clicked | - | User tapped "Remind me later" on the Assessment screen inside the plan-onboarding flow. Only fires from the plan-onboarding page |
**Note on plan tracking:** When you launch a plan workout directly via the SDK, you can pass `planId`, `planType`, and `progressWorkoutId` in the initial PostMessage configuration so the SDK associates the workout session with the correct plan. After the workout finishes, you'll receive either `plan_progression_saved` (success) or `plan_progression_failed` (error). See [Plan Context configuration](/docs/customization-parameters/plan-context) for details.
**Offline save recovery:** if a save fails (e.g. the user loses connection on the results screen), the result is stored locally and automatically re-sent when the connection returns or the app is reopened. `plan_progression_saved` (and `workout_session_saved`) can therefore arrive **after a delay** instead of only at save time — make your handler idempotent and treat `source` as optional.
### Challenge Events
Events for challenge mode.
| Event | Data Fields | Description |
|-------|-------------|-------------|
| challenge_started | exerciseId: string | Challenge exercise started |
| challenge_completed | repCount: number, mistakes: number | Challenge completed |
| challenge_exit | workout: string, date: string | Exit from challenge |
### Leaderboard Events
Events for leaderboard functionality.
| Event | Data Fields | Description |
|-------|-------------|-------------|
| highlighted_user | data: object | User's leaderboard position |
**highlighted_user Data Structure:**
```
{
username: string, // User's username
score: number, // User's score
position: number // Leaderboard position (1-based)
}
```
### Navigation Events
Events for app navigation.
| Event | Data Fields | Description |
|-------|-------------|-------------|
| kinestex_home_exit | workout: string, date: string | Exit from KinesteX home |
| navigation_back | data: { exercise_index: number, total_exercises: number } | User navigated back to a previous exercise during a workout |
### Feedback Events
Events dispatched when a user submits feedback.
| Event | Description |
|-------|-------------|
| feedback_submitted | User submitted training rating or per-exercise feedback |
**Training Feedback Payload (source: "training_feedback"):**
```
{
type: "feedback_submitted",
source: "training_feedback",
rating: number, // User rating (e.g., 1-5)
is_like: boolean, // Whether the user liked the workout
description: string, // Optional text feedback
workout_id: string, // Workout ID
workout_title: string // Workout name
}
```
**Per-Exercise Feedback Payload (source: "exercise_feedback"):**
```
{
type: "feedback_submitted",
source: "exercise_feedback",
workout_id: string, // Workout ID
workout_title: string, // Workout name
feedbacks: Array<{ // Per-exercise feedback entries
exercise_id: string, // Exercise ID
exercise_title: string, // Exercise name
is_like: boolean, // Whether the user liked the exercise
description: string // Optional text feedback
}>
}
```
### AI Trainer Events
Events emitted by the [AI Trainer Chat](/docs/ai-trainer-chat) view. On Swift/Kotlin/Flutter they arrive through the generic `custom_type` / `CustomType` case with the parsed JSON as a dictionary/map.
| Event | Data Fields | Description |
|-------|-------------|-------------|
| trainer_schedule_next_workout | scheduledFor: string, sessionType: string | User picked a date/time for their next session. `scheduledFor` is `YYYY-MM-DDTHH:MM` device-local time; `sessionType` is `"workout"` or `"assessment"` (treat missing as `"workout"`) |
| open_subscription_flow | source: string, date: string | A non-subscribed user tried to generate a workout — present your subscription flow and post `subscription_result` back (`"purchased"` or `"dismissed"`) |
| trainer_assessment_started | date: string | User began the in-app fitness assessment |
| trainer_assessment_completed | date: string, results: object, fitnessLevel: string | Assessment finished — per-exercise reps and level (`results.squats` / `results.pushups`) plus an overall `fitnessLevel` (`"beginner"` / `"intermediate"` / `"advanced"`) |
| trainer_assessment_skipped | date: string, reason: string | User chose to set their fitness level manually (`reason: "set_manually"`) |
| trainer_profile_updated | source: string, profile: object | The user's fitness profile changed — carries the full current profile; `source` is `"onboarding"`, `"assessment"`, or `"chat"`. Persist it on your side |
| workout_exit_request | - | The user exited a workout that was launched from the trainer |
Full payload examples and the subscription handshake: [AI Trainer Chat guide](/docs/ai-trainer-chat) and the [subscription gating guide](/docs/guides/guide-subscription-gating).
### Session & Upload Events
Events related to workout session saving and motion recording uploads. These events are only dispatched when `shouldSendStats: true` is passed in the SDK configuration.
| Event | Data Fields | Description |
|-------|-------------|-------------|
| workout_session_saved | data: object | Workout session successfully saved to backend. May arrive after a delay when the save was recovered by the offline outbox (same payload) — see the offline note under [Plans & Programs](/docs/data-points/plans-programs) |
| session_save_complete | - | Motion recording uploads finished successfully |
| motion_upload_progress | data: { completed: number, total: number } | Motion recording upload progress |
| motion_upload_error | data: { error: string } | Motion recording upload failed or timed out |
| workout_completion_overlay_dismissed | - | User dismissed the workout completion celebration overlay |
**workout_session_saved Data Structure:**
```
{
session_id: number, // Backend-assigned session ID
workout_title: string, // Name of the workout
accuracy_score: number, // Overall accuracy (0-100)
efficiency_score: number, // Efficiency score (0-100)
completion_percentage: number, // How much of the workout was completed (0-100)
completed_reps_count: number, // Total reps completed
calories_burned: number // Estimated calories burned
}
```
**motion_upload_progress Data Structure:**
```
{
completed: number, // Number of exercise recordings uploaded so far
total: number // Total number of exercise recordings to upload
}
```
### Error & Status Events
Events for errors, warnings, and active time tracking.
| Event | Data Fields | Description |
|-------|-------------|-------------|
| error_occurred | data: string | General error message |
| error_occurred | message: string | Alternative error format |
| error_occurred | data: string, error: any | Error with details |
| warning | data: string | Warning message |
| total_active_seconds | number | Active workout time (sent every 5s, pauses when user leaves camera frame) |
| ios_video_fallback_activated | reason, videoUrl, readyState, userAgent | iOS dropped to image-based playback after the full recovery ladder (silent retry → tap-to-play prompt → image mode) failed. The exercise still runs in a degraded-but-functional mode |
**ios_video_fallback_activated Payload:**
```
{
type: "ios_video_fallback_activated",
reason: string, // which recovery stage failed (see below)
videoUrl: string, // URL of the affected video
readyState: number | null, // HTMLMediaElement readyState; null when no video element was available
userAgent: string // Device user agent string
}
```
`reason` is one of:
- `"stuck_after_tap"` — user tapped the prompt but the video still had no media data
- `"tap_prompt_timeout"` — the tap-to-play prompt was ignored (~5s) and auto-advanced to image mode
- `"tap_play_element_missing"` — the video element was gone when the user tapped
- `"tap_play_rejected_"` — the gesture-driven play was rejected, e.g. `"tap_play_rejected_NotAllowedError"`
(The former single value `"video_stuck_at_metadata"` is no longer emitted — prefer treating any occurrence of this event as "iOS entered image mode" and branch on the reason prefixes only if needed. Handle `readyState` as nullable.)
Use this event for analytics or to surface a notice in your UI when iOS playback degrades. Only listen for it if you need visibility into iOS playback issues — no integrator action is required.
### Assessment Exit Events
Events dispatched when a user exits an assessment before completing it.
| Event | Data Fields | Description |
|-------|-------------|-------------|
| assessment_exit | exerciseId: string | User exited the assessment early (before results) |
| assessment_exit_results | exerciseId: string | User exited the assessment from the results screen |
**assessment_exit Payload:**
```
{
type: "assessment_exit",
data: {
exerciseId: string // The assessment exercise identifier
}
}
```
**assessment_exit_results Payload:**
```
{
type: "assessment_exit_results",
data: {
exerciseId: string // The assessment exercise identifier
}
}
```
Use `assessment_exit` to detect when users abandon an assessment mid-session, and `assessment_exit_results` to detect when they leave after viewing their results.
### Assessment Overview
AI-powered health assessments with two event types:
- **assessment_overview**: Sent when results page loads
- **assessment_completed**: Sent when user clicks restart or finish
**Common Fields (All Assessments):**
| Field | Type | Description |
|-------|------|-------------|
| type | string | "assessment_overview" or "assessment_completed" |
| assessmentType | string | Assessment identifier (e.g., "tug", "sls") |
| date | Date | Timestamp when completed |
| time | number | Total assessment time (seconds) |
| steps | number? | Estimated step count (walking assessments) |
**Assessment Types:**
- **Mobility**: TUG (tug), Gait Speed Test (gaitspeedtest)
- **Balance**: SLS (sls), SBSS (sbss), STSS (stss), Full Tandem (fulltandem)
- **Functional**: STS (sts), Five Times STS (fivetimessts), FRT (frt)
- **Range of Motion**: Shoulder ROM (romshoulder)
- **Games**: Balloon Pop (balloonpop), Color Chase (colorchase), Alien Squat Shooter (aliensquatshooter)
**Risk Levels:**
| Value | Meaning |
|-------|---------|
| low | Good performance, minimal fall/balance risk |
| moderate | Some limitations, may benefit from training |
| high | Significant limitations, balance training recommended |
### Mobility Assessments
**TUG (Timed Up and Go) - assessmentType: "tug"**
Stand-walk-turn-return-sit timing test.
| Field | Type | Description |
|-------|------|-------------|
| time | number | Total completion time (seconds) |
| steps | number | Estimated step count |
| standingUpTime | number | Time to stand from seated (seconds) |
| sittingDownTime | number | Time to sit at end (seconds) |
| walkingForwardTime | number | Time walking to 3m marker (seconds) |
| walkingBackwardTime | number | Time walking back (seconds) |
| turningTime | number | Time spent turning (seconds) |
| backBendingAngleSitting | number[] | Back angles during sitting countdown (degrees) |
| backBendingAngleStanding | number[] | Back angles during movement (degrees) |
| averageSpeedMs_tug | number | Average walking speed. Formula: 6m / time |
| avgBackBendingSitting | number? | Average back angle sitting (degrees) |
| avgBackBendingStanding | number? | Average back angle standing (degrees) |
---
**Gait Speed Test - assessmentType: "gaitspeedtest"**
Walking speed measurement over 4 meters.
| Field | Type | Description |
|-------|------|-------------|
| time | number | Total test time (seconds) |
| steps | number | Estimated step count |
| standingTime | number | Time in standing phase (seconds) |
| walkingTime | number | Active walking time (seconds) |
| averageGaitSpeed | number | Gait speed. Formula: 4m / time |
### Balance Assessments
**SLS (Single Leg Stand) - assessmentType: "sls"**
| Field | Type | Description |
|-------|------|-------------|
| rightTime | number | Duration on right leg (seconds) |
| leftTime | number | Duration on left leg (seconds) |
| symmetryScore_sls | number? | Leg symmetry % (0-100). Formula: (min/max) * 100 |
| riskLevel_sls | string | "low", "moderate", or "high" |
**Risk Calculation:**
- Low: Min time >=20s AND symmetry good (difference <=5s)
- Moderate: Min time >=10s AND average >=15s
- High: All other cases
---
**SBSS (Side-by-Side Stand) - assessmentType: "sbss"**
| Field | Type | Description |
|-------|------|-------------|
| timeInProperPosition_sbss | number | Time in correct stance (max 10s) |
| maxShoulderShift_sbss | number | Max lateral shoulder shift (% of shoulder width) |
| maxHipShift_sbss | number | Max lateral hip shift (% of hip width) |
| feetMoved_sbss | boolean | Whether feet moved |
| riskLevel_sbss | string | "low", "moderate", or "high" |
**Risk Calculation:**
- High: Feet moved OR (time <7s AND sway >=30%)
- Moderate: Time >=7s AND sway <30%
- Low: Time >=9.5s AND sway <15%
---
**STSS (Semi-Tandem Stand) - assessmentType: "stss"**
| Field | Type | Description |
|-------|------|-------------|
| timeInProperPosition_stss | number | Time in correct stance (max 10s) |
| maxShoulderShift_stss | number | Max shoulder shift (% of width) |
| maxHipShift_stss | number | Max hip shift (% of projection) |
| feetMoved_stss | boolean | Whether feet moved |
| riskLevel_stss | string | "low", "moderate", or "high" |
Risk calculation same as SBSS.
---
**Full Tandem Stand - assessmentType: "fulltandem"**
| Field | Type | Description |
|-------|------|-------------|
| time | number | Total test time (seconds) |
| timeInProperPosition_fulltandem | number | Time in heel-to-toe stance (max 10s) |
| maxShoulderShift_fulltandem | number | Max shoulder shift (%) |
| maxHipShift_fulltandem | number | Max hip shift (%) |
| feetMoved_fulltandem | boolean | Whether feet moved |
| testFailed_fulltandem | boolean | Whether test was terminated early |
| terminationReason_fulltandem | string? | Reason for early termination |
| riskLevel_fulltandem | string | "low", "moderate", or "high" |
**Risk Calculation:**
- Low: Time >=10s AND no feet movement
- Moderate: Time >=5s
- High: Time <5s
### Functional Assessments
**STS (30-Second Sit-to-Stand) - assessmentType: "sts"**
| Field | Type | Description |
|-------|------|-------------|
| reps | number | Total reps completed in 30 seconds |
| averageSittingTime | number | Average time sitting per rep (seconds) |
| averageStandingTime | number | Average time standing per rep (seconds) |
| avgTimePerRep_sts | number? | Average seconds per rep. Formula: 30 / reps |
| repTimeVariance_minRepTime | number? | Fastest rep time (seconds) |
| repTimeVariance_maxRepTime | number? | Slowest rep time (seconds) |
| repTimeVariance_minRepIndex | number? | Which rep was fastest (1-indexed) |
| repTimeVariance_maxRepIndex | number? | Which rep was slowest (1-indexed) |
---
**Five Times STS - assessmentType: "fivetimessts"**
| Field | Type | Description |
|-------|------|-------------|
| time | number | Total completion time for 5 reps (seconds) |
| averageSittingTime | number | Average time sitting (seconds) |
| averageStandingTime | number | Average time standing (seconds) |
---
**FRT (Functional Reach Test) - assessmentType: "frt"**
| Field | Type | Description |
|-------|------|-------------|
| reach | number | Maximum forward reach (cm) |
| maxHeelLift | number | Maximum heel lift detected (cm) |
| heelLiftCount | number | Count of heel lift violations |
| legLiftCount | number | Count of leg lift violations |
| testCompleted | boolean | Whether test finished normally |
| endReason | string? | Reason for early termination |
| riskLevel_frt | string? | "low", "moderate", or "high" |
**End Reason Values:**
- "Feet moved out of zone"
- "User left zone"
- "User turned forward"
- "Arm dropped completely"
- "Reference lost"
- "Unhandled state"
**Risk Calculation:**
- High: Test not completed OR reach <=15cm
- Moderate: Reach 15-25cm
- Low: Reach >25cm
### Range of Motion Assessments
**Shoulder ROM - assessmentType: "romshoulder"**
A quick range-of-motion check for the shoulders. The user stands facing the camera and lifts one arm at a time straight out to the side (shoulder abduction), as high as comfortably possible. The system tracks the peak abduction angle reached by each arm, compares left and right sides, and flags meaningful asymmetry.
Useful for tracking recovery, spotting asymmetry, and observing mobility improvements session over session.
| Field | Type | Description |
|-------|------|-------------|
| romMovement | string | Type of ROM movement assessed. Primary value: "shoulder_abduction" |
| romMaxLeft | number | Maximum ROM achieved on the left side (degrees, rounded to 0 decimals, range 0-180+) |
| romMaxRight | number | Maximum ROM achieved on the right side (degrees, rounded to 0 decimals, range 0-180+) |
| romMinLeft | number | Minimum ROM recorded on the left side (degrees, rounded to 0 decimals, range 0-180+) |
| romMinRight | number | Minimum ROM recorded on the right side (degrees, rounded to 0 decimals, range 0-180+) |
| romSymmetryDelta | number | Absolute difference between max left and max right (degrees). Formula: abs(romMaxLeft - romMaxRight) |
| romSymmetryFlagDegrees | number | Asymmetry threshold (degrees). Defaults to 10 if not provided |
| romAsymmetric | boolean | true if romSymmetryDelta > romSymmetryFlagDegrees, else false |
**Notes:**
- All angle measurements are in degrees.
- Numeric values are rounded to 0 decimal places.
- `romAsymmetric` is computed by comparing `romSymmetryDelta` against `romSymmetryFlagDegrees`.
### Game Assessments
**Balloon Pop - assessmentType: "balloonpop"**
| Field | Type | Description |
|-------|------|-------------|
| gameScore | number | Total balloons popped |
| averageReactionTime | string | Average time to pop (seconds) |
| maxIdleTime | string | Max time without action (seconds) |
| averageSpeed_balloonpop | number | Balloons/second. Formula: gameScore / 30 |
| masteryTitle_balloonpop | string | Achievement tier |
**Mastery Titles:**
| Score | Title |
|-------|-------|
| >=30 | pop_tastic_hero |
| >=25 | magic_popper |
| >=20 | super_duper_popper |
| >=15 | sparkly_popper |
| >=10 | giggly_popper |
| >=5 | bouncy_bubbler |
| <5 | tiny_popper |
---
**Color Chase - assessmentType: "colorchase"**
| Field | Type | Description |
|-------|------|-------------|
| gameScore | number | Total score achieved |
| levelReached | number | Highest level completed |
| totalDuration | string | Total game duration (seconds) |
| averageReactionTime | string | Average reaction time per tap (seconds) |
| maxIdleTime | string | Max time between taps (seconds) |
| masteryTitle_colorchase | string | Achievement tier |
**Mastery Titles:**
| Level | Title |
|-------|-------|
| >=10 | color_chase_legend |
| >=8 | magic_color_wizard |
| >=6 | super_color_star |
| >=4 | shiny_sequencer |
| >=2 | rainbow_chaser |
| >=1 | color_buddy |
| <1 | little_color_finder |
---
**Alien Squat Shooter - assessmentType: "aliensquatshooter"**
| Field | Type | Description |
|-------|------|-------------|
| gameScore | number | Total aliens destroyed |
| squatsPerformed | number | Number of squats completed |
| totalDuration | string | Total game duration (seconds) |
| averageSquatRate | string | Squats per second |
| maxTimeBetweenSquats | string | Max idle between squats (seconds) |
| averageAlienDestroyTime | string | Average time to destroy alien (seconds) |
| masteryTitle_aliensquatshooter | string | Achievement tier |
**Mastery Titles:**
| Score | Title |
|-------|-------|
| >=25 | alien_annihilator |
| >=20 | cosmic_blaster |
| >=15 | star_shooter |
| >=10 | galactic_gunner |
| >=5 | space_squatter |
| <5 | rookie_defender |
---
**Health Benefits (All Games)**
Included in payload for all game types.
```
healthBenefits: {
heartDiseaseReduction: number, // Estimated % reduction (max 15)
diabetesReduction: number, // Estimated % reduction (max 20)
obesityReduction: number, // Estimated % reduction (max 10)
depressionReduction: number // Estimated % reduction (max 15)
}
```
**Health Benefit Calculation:**
`reduction = min((gameScore / 100) * maxReduction, maxReduction)`
### Event Flow Examples
**Complete Workout Flow:**
1. `kinestex_launched` - Application starts
2. `workout_opened` - User views workout details
3. `workout_started` - Workout begins
4. `returned_camera_frame` / `left_camera_frame` - Frame tracking
5. Multiple `exercise_completed` - Each exercise finished
6. `workout_overview` - Summary statistics
7. `exercise_overview` - All exercises summary
8. `workout_completed` - User exits statistics
9. `exit_kinestex` - Application closed
**Challenge Flow:**
1. `challenge_started` - Challenge begins
2. `exercise_completed` - Exercise finished
3. `challenge_completed` - Challenge complete
4. `challenge_exit` - Exit from challenge
**Assessment Flow:**
1. `kinestex_launched` - Application starts
2. Assessment performed (user follows on-screen instructions)
3. `assessment_overview` - Results page loads with all metrics
4. `assessment_completed` - User clicks restart or finish
5. `assessment_exit_results` - User exits from results screen (or `assessment_exit` if they exit early)
6. `exit_kinestex` - Application closed
---
## Customization Parameters
This section describes every customization parameter the KinesteX SDK accepts, what each one does internally, and — critically — **which integration option(s) it applies to**. Parameters passed to an integration that doesn't consume them are stored but silently ignored, so always check the **Applies to** column before recommending or using a parameter.
**Parameter Passing Methods:**
- **Direct SDK Support**: Pass directly to SDK initialization or view creation methods
- **customParams / customParameters**: Additional parameters passed via a custom parameters object
- **HTML/JS postData**: All parameters passed as a flat object via the postMessage API
- **URL query string**: Most parameters can also be passed as URL query params. Values sent via postMessage override URL values — except `?style=` and `?delegate=`, where the URL wins, and `?debug=`, where either source enables it. See the precedence table in [URL Parameters](/docs/customization-parameters/url-parameters).
**Integration scope labels used throughout this section:**
| Label | Integration option (SDK method) | Internal route |
|-------|--------------------------------|----------------|
| **All** | Every integration option | — |
| **Main** | Complete UX — `createMainView` | `/` |
| **Workout** | `createWorkoutView` | `/workout/{id}` |
| **Plan** | `createPlanView` | `/plan/{id}` |
| **Personalized Plan** | `createPersonalizedPlanView` | `/personalized-plan` |
| **Custom Workout** | `createCustomWorkoutView` | `/custom-workout` |
| **Workout player** | Shorthand for any flow that runs the standard exercise player and statistics screen: Main, Workout, Plan, Personalized Plan, Custom Workout (and Challenge where noted) | — |
| **Challenge** | `createChallengeView` | `/challenge` |
| **Leaderboard** | `createLeaderboardView` | `/leaderboard` |
| **Experiences (Games)** | `createExperienceView` with `balloonpop`, `colorchase`, `aliensquatshooter` | `/experiences/{game}` |
| **Assessments** | `createExperienceView` with `assessment` | `/experiences/assessment` |
| **Camera** | `createCameraComponent` | `/camera` |
| **Plan Onboarding** | `createCustomComponentView` with route `plan-onboarding` | `/plan-onboarding` |
| **AI Trainer** | [AI Trainer Chat](/docs/ai-trainer-chat) | `/trainer` |
**Quick task index:**
- Test the integration **without physically performing exercises** → [`videoURL` — Testing & Simulation](/docs/customization-parameters/testing-simulation)
- Save workout results to the KinesteX backend → [`shouldSendStats`](/docs/customization-parameters/session-data)
- Jump into the middle of a workout → [`start_from_exercise`](/docs/customization-parameters/workout-configuration)
- Switch exercises at runtime in the Camera component → [Camera Component parameters](/docs/customization-parameters/camera-component-params)
- Control a running workout (pause/mute) → [Workout Activity Actions](/docs/customization-parameters/workout-activity-actions)
### Required Parameters
These parameters are mandatory for successful SDK initialization. **Applies to: All integrations.**
| Parameter | Type | Description |
|-----------|------|-------------|
| userId | string | Unique identifier for the user. Must be at least 2 characters — a shorter/missing value posts an `error_occurred` message instead of verifying. Used for tracking progress, analytics, and personalization |
| company | string | Company name associated with the API key. Determines the default theme name and which content the user can access. Compared case-insensitively (lowercased internally) |
| key | string | API key for authentication. Sent as the `x-api-key` header on the verify call and used as the company content filter |
**Session-based authentication:** integrations that use KinesteX session tokens may pass `session` (a session ID string) instead of `key`. Verification succeeds when **either** `key` or `session` is present. On the session path the user identity and subscription status are resolved server-side, and the verify response's `is_subscribed` overrides any host-passed `isSubscribed` flag.
**SDK Support:** All platforms support these as direct parameters.
**Required Parameters Setup**
_Swift (iOS)_
```swift
// Direct SDK support
let kinestex = KinesteXAIKit(
apiKey: "YOUR_API_KEY",
companyName: "YOUR_COMPANY",
userId: "unique-user-id"
)
```
_Kotlin (Android)_
```kotlin
// Direct SDK support
KinesteXSDK.initialize(
context = this,
apiKey = "YOUR_API_KEY",
companyName = "YOUR_COMPANY",
userId = "unique-user-id"
)
```
_React Native_
```jsx
// Direct support in postData
const postData: IPostData = {
key: 'YOUR_API_KEY',
userId: 'unique-user-id',
company: 'YOUR_COMPANY',
};
```
_Flutter_
```dart
// Direct SDK support
await KinesteXAIFramework.initialize(
apiKey: "YOUR_API_KEY",
companyName: "YOUR_COMPANY",
userId: "unique-user-id",
);
```
_HTML / JavaScript_
```html
// Direct in postData object
const postData = {
userId: "unique-user-id",
company: "YOUR_COMPANY",
key: "YOUR_API_KEY",
};
```
_React (TypeScript)_
```tsx
// Direct support in postData
const postData: IPostData = {
key: 'YOUR_API_KEY',
userId: 'unique-user-id',
company: 'YOUR_COMPANY',
};
```
### Testing & Simulation (videoURL)
**How to test the integration without physically performing the exercises.**
**Applies to: Workout player (Main, Workout, Plan, Personalized Plan, Custom Workout), Camera, Assessments.** Not supported by the games (Balloon Pop, Color Chase, Alien Squat Shooter) — they always use the live camera.
| Parameter | Type | Default | Applies to | What it does internally |
|-----------|------|---------|------------|-------------------------|
| videoURL | string | — | Workout player, Camera, Assessments | Replaces the live camera stream as the motion-tracking input. Instead of opening the device camera (`getUserMedia`), the SDK sets the provided URL as the source of its internal `` element and runs the full MediaPipe pose-tracking pipeline on that video — rep counting, form analysis, accuracy scoring, and statistics all work exactly as if a real person were moving in front of the camera |
**Behavior details:**
- The video **loops continuously** until the session ends — it never stops on its own, so reps keep counting for as long as the exercise runs.
- The video is loaded with `crossOrigin="anonymous"`, so the URL must be **CORS-accessible** (served with `Access-Control-Allow-Origin`), otherwise landmark extraction fails.
- Pose processing runs on the main thread when `videoURL` is set (the web-worker fast path is only used for the live camera). This is intentional and does not change results.
- No camera permission is requested for the video-driven pipeline itself.
- The video should show one fully visible person performing the target exercise, filmed like a normal front-facing camera feed. You can use the exercise's own demo video from the [Content API](/docs/content-api) (`video_URL` field) as a ready-made input.
**Recommended testing recipe:**
1. Pass `videoURL` pointing at a recording of the exercise being performed.
2. Pass `shouldSendStats: true` if you want the simulated session saved to the backend ([Session & Data Saving](/docs/customization-parameters/session-data)) so you can verify it via the Workout Sessions API.
3. Optionally add `showSilhouette: false` to skip the camera-positioning (silhouette) screen, or `immediateShowSkip: true` to show its Skip button instantly.
4. Optionally use `start_from_exercise` (with `completed_exercises` and `start_from_rest: true`) to jump to a later part of the workout ([Workout Configuration](/docs/customization-parameters/workout-configuration)).
**When NOT to use `videoURL`:** if you only need to click through screens without any tracking data, `motionTrackingEnabled: false` (see [Motion Tracking Settings](/docs/customization-parameters/motion-tracking-settings)) converts exercises to auto-advancing timers — but then **no reps are counted and no accuracy data is produced**. Use `videoURL` when you need realistic tracking data without a person.
**Simulate a workout with a prerecorded video**
_Swift (iOS)_
```swift
// Via customParams — full tracking pipeline runs on the video
kinestex.createWorkoutView(
workout: "Fitness Lite",
user: user,
customParams: [
"videoURL": "https://cdn.yourapp.com/test/squats-demo.mp4",
"shouldSendStats": true // save the simulated session
],
isLoading: $isLoading,
onMessageReceived: { /* ... */ }
)
```
_Kotlin (Android)_
```kotlin
// Via customParams — full tracking pipeline runs on the video
KinesteXSDK.createWorkoutView(
context = this,
workoutName = "Fitness Lite",
customParams = mutableMapOf(
"videoURL" to "https://cdn.yourapp.com/test/squats-demo.mp4",
"shouldSendStats" to true // save the simulated session
),
isLoading = viewModel.isLoading,
onMessageReceived = { message -> handleWebViewMessage(message) },
permissionHandler = this
)
```
_React Native_
```jsx
// Via customParameters — full tracking pipeline runs on the video
const postData: IPostData = {
key: 'YOUR_API_KEY',
userId: 'test-user-1',
company: 'YOUR_COMPANY',
customParameters: {
videoURL: 'https://cdn.yourapp.com/test/squats-demo.mp4',
shouldSendStats: true, // save the simulated session
},
};
```
_Flutter_
```dart
// Via customParams — full tracking pipeline runs on the video
KinesteXAIFramework.createWorkoutView(
workoutName: "Fitness Lite",
isShowKinestex: showKinesteX,
isLoading: ValueNotifier(false),
customParams: {
"videoURL": "https://cdn.yourapp.com/test/squats-demo.mp4",
"shouldSendStats": true, // save the simulated session
},
onMessageReceived: (message) { handleWebViewMessage(message); },
);
```
_HTML / JavaScript_
```html
// Direct in postData object — full tracking pipeline runs on the video
const postData = {
userId: "test-user-1",
company: "YOUR_COMPANY",
key: "YOUR_API_KEY",
videoURL: "https://cdn.yourapp.com/test/squats-demo.mp4",
shouldSendStats: true, // save the simulated session
};
```
_React (TypeScript)_
```tsx
// Via customParameters — full tracking pipeline runs on the video
const postData: IPostData = {
key: 'YOUR_API_KEY',
userId: 'test-user-1',
company: 'YOUR_COMPANY',
customParameters: {
videoURL: 'https://cdn.yourapp.com/test/squats-demo.mp4',
shouldSendStats: true, // save the simulated session
},
};
```
### User Profile
Parameters that define user characteristics for personalized content and recommendations.
| Parameter | Type | Default | Applies to | What it does internally |
|-----------|------|---------|------------|-------------------------|
| age | number | 30 | All | Sent to the verify API as `birth_year` and used for calorie (BMR) estimation. Accepts either an **age** (1–120) or a literal **birth year** (1900–current year) — both are converted to a birth year internally |
| gender | string | "male" | All | `"male"` or `"female"`. Sent to the verify API; used in the calorie (BMR) formula and AI Trainer profile prefill. (For which demo videos are shown, see `content_gender` under [Language & Localization](/docs/customization-parameters/language-localization)) |
| height | number | 160 | All | Height in **cm**. Sent to the verify API; used in the calorie (BMR) formula |
| weight | number | 70 | All | Weight in **kg**. Sent to the verify API; used in the calorie (BMR) formula |
| fitness_level | string | "beginner" | Main, Plan | `"beginner"`, `"intermediate"`, or `"advanced"`. Filters and sorts the plan list by difficulty. Also attached to analytics |
| lifestyle | string | "lightly_active" | All | `"sedentary"`, `"lightly_active"`, `"active"`, or `"very_active"`. Sent to the verify API and synced to the user's backend profile; prefills the AI Trainer profile |
| body_parts | string[] | [] | Workout, AI Trainer | Filters the exercise list shown on the workout detail page to workouts targeting those body parts, and prefills the AI Trainer's target muscle groups (unknown values are dropped) |
| plan_type | string | null | — (analytics only) | Recorded in analytics only. **Has no functional effect on plan structure in the current version** — do not use it to change plan behavior |
**Important — defaults vs. provided values:** the SDK tracks which of `age`/`gender`/`height`/`weight`/`lifestyle` you *explicitly* passed. Calorie estimates (Mifflin–St Jeor BMR) and AI Trainer profile prefill only use **explicitly provided** values — if any of weight/height/age/gender is missing, calories fall back to a fixed default BMR instead of silently using the defaults above. Passing a complete profile therefore gives noticeably more accurate calorie numbers.
**Backend profile sync:** when all of `age` (valid), `height` > 0, `weight` > 0, and `lifestyle` are provided, the SDK syncs them once per user/company to the KinesteX user profile (used by personalized plans). Partial profiles are never synced, so existing server data is not overwritten with defaults.
**Note:** There is no BMI calculation in the SDK — height/weight feed calorie estimation (BMR) only.
**SDK Support:** Most platforms have direct support via a UserDetails object or postData fields.
**User Profile Configuration**
_Swift (iOS)_
```swift
// Direct SDK support via UserDetails
let user = UserDetails(
age: 30,
height: 180,
weight: 75,
gender: .Male,
lifestyle: .Active
)
kinestex.createView(
user: user,
// ... other params
)
```
_Kotlin (Android)_
```kotlin
// Direct SDK support via UserDetails
val userDetails = UserDetails(
age = 30,
height = 180,
weight = 75,
gender = Gender.MALE,
lifestyle = Lifestyle.ACTIVE
)
KinesteXSDK.createView(
user = userDetails,
// ... other params
)
```
_React Native_
```jsx
// Direct support in postData
const postData: IPostData = {
key: 'YOUR_API_KEY',
userId: 'user-123',
company: 'YOUR_COMPANY',
// User profile - direct support
age: 30,
height: 180, // cm
weight: 75, // kg
gender: 'Male',
lifestyle: Lifestyle.Active,
};
```
_Flutter_
```dart
// Direct SDK support via UserDetails
final userDetails = UserDetails(
age: 30,
height: 180,
weight: 75,
gender: Gender.Male,
lifestyle: Lifestyle.Active,
);
KinesteXAIFramework.createMainView( // or any other create*View method
user: userDetails,
// ... other params
);
```
_HTML / JavaScript_
```html
// Direct in postData object
const postData = {
userId: "user-123",
company: "YOUR_COMPANY",
key: "YOUR_API_KEY",
// User profile
age: 30,
height: 180,
weight: 75,
gender: "Male",
lifestyle: "active",
};
```
_React (TypeScript)_
```tsx
// Direct support in postData
const postData: IPostData = {
key: 'YOUR_API_KEY',
userId: 'user-123',
company: 'YOUR_COMPANY',
// User profile - direct support
age: 30,
height: 180, // cm
weight: 75, // kg
gender: 'Male',
lifestyle: Lifestyle.Active,
};
```
### Theme & Appearance
Control the visual appearance of the application. **Applies to: All integrations.**
| Parameter | Type | Default | Applies to | What it does internally |
|-----------|------|---------|------------|-------------------------|
| style | "dark" or "light" | "dark" | All | Selects the theme mode requested from the backend at verification and switches the entire UI color scheme |
| themeName | string | Company name | All | Which theme configuration to load from the backend (themes are managed in the Admin Dashboard). The resolved theme's CSS variables are cached in the WebView's localStorage and only re-downloaded when the theme version changes |
**Precedence note:** the URL parameter `?style=dark` / `?style=light` **takes priority over the `style` value sent via postMessage** — this is the one parameter where the URL wins. If you see the wrong mode, check the launch URL first.
**SDK Support:**
- **Swift:** Direct support via `IStyle` class passed to view creation methods (hex values with #)
- **Flutter:** Direct support via `IStyle` class passed to view creation methods
- **Kotlin:** Direct support via `IStyle` data class passed to view creation methods (hex values without #)
- **React Native/React:** Direct support via `style` object in postData
- **HTML/JS:** Direct in postData object
**Theme Configuration**
_Swift (iOS)_
```swift
// Direct SDK support via IStyle class
let customStyle = IStyle(
style: "light",
themeName: "CustomBrand"
)
kinestex.createWorkoutView(
workout: "Fitness Lite",
user: user,
style: customStyle,
isLoading: $isLoading,
onMessageReceived: { /* ... */ }
)
```
_Kotlin (Android)_
```kotlin
// Direct SDK support via IStyle class
KinesteXSDK.createWorkoutView(
context = this,
workoutName = "Fitness Lite",
style = IStyle(
style = "light",
themeName = "Your Theme Name from Admin Dashboard (default company name)"
),
isLoading = viewModel.isLoading,
onMessageReceived = { message ->
handleWebViewMessage(message)
},
permissionHandler = this
)
```
_React Native_
```jsx
// Direct support via style object
const postData: IPostData = {
key: 'YOUR_API_KEY',
userId: 'user-123',
company: 'YOUR_COMPANY',
style: {
style: 'light', // 'dark' or 'light'
loadingBackgroundColor: 'FFFFFF', // hex without #
},
customParameters: {
themeName: 'CustomBrand', // via customParameters
},
};
```
_Flutter_
```dart
// Direct SDK support via IStyle class
KinesteXAIFramework.createWorkoutView(
workoutName: "Fitness Lite",
isShowKinestex: showKinesteX,
isLoading: ValueNotifier(false),
style: IStyle(
style: 'light', // 'dark' or 'light'
themeName: 'CustomBrand',
),
onMessageReceived: (message) {
handleWebViewMessage(message);
},
);
```
_HTML / JavaScript_
```html
// Direct in postData object
const postData = {
userId: "user-123",
company: "YOUR_COMPANY",
key: "YOUR_API_KEY",
style: "light",
themeName: "CustomBrand",
};
```
_React (TypeScript)_
```tsx
// Direct support via style object
const postData: IPostData = {
key: 'YOUR_API_KEY',
userId: 'user-123',
company: 'YOUR_COMPANY',
style: {
style: 'light', // 'dark' or 'light'
loadingBackgroundColor: 'FFFFFF', // hex without #
},
customParameters: {
themeName: 'CustomBrand', // via customParameters
},
};
```
### Language & Localization
Configure the language for all UI text, voice prompts, and content.
| Parameter | Type | Default | Applies to | What it does internally |
|-----------|------|---------|------------|-------------------------|
| language | string | "en" | All | Loads the UI locale, sets the `Language` header on all subsequent KinesteX API calls (so backend content arrives translated), and selects the language folder for spoken audio cues. Changing language between sessions automatically clears the cached speech audio. Unknown codes silently fall back to English |
| voiceActor | string | "Glinda" | All views with spoken audio | Selects the text-to-speech voice used for all spoken coaching cues (speech files are fetched per voice actor). Changing it clears the cached speech audio so the new voice is downloaded |
| content_gender | string | "female" | Workout player, Challenge, Leaderboard, Assessments | `"male"` or `"female"`. Selects which exercise **demo video** is shown: `"male"` uses the exercise's male demo video where one exists, otherwise the default (female) video is used. This is about the on-screen instructor content, not the user's own gender |
**Supported Languages:**
| Code | Language | RTL Support |
|------|----------|-------------|
| en | English | No |
| es | Spanish | No |
| fr | French | No |
| de | German | No |
| nl | Dutch | No |
| it | Italian | No |
| pt | Portuguese | No |
| ru | Russian | No |
| ar | Arabic | Yes |
| he | Hebrew | Yes |
| hi | Hindi | No |
| bn | Bengali | No |
| id | Indonesian | No |
| da | Danish | No |
| el | Greek | No |
| zh | Chinese (Simplified) | No |
| uz | Uzbek | No |
**SDK Support:** Requires customParams on most platforms.
**Language Configuration**
_Swift (iOS)_
```swift
// Via customParams
kinestex.createView(
customParams: [
"language": "es",
"content_gender": "female"
],
// ... other params
)
```
_Kotlin (Android)_
```kotlin
// Via customParams
KinesteXSDK.createView(
customParams = mutableMapOf(
"language" to "es",
"content_gender" to "female"
),
// ... other params
)
```
_React Native_
```jsx
// Via customParameters
const postData: IPostData = {
key: 'YOUR_API_KEY',
userId: 'user-123',
company: 'YOUR_COMPANY',
customParameters: {
language: 'es',
content_gender: 'female',
},
};
```
_Flutter_
```dart
// Via customParams
KinesteXAIFramework.createMainView( // or any other create*View method
customParams: {
"language": "es",
"content_gender": "female",
},
// ... other params
);
```
_HTML / JavaScript_
```html
// Direct in postData object
const postData = {
userId: "user-123",
company: "YOUR_COMPANY",
key: "YOUR_API_KEY",
language: "es",
content_gender: "female",
};
```
_React (TypeScript)_
```tsx
// Via customParameters
const postData: IPostData = {
key: 'YOUR_API_KEY',
userId: 'user-123',
company: 'YOUR_COMPANY',
customParameters: {
language: 'es',
content_gender: 'female',
},
};
```
### Workout Configuration
Parameters for configuring workout behavior and progression.
| Parameter | Type | Default | Applies to | What it does internally |
|-----------|------|---------|------------|-------------------------|
| planC | string | "Cardio" | Main | Plan category: `"Weight Management"`, `"Strength"`, `"Rehabilitation"`, or `"Cardio"`. On the Complete UX home screen it is written to the user's profile, selects which default plans are loaded, and picks the default challenge/game cards. Ignored by other integration options |
| completed_exercises | string[] | — | Workout player | Exercise IDs the user already finished. **Required (together with the other two rows below) for mid-workout resume to activate** — see note |
| start_from_exercise | string | — | Workout player | Exercise **ID** (not title) to resume from. The workout jumps to that exercise's position in the sequence; if the ID is not found, the workout silently starts from the beginning |
| start_from_rest | boolean | false | Workout player | Must be `true` for resume to activate. The session starts on the rest screen immediately **before** `start_from_exercise` |
| resetPlanProgress | boolean | false | All (verify-time) | Deletes **all** of the user's saved plan progress documents from the backend during verification. Local storage is not touched. Irreversible — intended for test/reset flows |
| on_start_url | string | "/workout/audioCheck" | Workout player, Challenge, Assessments, Training | **Internal route override, not a webhook.** After camera access is granted / Start is pressed, the SDK navigates to this in-app route instead of the default audio-check screen. It never makes an HTTP call to this value |
**Mid-workout resume — all three parameters are required together.** The resume configuration is only applied when `completed_exercises`, `start_from_exercise`, AND `start_from_rest: true` are all present. Passing `start_from_exercise` alone, or `start_from_rest: false`, disables the feature entirely and the workout starts from the first exercise. Matching is by exercise **ID** as it appears in the workout sequence (retrievable from the [Content API](/docs/content-api)).
**Looking for `exercises` / `currentExercise`?** Those belong to the standalone Camera component — see [Camera Component parameters](/docs/customization-parameters/camera-component-params).
**Workout Configuration**
_Swift (iOS)_
```swift
// Resume a workout mid-way: all THREE resume params are required together
kinestex.createWorkoutView(
workout: "Fitness Lite",
user: user,
customParams: [
"completed_exercises": ["exercise-id-1", "exercise-id-2"],
"start_from_exercise": "exercise-id-3", // exercise ID, not title
"start_from_rest": true // must be true or resume is ignored
],
isLoading: $isLoading,
onMessageReceived: { /* ... */ }
)
```
_Kotlin (Android)_
```kotlin
// Resume a workout mid-way: all THREE resume params are required together
KinesteXSDK.createWorkoutView(
context = this,
workoutName = "Fitness Lite",
customParams = mutableMapOf(
"completed_exercises" to listOf("exercise-id-1", "exercise-id-2"),
"start_from_exercise" to "exercise-id-3", // exercise ID, not title
"start_from_rest" to true // must be true or resume is ignored
),
isLoading = viewModel.isLoading,
onMessageReceived = { message -> handleWebViewMessage(message) },
permissionHandler = this
)
```
_React Native_
```jsx
// Resume a workout mid-way: all THREE resume params are required together
const postData: IPostData = {
key: 'YOUR_API_KEY',
userId: 'user-123',
company: 'YOUR_COMPANY',
customParameters: {
completed_exercises: ['exercise-id-1', 'exercise-id-2'],
start_from_exercise: 'exercise-id-3', // exercise ID, not title
start_from_rest: true, // must be true or resume is ignored
},
};
```
_Flutter_
```dart
// Resume a workout mid-way: all THREE resume params are required together
KinesteXAIFramework.createWorkoutView(
workoutName: "Fitness Lite",
isShowKinestex: showKinesteX,
isLoading: ValueNotifier(false),
customParams: {
"completed_exercises": ["exercise-id-1", "exercise-id-2"],
"start_from_exercise": "exercise-id-3", // exercise ID, not title
"start_from_rest": true, // must be true or resume is ignored
},
onMessageReceived: (message) { handleWebViewMessage(message); },
);
```
_HTML / JavaScript_
```html
// Resume a workout mid-way: all THREE resume params are required together
const postData = {
userId: "user-123",
company: "YOUR_COMPANY",
key: "YOUR_API_KEY",
completed_exercises: ["exercise-id-1", "exercise-id-2"],
start_from_exercise: "exercise-id-3", // exercise ID, not title
start_from_rest: true, // must be true or resume is ignored
};
```
_React (TypeScript)_
```tsx
// Resume a workout mid-way: all THREE resume params are required together
const postData: IPostData = {
key: 'YOUR_API_KEY',
userId: 'user-123',
company: 'YOUR_COMPANY',
customParameters: {
completed_exercises: ['exercise-id-1', 'exercise-id-2'],
start_from_exercise: 'exercise-id-3', // exercise ID, not title
start_from_rest: true, // must be true or resume is ignored
},
};
```
### Camera & Pose Detection
Fine-tune the camera and pose detection system. These parameters affect any view that runs the pose-tracking pipeline (Workout player, Camera component, Assessments, Games) unless a narrower scope is listed.
| Parameter | Type | Default | Applies to | What it does internally |
|-----------|------|---------|------------|-------------------------|
| shouldAskCamera | boolean | true | Workout player, Challenge, Assessments | When `false`, the pre-workout camera-permission gate is skipped and navigation proceeds directly (the browser will still ask on first actual camera use). Use when your host app already manages the camera permission |
| shouldShowCameraSelector | boolean | false | Workout player, Camera, Assessments | Shows a camera-switch button on the camera-positioning (check-frame) screens so the user can pick between available camera devices |
| shouldShowOpenCameraSettings | boolean | false | All (camera-help modal) | Switches the camera-permission help modal to a step set that guides the user to their device camera settings (for hosts that can deep-link there) |
| cameraLabel | string | — | All pose-tracking views | Selects the camera device whose **label contains this substring** (e.g. `"back"`, `"wide"`). If no device matches, falls back to the default front camera. Ignored when `videoURL` is set |
| minPoseDetectionConfidence | number | 0.75 | All pose-tracking views | MediaPipe minimum detection confidence (0–1). Lower = detects poses more easily but with more false positives |
| minTrackingConfidence | number | 0.75 | All pose-tracking views | MediaPipe minimum tracking confidence (0–1). Affects how persistently a pose is tracked between frames |
| minPosePresenceConfidence | number | 0.75 | All pose-tracking views | MediaPipe minimum presence confidence (0–1). Threshold for deciding a person is in frame |
| mediapipeModel | "full", "heavy", or "light" | auto | All pose-tracking views | Explicitly pins the MediaPipe pose model. When omitted, the SDK auto-selects `light` or `full` based on device speed — `heavy` is **never** auto-selected and is only used when you pass it explicitly here |
| defaultDelegate | "GPU" or "CPU" | "GPU" | All pose-tracking views | Processing backend for pose detection. The URL parameter `?delegate=` **overrides** the postMessage value |
| landmarkColor | string | "#14FF00" | All pose-tracking views | Color of the skeleton overlay when the user's form is correct (`#` optional — it is added automatically). Mistake highlighting always uses red/orange on top of this |
| isDrawingPose | boolean | true | All pose-tracking views | When `false`, hides the skeleton overlay entirely. Pose recognition, rep counting, and mistake detection **still run** — only the drawing is disabled |
| showSilhouette | boolean | true | Workout player, Camera | When `false`, the camera-positioning (silhouette/check-frame) step is skipped entirely and the session starts straight on the exercise screen |
| includePoseData | string[] | — | **Camera only** | Which raw pose streams to emit as postMessage events, e.g. `["angles", "poseLandmarks", "worldLandmarks"]`. `"poseLandmarks"` emits per-frame `pose_landmarks` messages, `"worldLandmarks"` emits `world_landmarks`, `"angles"` enables joint-angle computation. Ignored by every other integration option |
| includePoseBorders | boolean | true | All pose-tracking views | Enables the out-of-frame guard: when body parts leave the frame, the skeleton turns red, a "step back" cue plays, and rep counting pauses until the user is fully visible. `false` disables that guard |
| includeRealtimeAccuracy | boolean | true | Workout player, Camera | Tracks per-rep form accuracy and, when explicitly passed as `true`, additionally streams per-frame `correct_position_accuracy` postMessage events to the host. Note: passing `false` does **not** disable accuracy tracking (the flag is only applied when truthy) |
| videoFit | "cover" or "contain" | "cover" | All pose-tracking views | `"contain"` shows the full camera frame (letterboxed) instead of zooming to fill the view — useful when the full body must stay visible in tight layouts. Assessments force `contain` in landscape regardless of this value |
**Notes:**
- `videoURL` (feed a video file instead of the live camera) is documented in [Testing & Simulation](/docs/customization-parameters/testing-simulation).
- Changing confidence values, `mediapipeModel`, or `defaultDelegate` triggers a pose-model rebuild — set them at launch, not mid-session.
- There is **no automatic "heavy" model for balance assessments**; if an assessment needs maximum accuracy, pass `mediapipeModel: "heavy"` explicitly.
**Camera & Pose Detection Settings**
_Swift (iOS)_
```swift
// Via customParams
kinestex.createCameraView(
exercises: exerciseList,
currentExercise: $currentExercise,
customParams: [
"landmarkColor": "#FF5500",
"showSilhouette": true,
"mediapipeModel": "heavy",
"defaultDelegate": "GPU",
"includePoseData": ["angles", "poseLandmarks"], // Camera component only
"includeRealtimeAccuracy": true,
"shouldShowCameraSelector": true,
"videoFit": "contain" // show full camera frame
]
)
```
_Kotlin (Android)_
```kotlin
// Via customParams
KinesteXSDK.createCameraComponent(
exercises = exerciseList,
currentExercise = "Squats",
customParams = mutableMapOf(
"landmarkColor" to "#FF5500",
"showSilhouette" to true,
"mediapipeModel" to "heavy",
"defaultDelegate" to "GPU",
"includePoseData" to listOf("angles", "poseLandmarks"), // Camera component only
"includeRealtimeAccuracy" to true,
"videoFit" to "contain" // show full camera frame
)
)
```
_React Native_
```jsx
// Via customParameters
const postData: IPostData = {
key: 'YOUR_API_KEY',
userId: 'user-123',
company: 'YOUR_COMPANY',
exercises: ['Squats', 'Lunges'],
currentExercise: 'Squats',
customParameters: {
landmarkColor: '#FF5500',
showSilhouette: true,
mediapipeModel: 'heavy',
defaultDelegate: 'GPU',
includePoseData: ['angles', 'poseLandmarks'], // Camera component only
includeRealtimeAccuracy: true,
shouldShowCameraSelector: true,
videoFit: 'contain', // show full camera frame
},
};
```
_Flutter_
```dart
// Via customParams
KinesteXAIFramework.createCameraComponent(
exercises: ["Squats", "Lunges"],
currentExercise: "Squats",
customParams: {
"landmarkColor": "#FF5500",
"showSilhouette": true,
"mediapipeModel": "heavy",
"defaultDelegate": "GPU",
"includePoseData": ["angles", "poseLandmarks"], // Camera component only
"includeRealtimeAccuracy": true,
"videoFit": "contain", // show full camera frame
},
);
```
_HTML / JavaScript_
```html
// Direct in postData object
const postData = {
userId: "user-123",
company: "YOUR_COMPANY",
key: "YOUR_API_KEY",
exercises: ["Squats", "Lunges"],
currentExercise: "Squats",
landmarkColor: "#FF5500",
showSilhouette: true,
mediapipeModel: "heavy",
defaultDelegate: "GPU",
includePoseData: ["angles", "poseLandmarks"], // Camera component only
includeRealtimeAccuracy: true,
shouldShowCameraSelector: true,
videoFit: "contain", // show full camera frame
};
```
_React (TypeScript)_
```tsx
// Via customParameters
const postData: IPostData = {
key: 'YOUR_API_KEY',
userId: 'user-123',
company: 'YOUR_COMPANY',
exercises: ['Squats', 'Lunges'],
currentExercise: 'Squats',
customParameters: {
landmarkColor: '#FF5500',
showSilhouette: true,
mediapipeModel: 'heavy',
defaultDelegate: 'GPU',
includePoseData: ['angles', 'poseLandmarks'], // Camera component only
includeRealtimeAccuracy: true,
videoFit: 'contain', // show full camera frame
},
};
```
### Camera Component Parameters
Parameters that are **only consumed by the standalone Camera component** (`createCameraComponent` / `IntegrationOption.CAMERA`, route `/camera`). Passing them to any other integration option has no effect. For the full Camera component guide — including events and control commands — see [Camera Component](/docs/integration/camera-component).
| Parameter | Type | Default | What it does internally |
|-----------|------|---------|-------------------------|
| exercises | string[] | — | The allowlist of exercise models to download at launch. Only exercises in this list (plus any added later via `load_models`) can be activated with `currentExercise` — switching to an unlisted exercise posts `error_occurred` |
| currentExercise | string | — | The active exercise. Can be updated at runtime (postMessage / `changeExercise` / `updateCurrentExercise`) to switch tracking instantly. Also accepts control commands (`"Pause Exercise"`, `"Pause Audio"`, `"Resume Audio"`, `"Workout Overview"`, `"Stop Camera"`) |
| exerciseFetchType | string | "model_id" | How the backend should interpret each value in `exercises` (and in `load_models` requests): `"model_id"`, `"exercise_id"`, or `"exercise_title"`. Invalid values silently fall back to the backend default |
| restSpeeches | string[] | — | Speech audio IDs (from the exercise's `rest_speech` field) to pre-download so they can be played instantly later |
| currentRestSpeech | string | — | Plays the given pre-loaded speech ID immediately. Send the special value `"Stop audio"` to stop playback. Update it at runtime the same way as `currentExercise` |
**Loading more exercises at runtime:** send the `load_models` action (`{ workout_activity_action: "load_models", exercises: [...], exerciseFetchType?: "..." }`) to fetch and cache additional models mid-session without re-mounting. The SDK replies with `models_loaded` (listing the IDs that resolved) and `speech_fetch_complete`; failures arrive as `error_occurred`. Successfully loaded IDs are automatically added to the `exercises` allowlist so `currentExercise` can switch to them.
**Related parameters that also work here:** `videoURL` ([Testing & Simulation](/docs/customization-parameters/testing-simulation)), `includePoseData`, `includeRealtimeAccuracy`, `landmarkColor`, `showSilhouette`, `videoFit` ([Camera & Pose Detection](/docs/customization-parameters/camera-pose-detection)).
### UI Controls
Control visibility and behavior of UI elements.
| Parameter | Type | Default | Applies to | What it does internally |
|-----------|------|---------|------------|-------------------------|
| isHideHeaderMain | boolean | false | Main, Plan, Personalized Plan, Workout, Challenge, Leaderboard, Assessments, AI Trainer | Hides the top header / back button on the listed screens. Note: on some screens the header is additionally gated by how the view was opened, so a back button may still appear when a screen is reached from inside the Complete UX home |
| hideFeelingDialog | boolean | false | Any flow ending on the statistics screen | Permanently suppresses the post-workout "How are you feeling?" emoji prompt (which otherwise appears on every 3rd completed session) |
| showSettings | boolean | true | Workout, Plan, Personalized Plan, Challenge, Assessments | When `false`, hides the settings gear button on the pre-workout detail screens (the settings modal itself stays available to internal flows) |
| hideCompletionOverlay | boolean | false | Workout player | Skips the full-screen animated completion summary (badge, confetti, farewell speech, auto-dismiss after ~5s) shown when a workout with completed reps ends. When the overlay is dismissed normally, the SDK posts `workout_completion_overlay_dismissed` |
| preventGestureControl | boolean | false | Workout player | Disables in-exercise body controls: the hand-gesture pause/resume (T-pose then crossing forearms overhead) AND the auto-pause that triggers when the user leaves the camera frame for ~3.5s. Not applicable to Camera component, games, or assessments — they never have gesture control |
| disableGuide | boolean | false | Main, Plan | Suppresses the coach-mark tooltip tour (the step-by-step "tap here" overlay) on the Complete UX home and plan screens |
| disableCookies | boolean | false | All | Declines analytics-cookie consent before any analytics call, switches analytics to memory-only persistence (nothing written to storage), and removes the cookie-preferences entries from the Settings modal and camera-access screen. Use for GDPR-sensitive embeds |
| hideStatisticsHeader | boolean | false | Statistics screen (FitPass-themed companies only) | Hides the top bar on the statistics screen. **Only implemented in the FitPass statistics variant** — companies on the default statistics layout always show the header regardless of this flag |
| nativeParentScroll | boolean | false | Statistics screen | Makes the statistics screen delegate scrolling to the native parent container (removes internal scroll + safe-area padding on the `/statistic` route only). Enable if your native app wraps the WebView in its own scroll view |
| immediateShowSkip | boolean | false | Workout player, Games, Camera | Shows the Skip button on the camera frame-positioning (silhouette) screen immediately with a themed background, instead of fading it in after ~5 seconds. Has no effect if Skip is disabled for the screen |
| hideOtherGender | boolean | false | Plan Onboarding, AI Trainer | Removes the "I'd rather not say" choice from the plan-onboarding gender step and the AI Trainer profile pickers, leaving only Male / Female. Also accepted as a URL query param |
| exitUrl | string | — | All (standalone/link integrations) | For launches with no host app: when the session ends, the browser is redirected to this URL after the `exit_kinestex` event is posted. Accepts https:// URLs or app deep links (e.g. `clientapp://home`); script-executing schemes (`javascript:`, `data:`) are rejected for security |
**Skeleton drawing (`isDrawingPose`)** is documented under [Camera & Pose Detection](/docs/customization-parameters/camera-pose-detection).
**UI Controls Configuration**
_Swift (iOS)_
```swift
// Via customParams
kinestex.createView(
customParams: [
"isHideHeaderMain": true,
"hideFeelingDialog": true,
"hideCompletionOverlay": true,
"preventGestureControl": true,
"disableGuide": true,
"disableCookies": true,
"hideStatisticsHeader": false,
"nativeParentScroll": false
]
)
```
_Kotlin (Android)_
```kotlin
// Via customParams
KinesteXSDK.createView(
customParams = mutableMapOf(
"isHideHeaderMain" to true,
"hideFeelingDialog" to true,
"hideCompletionOverlay" to true,
"preventGestureControl" to true,
"disableGuide" to true,
"disableCookies" to true,
"hideStatisticsHeader" to false,
"nativeParentScroll" to false
)
)
```
_React Native_
```jsx
// Via customParameters
const postData: IPostData = {
key: 'YOUR_API_KEY',
userId: 'user-123',
company: 'YOUR_COMPANY',
customParameters: {
isHideHeaderMain: true,
hideFeelingDialog: true,
hideCompletionOverlay: true,
preventGestureControl: true,
disableGuide: true,
disableCookies: true,
hideStatisticsHeader: false,
nativeParentScroll: false,
},
};
```
_Flutter_
```dart
// Via customParams
KinesteXAIFramework.createMainView( // or any other create*View method
customParams: {
"isHideHeaderMain": true,
"hideFeelingDialog": true,
"hideCompletionOverlay": true,
"preventGestureControl": true,
"disableGuide": true,
"disableCookies": true,
"hideStatisticsHeader": false,
"nativeParentScroll": false,
},
);
```
_HTML / JavaScript_
```html
// Direct in postData object
const postData = {
userId: "user-123",
company: "YOUR_COMPANY",
key: "YOUR_API_KEY",
isHideHeaderMain: true,
hideFeelingDialog: true,
hideCompletionOverlay: true,
preventGestureControl: true,
disableGuide: true,
disableCookies: true,
hideStatisticsHeader: false,
nativeParentScroll: false,
};
```
_React (TypeScript)_
```tsx
// Via customParameters
const postData: IPostData = {
key: 'YOUR_API_KEY',
userId: 'user-123',
company: 'YOUR_COMPANY',
customParameters: {
isHideHeaderMain: true,
hideFeelingDialog: true,
hideCompletionOverlay: true,
preventGestureControl: true,
disableGuide: true,
disableCookies: true,
hideStatisticsHeader: false,
nativeParentScroll: false,
},
};
```
### Challenge Mode
Configure challenge-specific parameters for direct challenge launches. **Applies to: Challenge view and the Games** (Balloon Pop / Color Chase / Alien Squat Shooter, where noted).
| Parameter | Type | Default | Applies to | What it does internally |
|-----------|------|---------|------------|-------------------------|
| exercise | string | — | Challenge, Assessments/Games (as selector) | Which exercise to run. Accepts an exercise **title** or a 20-character exercise **ID**. For Experience views, the same parameter selects which assessment or game to launch (e.g. `"tugtest"`, a game key) |
| countdown | number | exercise default | Challenge | **Duration of the challenge in seconds** — overrides the exercise's own configured duration. This is NOT the 3-2-1 preparation countdown (that one is fixed). Note: the challenge intro screen displays the exercise's stored duration; the override takes effect when the challenge actually starts |
| reps | number | — | Challenge | When set, converts the challenge into a **rep-target challenge** (finish N reps) instead of a timed one |
| gameTotalRounds | number | 10 | Color Chase ONLY | How many rounds the Color Chase game lasts. Values ≤ 10 use the fixed level table; values > 10 are procedurally generated with increasing difficulty (more colors, shorter memorize time). Ignored by every other game |
**Alien Squat Shooter** ignores `reps`, `countdown`, and `gameTotalRounds` — its rounds and ship counts are fixed.
**Balloon Pop — rounds mode (special case):**
When you launch the **Balloon Pop** game and pass `reps`, the game switches from the default 30-second escalating-difficulty timer mode into a **rounds mode**, and the two challenge parameters are repurposed:
- `reps` = number of rounds
- `countdown` = number of balloons spawned per round (defaults to **2** if omitted)
The game ends after all rounds are completed (balloons never time out in this mode; only a 5-minute safety cap applies). The HUD shows a round counter instead of a timer, and the difficulty stage indicator is hidden.
| Configuration | Behavior |
|---------------|----------|
| `reps: 4, countdown: 2` | 4 rounds × 2 balloons = 8 total pops |
| `reps: 3, countdown: 3` | 3 rounds × 3 balloons = 9 total pops |
| `reps: 6, countdown: 1` | 6 rounds × 1 balloon = 6 total pops (very easy) |
| `reps` not provided | Default timer mode (30s, escalating difficulty) |
**Challenge Mode Configuration**
_Swift (iOS)_
```swift
// Via customParams for challenge integration
kinestex.createChallengeView(
exercise: exerciseId, // direct
customParams: [
"countdown": 5,
"reps": 20,
// Color Chase only:
"gameTotalRounds": 15
// Balloon Pop rounds-mode example:
// "reps": 4, "countdown": 2 → 4 rounds × 2 balloons
]
)
```
_Kotlin (Android)_
```kotlin
// Via customParams for challenge integration
KinesteXSDK.createChallengeView(
exercise = exerciseId, // direct
customParams = mutableMapOf(
"countdown" to 5,
"reps" to 20,
// Color Chase only:
"gameTotalRounds" to 15
// Balloon Pop rounds-mode example:
// "reps" to 4, "countdown" to 2 → 4 rounds × 2 balloons
)
)
```
_React Native_
```jsx
// Via postData for challenge integration
const postData: IPostData = {
key: 'YOUR_API_KEY',
userId: 'user-123',
company: 'YOUR_COMPANY',
customParameters: {
exercise: 'exerciseId',
countdown: 5,
reps: 20,
// Color Chase only:
gameTotalRounds: 15,
// Balloon Pop rounds-mode example:
// reps: 4, countdown: 2 → 4 rounds × 2 balloons
},
};
```
_Flutter_
```dart
// exercise and countdown are direct params; the rest via customParams
KinesteXAIFramework.createChallengeView(
exercise: exerciseId, // direct
countdown: 5, // direct (required)
customParams: {
"reps": 20,
// Color Chase only:
"gameTotalRounds": 15,
// Balloon Pop rounds-mode example:
// "reps": 4, "countdown": 2 → 4 rounds × 2 balloons
},
);
```
_HTML / JavaScript_
```html
// Direct in postData object
const postData = {
userId: "user-123",
company: "YOUR_COMPANY",
key: "YOUR_API_KEY",
exercise: exerciseId,
countdown: 5,
reps: 20,
// Color Chase only:
gameTotalRounds: 15,
// Balloon Pop rounds-mode example:
// reps: 4, countdown: 2 → 4 rounds × 2 balloons
};
```
_React (TypeScript)_
```tsx
// Via postData for challenge integration
const postData: IPostData = {
key: 'YOUR_API_KEY',
userId: 'user-123',
company: 'YOUR_COMPANY',
customParameters: {
exercise: 'exerciseId',
countdown: 5,
reps: 20,
// Color Chase only:
gameTotalRounds: 15,
// Balloon Pop rounds-mode example:
// reps: 4, countdown: 2 → 4 rounds × 2 balloons
},
};
```
### Complete UX Customization
Customize the Complete UX (Main View) home page experience. **Applies to: Main only** — no other integration option reads this parameter.
| Parameter | Type | Default | Applies to | What it does internally |
|-----------|------|---------|------------|-------------------------|
| challenges_home | array | per-planC defaults | Main | Replaces the two challenge/game cards on the Complete UX home screen. When omitted, the cards are chosen from built-in defaults based on `planC` |
**challenges_home Configuration:**
The `challenges_home` parameter customizes the challenge/game cards displayed on the home screen of the Complete UX experience. Pass exactly **2 objects** in the array — the layout and the home guide are designed for two cards.
**Array Object Structure:**
| Property | Type | Required | Description |
|----------|------|----------|-------------|
| id | string | Yes | Exercise ID for challenges, or game ID for games |
| name | string | Yes | Display name shown verbatim on the card (a value starting with `home.` is treated as a translation key instead) |
| isGame | boolean | Yes | Set to `true` for games, `false` for challenges |
| image | string | No | Card image URL. Falls back to a built-in image when omitted |
**Available Games:**
- `balloonpop` - Balloon Pop game
- `aliensquatshooter` - Alien Squat Shooter game
- `colorchase` - Color Chase game
**Combination Options:**
- Two challenges (both with `isGame: false`)
- Two games (both with `isGame: true`)
- One challenge + one game (mixed `isGame` values)
**Note:** You can pass any exercise ID as a challenge. When specifying a game, ensure `isGame` is set to `true`.
**Complete UX Home Page Customization**
_Swift (iOS)_
```swift
// Customize challenges on Complete UX home page
let exerciseId = "your-exercise-id" // Get from Content API
let exerciseName = "Your Exercise Name"
kinestex.createMainView(
customParams: [
"challenges_home": [
["id": exerciseId, "name": exerciseName, "isGame": false],
["id": "balloonpop", "name": "Balloon Pop", "isGame": true]
]
]
)
```
_Kotlin (Android)_
```kotlin
// Customize challenges on Complete UX home page
val exerciseId = "your-exercise-id" // Get from Content API
val exerciseName = "Your Exercise Name"
KinesteXSDK.createMainView(
customParams = mutableMapOf(
"challenges_home" to listOf(
mapOf("id" to exerciseId, "name" to exerciseName, "isGame" to false),
mapOf("id" to "balloonpop", "name" to "Balloon Pop", "isGame" to true)
)
)
)
```
_React Native_
```jsx
// Customize challenges on Complete UX home page
const exerciseId = 'your-exercise-id'; // Get from Content API
const exerciseName = 'Your Exercise Name';
const postData: IPostData = {
key: 'YOUR_API_KEY',
userId: 'user-123',
company: 'YOUR_COMPANY',
customParameters: {
challenges_home: [
{ id: exerciseId, name: exerciseName, isGame: false },
{ id: 'balloonpop', name: 'Balloon Pop', isGame: true },
],
},
};
```
_Flutter_
```dart
// Customize challenges on Complete UX home page
final exerciseId = "your-exercise-id"; // Get from Content API
final exerciseName = "Your Exercise Name";
KinesteXAIFramework.createMainView(
customParams: {
"challenges_home": [
{"id": exerciseId, "name": exerciseName, "isGame": false},
{"id": "balloonpop", "name": "Balloon Pop", "isGame": true},
],
},
);
```
_HTML / JavaScript_
```html
// Customize challenges on Complete UX home page
const exerciseId = "your-exercise-id"; // Get from Content API
const exerciseName = "Your Exercise Name";
const postData = {
userId: "user-123",
company: "YOUR_COMPANY",
key: "YOUR_API_KEY",
challenges_home: [
{ id: exerciseId, name: exerciseName, isGame: false },
{ id: "balloonpop", name: "Balloon Pop", isGame: true },
],
};
```
_React (TypeScript)_
```tsx
// Customize challenges on Complete UX home page
const exerciseId = 'your-exercise-id'; // Get from Content API
const exerciseName = 'Your Exercise Name';
const postData: IPostData = {
key: 'YOUR_API_KEY',
userId: 'user-123',
company: 'YOUR_COMPANY',
customParameters: {
challenges_home: [
{ id: exerciseId, name: exerciseName, isGame: false },
{ id: 'balloonpop', name: 'Balloon Pop', isGame: true },
],
},
};
```
### Leaderboard
Configure leaderboard functionality. **Applies to: Challenge** (result submission happens on the challenge results screen) **and Leaderboard view**.
| Parameter | Type | Default | Applies to | What it does internally |
|-----------|------|---------|------------|-------------------------|
| showLeaderboard | boolean | false | Challenge | Shows the leaderboard entry point on the challenge screen, and — after a challenge with completed reps — opens the username-entry modal that publishes the result to the leaderboard |
| username | string | user ID | Challenge, Leaderboard | Display name for leaderboard entries. Saved to the WebView's localStorage (`leaderboard_username`) at verification so it persists across sessions. When absent, the raw `userId` is used as the display name |
| autoSubmitLeaderboard | boolean | false | Challenge | Silently publishes the challenge result to the leaderboard without showing the submission modal. The display name comes from the stored `leaderboard_username` (falls back to the user ID) — so pass `username` together with this flag |
**How submission works internally:** when a challenge with completed reps finishes, the result is always recorded; `showLeaderboard` controls whether the user is prompted to publish it with a display name, while `autoSubmitLeaderboard: true` skips the prompt and publishes immediately.
**`autoSubmitLeaderboard` use case:** Use this only for Challenge integrations where you want every completion silently posted to the leaderboard (e.g., when your host app already manages display names and doesn't want a second prompt). Leave it unset (or `false`) to keep the standard modal-based flow.
**Leaderboard Configuration**
_Swift (iOS)_
```swift
// Via customParams
kinestex.createView(
customParams: [
"showLeaderboard": true,
"username": "FitnessPro123",
"autoSubmitLeaderboard": true // Challenge-only: skip submit modal
]
)
```
_Kotlin (Android)_
```kotlin
// Via customParams
KinesteXSDK.createView(
customParams = mutableMapOf(
"showLeaderboard" to true,
"username" to "FitnessPro123",
"autoSubmitLeaderboard" to true // Challenge-only: skip submit modal
)
)
```
_React Native_
```jsx
// Via customParameters
const postData: IPostData = {
key: 'YOUR_API_KEY',
userId: 'user-123',
company: 'YOUR_COMPANY',
customParameters: {
showLeaderboard: true,
username: 'FitnessPro123',
autoSubmitLeaderboard: true, // Challenge-only: skip submit modal
},
};
```
_Flutter_
```dart
// Via customParams
KinesteXAIFramework.createMainView( // or any other create*View method
customParams: {
"showLeaderboard": true,
"username": "FitnessPro123",
"autoSubmitLeaderboard": true, // Challenge-only: skip submit modal
},
);
```
_HTML / JavaScript_
```html
// Direct in postData object
const postData = {
userId: "user-123",
company: "YOUR_COMPANY",
key: "YOUR_API_KEY",
showLeaderboard: true,
username: "FitnessPro123",
autoSubmitLeaderboard: true, // Challenge-only: skip submit modal
};
```
_React (TypeScript)_
```tsx
// Via customParameters
const postData: IPostData = {
key: 'YOUR_API_KEY',
userId: 'user-123',
company: 'YOUR_COMPANY',
customParameters: {
showLeaderboard: true,
username: 'FitnessPro123',
autoSubmitLeaderboard: true, // Challenge-only: skip submit modal
},
};
```
### Loading Screen
Customize the loading screen appearance (includes native overlay color that is displayed during initial loading phase). **Applies to: All integrations** — the loading screen is shown while any view verifies and downloads resources.
| Parameter | Type | Default | Applies to | What it does internally |
|-----------|------|---------|------------|-------------------------|
| loadingStickmanColor | string | #00FF0B | All | Color of the animated stickman loading character |
| loadingBackgroundColor | string | Black (dark) / White (light) | All | Loading screen background color |
| loadingTextColor | string | White (dark) / Black (light) | All | Color of the loading text and progress messages |
**Color format:** a bare 6-character hex value (e.g. `"1A1A2E"`) gets a `#` added automatically; `#RRGGBB` and CSS color names also work. Avoid 3-character shorthand hex (`"fff"`) without `#` — it is passed through as-is and is not valid CSS.
**SDK Support:**
- **Swift:** Direct support via `IStyle` class (hex values with #)
- **Flutter:** Direct support via `IStyle` class (hex values without #)
- **Kotlin:** Direct support via `IStyle` data class (hex values without #)
- **React Native/React:** Direct support via `style` object (hex values without #)
- **HTML/JS:** Direct in postData object
**Native Loading Overlay (Swift):**
The SDK displays a native overlay on top of the WebView until content is fully loaded, preventing users from seeing a blank screen.
- Overlay automatically hides when `KinestexLoaded` message is received
- Overlay color priority:
1. Uses `loadingBackgroundColor` if set (hex color with #)
2. Uses white (#FFFFFF) if `style = "light"`
3. Uses black (#000000) if `style = "dark"` (default)
**Style Properties:**
| Property | Type | Default | Description |
|----------|------|---------|-------------|
| style | String? | "dark" | Base theme style ("dark" or "light") |
| themeName | String? | null | Custom theme name |
| loadingStickmanColor | String? | null | Color for the loading animation stickman (hex without #) |
| loadingBackgroundColor | String? | null | Background color during loading (hex without #) |
| loadingTextColor | String? | null | Text color during loading (hex without #) |
**Loading Screen Customization**
_Swift (iOS)_
```swift
// Direct SDK support via IStyle class
let customStyle = IStyle(
style: "dark",
loadingBackgroundColor: "#1A1A2E",
loadingStickmanColor: "#FF6B00",
loadingTextColor: "#FFFFFF"
)
kinestex.createWorkoutView(
workout: "Fitness Lite",
user: user,
style: customStyle,
isLoading: $isLoading,
onMessageReceived: { /* ... */ }
)
```
_Kotlin (Android)_
```kotlin
// Direct SDK support via IStyle class
KinesteXSDK.createWorkoutView(
context = this,
workoutName = "Fitness Lite",
style = IStyle(
style = "dark",
loadingBackgroundColor = "1A1A2E", // hex value without #
loadingStickmanColor = "FF6B00",
loadingTextColor = "FFFFFF"
),
isLoading = viewModel.isLoading,
onMessageReceived = { message ->
handleWebViewMessage(message)
},
permissionHandler = this
)
```
_React Native_
```jsx
// Direct support via style object
const postData: IPostData = {
key: 'YOUR_API_KEY',
userId: 'user-123',
company: 'YOUR_COMPANY',
style: {
style: 'dark',
loadingBackgroundColor: '1A1A2E', // hex without #
loadingTextColor: 'FFFFFF',
},
customParameters: {
loadingStickmanColor: '#FF6B00', // via customParams
},
};
```
_Flutter_
```dart
// Direct SDK support via IStyle class
KinesteXAIFramework.createWorkoutView(
workoutName: "Fitness Lite",
isShowKinestex: showKinesteX,
isLoading: ValueNotifier(false),
style: IStyle(
style: 'dark',
loadingBackgroundColor: '1A1A2E', // hex without #
loadingStickmanColor: 'FF6B00',
loadingTextColor: 'FFFFFF',
),
onMessageReceived: (message) {
handleWebViewMessage(message);
},
);
```
_HTML / JavaScript_
```html
// Direct in postData object
const postData = {
userId: "user-123",
company: "YOUR_COMPANY",
key: "YOUR_API_KEY",
loadingStickmanColor: "#FF6B00",
loadingBackgroundColor: "#1A1A2E",
loadingTextColor: "#FFFFFF",
};
```
_React (TypeScript)_
```tsx
// Direct support via style object
const postData: IPostData = {
key: 'YOUR_API_KEY',
userId: 'user-123',
company: 'YOUR_COMPANY',
style: {
style: 'dark',
loadingBackgroundColor: '1A1A2E', // hex without #
loadingTextColor: 'FFFFFF',
},
customParameters: {
loadingStickmanColor: '#FF6B00', // via customParams
},
};
```
### Motion Tracking Settings
Control AI-powered motion tracking behavior. **Applies to: Workout player** (motion tracking in other views — Camera, Games, Assessments — is always on and cannot be toggled off by users).
| Parameter | Type | Default | Applies to | What it does internally |
|-----------|------|---------|------------|-------------------------|
| motionTrackingSettingOn | boolean | true | Settings modal | When `false`, hides the AI-tracking on/off toggle from the in-app Settings modal, so users cannot change the tracking mode themselves |
| motionTrackingEnabled | boolean | user preference | Workout player | Session-level override of the AI tracking state. When explicitly set (true or false), it **clears the user's saved preference** (localStorage `aiTrackingEnabled`) and uses the provided value for this session |
| motionDataEnabled | boolean | true | Workout player, Camera | When `false`, the SDK does NOT record per-frame pose landmark data during the session. Session replay will be empty for affected sessions, but rep counting and all other workout functionality is unaffected |
**What happens when motion tracking is OFF (`motionTrackingEnabled: false` or user toggle):**
- The camera-positioning (silhouette) step is skipped and no camera/pose pipeline runs; a "No AI" badge is shown.
- Rep-based exercises are converted to **timers** (roughly 2 seconds per configured rep) and **auto-advance** when the timer ends — there is no manual "next rep" button, and **no reps, accuracy, or mistakes are recorded**.
- If you need realistic tracking data without a person in front of the camera, use `videoURL` instead — see [Testing & Simulation](/docs/customization-parameters/testing-simulation).
**`motionDataEnabled` — only use this if you know why it's necessary.** This flag exists for memory-constrained scenarios (e.g., long workouts with many unique exercise videos on older iOS devices where peak memory pressure can cause the WebView to crash). Disabling motion data reduces memory usage at the cost of losing session replay. The recorder state resets on every verification, so the flag does not leak across sessions. Default behavior is unchanged if you omit the parameter.
**Motion Tracking Settings**
_Swift (iOS)_
```swift
// Via customParams
kinestex.createView(
customParams: [
"motionTrackingSettingOn": true,
"motionTrackingEnabled": true,
"motionDataEnabled": false // Only set when memory-constrained
]
)
```
_Kotlin (Android)_
```kotlin
// Via customParams
KinesteXSDK.createView(
customParams = mutableMapOf(
"motionTrackingSettingOn" to true,
"motionTrackingEnabled" to true,
"motionDataEnabled" to false // Only set when memory-constrained
)
)
```
_React Native_
```jsx
// Via customParameters
const postData: IPostData = {
key: 'YOUR_API_KEY',
userId: 'user-123',
company: 'YOUR_COMPANY',
customParameters: {
motionTrackingSettingOn: true,
motionTrackingEnabled: true,
motionDataEnabled: false, // Only set when memory-constrained
},
};
```
_Flutter_
```dart
// Via customParams
KinesteXAIFramework.createMainView( // or any other create*View method
customParams: {
"motionTrackingSettingOn": true,
"motionTrackingEnabled": true,
"motionDataEnabled": false, // Only set when memory-constrained
},
);
```
_HTML / JavaScript_
```html
// Direct in postData object
const postData = {
userId: "user-123",
company: "YOUR_COMPANY",
key: "YOUR_API_KEY",
motionTrackingSettingOn: true,
motionTrackingEnabled: true,
motionDataEnabled: false, // Only set when memory-constrained
};
```
_React (TypeScript)_
```tsx
// Via customParameters
const postData: IPostData = {
key: 'YOUR_API_KEY',
userId: 'user-123',
company: 'YOUR_COMPANY',
customParameters: {
motionTrackingSettingOn: true,
motionTrackingEnabled: true,
motionDataEnabled: false, // Only set when memory-constrained
},
};
```
### Debug & Development
Parameters for debugging and development purposes. Not intended for end users.
| Parameter | Type | Default | Applies to | What it does internally |
|-----------|------|---------|------------|-------------------------|
| showDebugRecording | boolean | false | Workout player, Camera | Shows recording controls on the exercise screen that capture the camera/pose session for QA analysis and replay |
| showNetworkDebugTool | boolean | false | All | Installs a network-request interceptor and shows a floating panel listing all API requests/responses made inside the WebView — useful for diagnosing connectivity issues on devices |
| posePerfHud | boolean | false | All pose-tracking views | Shows a performance HUD over the camera view (frame timings, worker/main-thread mode, device tier) plus extra developer toggles on the workout detail screens. Resets on reload |
**Note:** `showDebugRecording` can also be enabled via the URL parameter `?debug=true`
**Debug & Development Settings**
_Swift (iOS)_
```swift
// Via customParams
kinestex.createView(
customParams: [
"showDebugRecording": true,
"showNetworkDebugTool": true,
"posePerfHud": true
]
)
```
_Kotlin (Android)_
```kotlin
// Via customParams
KinesteXSDK.createView(
customParams = mutableMapOf(
"showDebugRecording" to true,
"showNetworkDebugTool" to true,
"posePerfHud" to true
)
)
```
_React Native_
```jsx
// Via customParameters
const postData: IPostData = {
key: 'YOUR_API_KEY',
userId: 'user-123',
company: 'YOUR_COMPANY',
customParameters: {
showDebugRecording: true,
showNetworkDebugTool: true,
posePerfHud: true,
},
};
```
_Flutter_
```dart
// Via customParams
KinesteXAIFramework.createMainView( // or any other create*View method
customParams: {
"showDebugRecording": true,
"showNetworkDebugTool": true,
"posePerfHud": true,
},
);
```
_HTML / JavaScript_
```html
// Direct in postData object
const postData = {
userId: "user-123",
company: "YOUR_COMPANY",
key: "YOUR_API_KEY",
showDebugRecording: true,
showNetworkDebugTool: true,
posePerfHud: true,
};
```
_React (TypeScript)_
```tsx
// Via customParameters
const postData: IPostData = {
key: 'YOUR_API_KEY',
userId: 'user-123',
company: 'YOUR_COMPANY',
customParameters: {
showDebugRecording: true,
showNetworkDebugTool: true,
posePerfHud: true,
},
};
```
### Custom Workout
Configure custom workout sequences. **Applies to: Custom Workout only** (`createCustomWorkoutView`, route `/custom-workout`) — the parameter is ignored on every other integration option. For the complete implementation guide, see [Custom Workout integration](/docs/integration/custom-workout).
| Parameter | Type | Default | Applies to | What it does internally |
|-----------|------|---------|------------|-------------------------|
| customWorkoutExercises | array | — | Custom Workout | The exercise sequence to build the workout from. After verification the SDK fetches each exercise, downloads videos, models, and audio, then posts `all_resources_loaded` when everything is ready |
**Sequence object structure (per exercise):**
| Property | Type | Required | Description |
|----------|------|----------|-------------|
| exerciseId | string | Yes | Exercise document ID (from the [Content API](/docs/content-api)) |
| reps | number or null | One of reps/duration | Target repetitions (rep-based exercise) |
| duration | number or null | One of reps/duration | Duration in seconds (timer-based exercise) |
| includeRestPeriod | boolean | Yes | Insert a rest screen **before** this exercise |
| restDuration | number | Yes | Rest length in seconds (rest is only inserted when > 0) |
| stage | "warmup", "exercise", or "cooldown" | No | Colors the rest-screen progress bar by workout stage |
**Custom Workout Flow:**
1. Pass `customWorkoutExercises` during initial verification.
2. Wait for the `all_resources_loaded` message (an `error_occurred` message is posted if the sequence is invalid or a resource fails).
3. Send `workout_activity_action: "start"` to begin — the SDK navigates into the standard workout flow automatically.
**Note:** `restSpeeches` / `currentRestSpeech` are Camera-component parameters ([Camera Component](/docs/customization-parameters/camera-component-params)); custom workouts generate their own rest audio. To simulate the workout with a video instead of the live camera, see [Testing & Simulation](/docs/customization-parameters/testing-simulation).
**Custom Workout Parameters**
_Swift (iOS)_
```swift
// Via customParams for additional settings
let customExercises = [
WorkoutSequenceExercise(
exerciseId: "exercise-id-1",
reps: 15,
duration: nil,
includeRestPeriod: true,
restDuration: 20
)
]
kinestex.createCustomWorkoutView(
customWorkouts: customExercises, // direct
customParams: [
"shouldSendStats": true // optional: save the session on completion
]
)
```
_Kotlin (Android)_
```kotlin
// Via customParams for additional settings
val customExercises = listOf(
WorkoutSequenceExercise(
exerciseId = "exercise-id-1",
reps = 15,
duration = null,
includeRestPeriod = true,
restDuration = 20
)
)
KinesteXSDK.createCustomWorkoutView(
customWorkouts = customExercises, // direct
customParams = mutableMapOf(
"shouldSendStats" to true // optional: save the session on completion
)
)
```
_React Native_
```jsx
// Direct support in postData
const customWorkoutExercises = [
{
exerciseId: 'exercise-id-1',
reps: 15,
duration: null,
includeRestPeriod: true,
restDuration: 20,
},
];
const postData: IPostData = {
key: 'YOUR_API_KEY',
userId: 'user-123',
company: 'YOUR_COMPANY',
customWorkoutExercises: customWorkoutExercises, // direct
customParameters: {
shouldSendStats: true, // optional: save the session on completion
},
};
```
_Flutter_
```dart
// Via customParams for additional settings
final customExercises = [
WorkoutSequenceExercise(
exerciseId: "exercise-id-1",
reps: 15,
duration: null,
includeRestPeriod: true,
restDuration: 20,
),
];
KinesteXAIFramework.createCustomWorkoutView(
customWorkouts: customExercises, // direct
customParams: {
"shouldSendStats": true, // optional: save the session on completion
},
);
```
_HTML / JavaScript_
```html
// Direct in postData object
const customWorkoutExercises = [
{
exerciseId: "exercise-id-1",
reps: 15,
duration: null,
includeRestPeriod: true,
restDuration: 20,
},
];
const postData = {
userId: "user-123",
company: "YOUR_COMPANY",
key: "YOUR_API_KEY",
customWorkoutExercises: customWorkoutExercises,
shouldSendStats: true, // optional: save the session on completion
};
```
_React (TypeScript)_
```tsx
// Direct support in postData
const customWorkoutExercises: WorkoutSequenceExercise[] = [
{
exerciseId: 'exercise-id-1',
reps: 15,
duration: null,
includeRestPeriod: true,
restDuration: 20,
},
];
const postData: IPostData = {
key: 'YOUR_API_KEY',
userId: 'user-123',
company: 'YOUR_COMPANY',
customWorkoutExercises: customWorkoutExercises, // direct
customParameters: {
shouldSendStats: true, // optional: save the session on completion
},
};
```
### Workout Activity Actions
Control workout state programmatically at runtime via postMessage using the `workout_activity_action` property.
| Action | Applies to | What it does internally |
|--------|------------|-------------------------|
| pause_workout | Workout player | Pauses the workout (video, timer, and tracking) and echoes a `pause_workout` confirmation message back |
| resume_workout | Workout player | Resumes a paused workout (echoes `resume_workout`) |
| mute_workout | Workout player | Mutes ALL audio — speech, sounds, and background music (echoes `mute_workout`) |
| unmute_workout | Workout player | Unmutes all audio (echoes `unmute_workout`) |
| mute_speech | Workout player | Mutes only spoken feedback; sound effects still play (echoes `mute_speech`) |
| unmute_speech | Workout player | Unmutes spoken feedback (echoes `unmute_speech`) |
| start | Custom Workout only | Starts the workout built from `customWorkoutExercises` — send it after `all_resources_loaded` arrives. Ignored on other integration options |
| load_models | Camera only | Fetches and caches additional exercise models mid-session: `{ workout_activity_action: "load_models", exercises: ["id1", "id2"], exerciseFetchType?: "model_id" }`. Replies with `models_loaded` / `error_occurred`; loaded IDs become switchable via `currentExercise` |
**Scope note:** the pause/mute family only affects the standard workout player (Main, Workout, Plan, Personalized Plan, Custom Workout while an exercise is running). The Camera component, games, and assessments do not react to these actions — the Camera component has its own control commands sent through `currentExercise` (see [Camera Component](/docs/customization-parameters/camera-component-params)).
**Note:** These actions are sent via postMessage to the KinesteX iframe/webview after the initial verification.
**Workout Activity Actions**
_Swift (iOS)_
```swift
// Send workout activity action using @State binding
// First, declare the state variable and pass it to the view:
// @State var workoutAction: [String: Any]? = nil
// kinestex.createWorkoutView(..., workoutAction: $workoutAction, ...)
// Pause the workout
workoutAction = ["workout_activity_action": "pause_workout"]
// Resume the workout
workoutAction = ["workout_activity_action": "resume_workout"]
// Mute all audio
workoutAction = ["workout_activity_action": "mute_workout"]
// Unmute all audio
workoutAction = ["workout_activity_action": "unmute_workout"]
// Mute only speech (sounds still play)
workoutAction = ["workout_activity_action": "mute_speech"]
// Unmute speech
workoutAction = ["workout_activity_action": "unmute_speech"]
```
_Kotlin (Android)_
```kotlin
// Send workout activity action
// Pause the workout
KinesteXSDK.sendAction("workout_activity_action", "pause_workout")
// Resume the workout
KinesteXSDK.sendAction("workout_activity_action", "resume_workout")
// Mute all audio
KinesteXSDK.sendAction("workout_activity_action", "mute_workout")
// Unmute all audio
KinesteXSDK.sendAction("workout_activity_action", "unmute_workout")
// Mute only speech (sounds still play)
KinesteXSDK.sendAction("workout_activity_action", "mute_speech")
// Unmute speech
KinesteXSDK.sendAction("workout_activity_action", "unmute_speech")
```
_React Native_
```jsx
// Send workout activity action
const kinestexSDKRef = useRef(null);
// Pause the workout
kinestexSDKRef.current?.sendAction("workout_activity_action", "pause_workout");
// Resume the workout
kinestexSDKRef.current?.sendAction("workout_activity_action", "resume_workout");
// Mute all audio
kinestexSDKRef.current?.sendAction("workout_activity_action", "mute_workout");
// Unmute all audio
kinestexSDKRef.current?.sendAction("workout_activity_action", "unmute_workout");
// Mute only speech (sounds still play)
kinestexSDKRef.current?.sendAction("workout_activity_action", "mute_speech");
// Unmute speech
kinestexSDKRef.current?.sendAction("workout_activity_action", "unmute_speech");
```
_Flutter_
```dart
// Send workout activity action
// Pause the workout
KinesteXAIFramework.sendAction("workout_activity_action", "pause_workout");
// Resume the workout
KinesteXAIFramework.sendAction("workout_activity_action", "resume_workout");
// Mute all audio
KinesteXAIFramework.sendAction("workout_activity_action", "mute_workout");
// Unmute all audio
KinesteXAIFramework.sendAction("workout_activity_action", "unmute_workout");
// Mute only speech (sounds still play)
KinesteXAIFramework.sendAction("workout_activity_action", "mute_speech");
// Unmute speech
KinesteXAIFramework.sendAction("workout_activity_action", "unmute_speech");
```
_HTML / JavaScript_
```html
// Send workout activity action via postMessage
const iframe = document.getElementById('kinestex-iframe');
// Pause the workout
iframe.contentWindow.postMessage({
workout_activity_action: "pause_workout"
}, "*");
// Resume the workout
iframe.contentWindow.postMessage({
workout_activity_action: "resume_workout"
}, "*");
// Mute all audio
iframe.contentWindow.postMessage({
workout_activity_action: "mute_workout"
}, "*");
// Unmute all audio
iframe.contentWindow.postMessage({
workout_activity_action: "unmute_workout"
}, "*");
// Mute only speech (sounds still play)
iframe.contentWindow.postMessage({
workout_activity_action: "mute_speech"
}, "*");
// Unmute speech
iframe.contentWindow.postMessage({
workout_activity_action: "unmute_speech"
}, "*");
```
_React (TypeScript)_
```tsx
// Send workout activity action
import { useRef } from 'react';
import { type KinesteXSDKCamera } from 'kinestex-sdk-react-ts';
const ref = useRef(null);
// Pause the workout
ref.current?.sendAction("workout_activity_action", "pause_workout");
// Resume the workout
ref.current?.sendAction("workout_activity_action", "resume_workout");
// Mute all audio
ref.current?.sendAction("workout_activity_action", "mute_workout");
// Unmute all audio
ref.current?.sendAction("workout_activity_action", "unmute_workout");
// Mute only speech (sounds still play)
ref.current?.sendAction("workout_activity_action", "mute_speech");
// Unmute speech
ref.current?.sendAction("workout_activity_action", "unmute_speech");
```
### Navigation
Control navigation and routing behavior. **Applies to: All integrations.**
| Parameter | Type | Default | Applies to | What it does internally |
|-----------|------|---------|------------|-------------------------|
| instantRedirect | string | — | All | Immediately navigates the WebView to the given internal route as soon as the launch data arrives (even before verification finishes). Any in-app route is accepted; a leading `/` is added if missing. There is no allowlist — an invalid route lands on the error screen |
**Navigation Control**
_Swift (iOS)_
```swift
// Via customParams
kinestex.createView(
customParams: [
"instantRedirect": "/workout/start"
]
)
```
_Kotlin (Android)_
```kotlin
// Via customParams
KinesteXSDK.createView(
customParams = mutableMapOf(
"instantRedirect" to "/workout/start"
)
)
```
_React Native_
```jsx
// Via customParameters
const postData: IPostData = {
key: 'YOUR_API_KEY',
userId: 'user-123',
company: 'YOUR_COMPANY',
customParameters: {
instantRedirect: '/workout/start',
},
};
```
_Flutter_
```dart
// Via customParams
KinesteXAIFramework.createMainView( // or any other create*View method
customParams: {
"instantRedirect": "/workout/start",
},
);
```
_HTML / JavaScript_
```html
// Direct in postData object
const postData = {
userId: "user-123",
company: "YOUR_COMPANY",
key: "YOUR_API_KEY",
instantRedirect: "/workout/start",
};
```
_React (TypeScript)_
```tsx
// Via customParameters
const postData: IPostData = {
key: 'YOUR_API_KEY',
userId: 'user-123',
company: 'YOUR_COMPANY',
customParameters: {
instantRedirect: '/workout/start',
},
};
```
### Assessment Configuration
Parameters specific to assessment modes (TUG test, gait speed, balance tests, etc.). **Applies to: Assessments only.** For complete assessment data structures, see [Data Points](/docs/data-points).
| Parameter | Type | Default | Applies to | What it does internally |
|-----------|------|---------|------------|-------------------------|
| tugMinRequiredSpace | number | 3.2 | TUG + Gait Speed assessments | Required walk distance in **meters** (must be > 0). Drives the space-validation step, the walk target position, and the reported speeds: TUG assumes 2 × the distance (out and back), Gait Speed assumes 1 ×, so `averageSpeedMs_tug` = (2 × distance) / time and `averageGaitSpeed` = distance / time |
| isHorizontalMode | boolean | **false** | TUG + Gait Speed assessments, mobile/tablet only | Only an explicit `true` (or string `"true"`) enables it. Runs the walking tests in landscape, widening the camera's field of view so the test fits a much smaller space. Users get "Rotate your device" prompts; your WebView/activity must allow landscape orientation. Ignored on desktop. Also accepted as a URL query param. The flag is reset on every launch — it never carries over from a previous session |
**Model accuracy note:** assessments use the same auto-selected MediaPipe model as everything else (`light`/`full` by device speed). There is **no automatic switch to the "heavy" model for balance assessments** — pass `mediapipeModel: "heavy"` explicitly if you need maximum landmark accuracy ([Camera & Pose Detection](/docs/customization-parameters/camera-pose-detection)).
**Assessment Configuration**
_Swift (iOS)_
```swift
// Via customParams for TUG assessment
kinestex.createAssessmentView(
exercise: "tugtest", // direct
customParams: [
"tugMinRequiredSpace": 3
]
)
```
_Kotlin (Android)_
```kotlin
// Via customParams for TUG assessment
KinesteXSDK.createAssessmentView(
exercise = "tugtest", // direct
customParams = mutableMapOf(
"tugMinRequiredSpace" to 3
)
)
```
_React Native_
```jsx
// Via customParameters
const postData: IPostData = {
key: 'YOUR_API_KEY',
userId: 'user-123',
company: 'YOUR_COMPANY',
customParameters: {
exercise: 'tugtest',
tugMinRequiredSpace: 3,
},
};
```
_Flutter_
```dart
// Via customParams
KinesteXAIFramework.createExperienceView(
experience: "assessment",
customParams: {
"exercise": "tugtest",
"tugMinRequiredSpace": 3,
},
);
```
_HTML / JavaScript_
```html
// Direct in postData object
const postData = {
userId: "user-123",
company: "YOUR_COMPANY",
key: "YOUR_API_KEY",
exercise: "tugtest",
tugMinRequiredSpace: 3,
};
```
_React (TypeScript)_
```tsx
// Via customParameters
const postData: IPostData = {
key: 'YOUR_API_KEY',
userId: 'user-123',
company: 'YOUR_COMPANY',
customParameters: {
exercise: 'tugtest',
tugMinRequiredSpace: 3,
},
};
```
### Audio Configuration
Control audio playback settings. **Applies to: all views with spoken audio feedback.**
| Parameter | Type | Default | Applies to | What it does internally |
|-----------|------|---------|------------|-------------------------|
| enableM4a | boolean | false | All | Forces speech audio to be fetched in M4A format instead of the default WebM. Use for WebViews/browsers where WebM playback is unreliable (notably some iOS WKWebView contexts) |
| includePhrases | string[] | all categories | All | Allow-list of spoken coaching-cue categories. Core `static` cues (essential instructions) are ALWAYS included and cannot be disabled. Omit the parameter for all categories; pass an explicit list to enable exactly those — e.g. `["greeting", "farewell"]` re-enables the greeting/farewell voice cues for hosts that had them silenced. An empty array `[]` means static cues only |
**Valid `includePhrases` categories:** `"motivational"`, `"praisal"`, `"greeting"`, `"farewell"`, `"closing_exercise"`, `"auto_skip"`. Unknown values are silently dropped.
**Voice selection** (`voiceActor`) and **language** are documented under [Language & Localization](/docs/customization-parameters/language-localization).
**Audio Configuration**
_Swift (iOS)_
```swift
// Via customParams
kinestex.createView(
customParams: [
"enableM4a": true
]
)
```
_Kotlin (Android)_
```kotlin
// Via customParams
KinesteXSDK.createView(
customParams = mutableMapOf(
"enableM4a" to true
)
)
```
_React Native_
```jsx
// Via customParameters
const postData: IPostData = {
key: 'YOUR_API_KEY',
userId: 'user-123',
company: 'YOUR_COMPANY',
customParameters: {
enableM4a: true,
},
};
```
_Flutter_
```dart
// Via customParams
KinesteXAIFramework.createMainView( // or any other create*View method
customParams: {
"enableM4a": true,
},
);
```
_HTML / JavaScript_
```html
// Direct in postData object
const postData = {
userId: "user-123",
company: "YOUR_COMPANY",
key: "YOUR_API_KEY",
enableM4a: true,
};
```
_React (TypeScript)_
```tsx
// Via customParameters
const postData: IPostData = {
key: 'YOUR_API_KEY',
userId: 'user-123',
company: 'YOUR_COMPANY',
customParameters: {
enableM4a: true,
},
};
```
### Session & Data Saving
Control session saving and data upload behavior. **Applies to: any flow ending on the statistics screen** (Workout player and Challenge completions).
| Parameter | Type | Default | Applies to | What it does internally |
|-----------|------|---------|------------|-------------------------|
| shouldSendStats | boolean | false (true for AI Trainer) | Workout player | When enabled, the completed session is automatically saved to the KinesteX backend — per-exercise stats, accuracy scores, calories, and the motion recording (used for session replay). Saved sessions are retrievable via the [Workout Sessions API](/docs/trainer-api/trainer-api-workout-sessions). For AI Trainer launches the default is `true` so the chat can show the completed-workout card |
**Messages posted to the host during saving:**
- `workout_session_saved` — the session document was stored (includes the session ID and scores)
- `motion_upload_progress` — motion-recording upload progress
- `session_save_complete` — everything (session + motion data) finished uploading
- `motion_upload_error` — the motion upload failed or timed out
**Offline behavior:** if saving fails (e.g. no connectivity), the result is queued locally and retried automatically on the next launch — the corresponding messages fire when the retry succeeds.
**Note:** independent of this flag, the statistics screen always posts the `workout_overview` / `exercise_overview` summary messages to the host. `shouldSendStats` only controls **backend persistence**. If `motionDataEnabled: false` was passed, the saved session will have no replay data.
**Session Saving Configuration**
_Swift (iOS)_
```swift
// Via customParams
kinestex.createWorkoutView(
workout: "Full Body Burn",
customParams: [
"shouldSendStats": true
]
)
```
_Kotlin (Android)_
```kotlin
// Via customParams
KinesteXSDK.createWorkoutView(
workoutName = "Full Body Burn",
customParams = mutableMapOf(
"shouldSendStats" to true
)
)
```
_React Native_
```jsx
// Via customParameters
const postData: IPostData = {
key: 'YOUR_API_KEY',
userId: 'user-123',
company: 'YOUR_COMPANY',
customParameters: {
shouldSendStats: true,
},
};
```
_Flutter_
```dart
// Via customParams
KinesteXAIFramework.createWorkoutView(
workoutName: "Full Body Burn",
customParams: {
"shouldSendStats": true,
},
);
```
_HTML / JavaScript_
```html
// Direct in postData object
const postData = {
userId: "user-123",
company: "YOUR_COMPANY",
key: "YOUR_API_KEY",
shouldSendStats: true,
};
```
_React (TypeScript)_
```tsx
// Via customParameters
const postData: IPostData = {
key: 'YOUR_API_KEY',
userId: 'user-123',
company: 'YOUR_COMPANY',
customParameters: {
shouldSendStats: true,
},
};
```
### Plan Context
Pass plan progression context so the SDK associates a directly-launched workout with the correct plan. **Applies to: Workout view** (a workout launched directly that should count toward a plan). After the workout finishes, the SDK posts `plan_progression_saved` (or `plan_progression_failed` on error). See [Plans & Programs events](/docs/data-points/plans-programs).
| Parameter | Type | Applies to | What it does internally |
|-----------|------|------------|-------------------------|
| planId | string | Workout view | The ID of the plan the workout belongs to — used on the statistics screen to resolve the plan and record progression against it |
| planType | string | Workout view | `"personalized"` or `"goal-based"`. Selects which progression pipeline records the completion |
| progressWorkoutId | string | Workout view | The plan-day workout ID used to record progression when the workout wasn't launched from the in-app plan UI |
**When to use:** Only when you launch a workout **directly** via the SDK (not through the in-app plan UI) and want it to count toward plan progression. The in-app plan flow handles this automatically — you don't need to pass these fields if the user navigates from the plan dashboard.
**Progression is only recorded for a meaningful session:** personalized-plan progression requires a minimum efficiency score, and goal-based progression requires some completed work (several reps or ~10s of hold time). A workout abandoned immediately won't advance the plan.
**Note:** If you launch a workout without these fields, it is treated as a standalone session and won't update plan progression.
**Plan Context Configuration**
_Swift (iOS)_
```swift
// Pass plan context when launching a workout directly
kinestex.createWorkoutView(
workout: "Day 3 Workout",
customParams: [
"planId": "plan_abc123",
"planType": "personalized",
"progressWorkoutId": "plan_day_3"
]
)
```
_Kotlin (Android)_
```kotlin
// Pass plan context when launching a workout directly
KinesteXSDK.createWorkoutView(
workoutName = "Day 3 Workout",
customParams = mutableMapOf(
"planId" to "plan_abc123",
"planType" to "personalized",
"progressWorkoutId" to "plan_day_3"
)
)
```
_React Native_
```jsx
// Pass plan context via customParameters
const postData: IPostData = {
key: 'YOUR_API_KEY',
userId: 'user-123',
company: 'YOUR_COMPANY',
customParameters: {
planId: 'plan_abc123',
planType: 'personalized',
progressWorkoutId: 'plan_day_3',
},
};
```
_Flutter_
```dart
// Pass plan context via customParams
KinesteXAIFramework.createWorkoutView(
workoutName: "Day 3 Workout",
customParams: {
"planId": "plan_abc123",
"planType": "personalized",
"progressWorkoutId": "plan_day_3",
},
);
```
_HTML / JavaScript_
```html
// Direct in postData object
const postData = {
userId: "user-123",
company: "YOUR_COMPANY",
key: "YOUR_API_KEY",
planId: "plan_abc123",
planType: "personalized",
progressWorkoutId: "plan_day_3",
};
```
_React (TypeScript)_
```tsx
// Pass plan context via customParameters
const postData: IPostData = {
key: 'YOUR_API_KEY',
userId: 'user-123',
company: 'YOUR_COMPANY',
customParameters: {
planId: 'plan_abc123',
planType: 'personalized',
progressWorkoutId: 'plan_day_3',
},
};
```
### Plan Onboarding Prefill
Pre-fill or skip onboarding survey questions for personalized plans. All fields are optional — only provided fields will be pre-filled and their corresponding screens will be skipped. If all answers are provided for a goal-based plan, the user goes straight to results. **Applies to: Plan Onboarding only** (route `"plan-onboarding"`).
**Availability:** This parameter is used with `createCustomComponentView` (Swift, Kotlin, Flutter) or the `CUSTOM_COMPONENT` integration option (React Native) with `route: "plan-onboarding"`.
| Parameter | Type | Accepted values | Description |
|-----------|------|-----------------|-------------|
| route | string | `"plan-onboarding"` | Must be `"plan-onboarding"` for this feature to work |
| planOnboardingPrefill | object | — | Object containing prefill values for the onboarding survey |
| planOnboardingPrefill.goal | string | `"automatic"`, `"lose_weight"`, `"build_muscle"`, `"get_stronger"`, `"stay_active"` | The fitness goal. `"automatic"` runs the assessment-based personalized path (adds the Duration and Assessment steps); the other goals map to fixed goal-based plans and skip the Duration step |
| planOnboardingPrefill.healthIssues | string[] | `"None"`, `"High Blood Pressure"`, `"Diabetes"`, `"High Cholesterol"`, `"Insomnia"`, `"Depression"` | Pre-selects health issues; the health-issues screen is skipped when provided |
| planOnboardingPrefill.injuries | string[] | `"None"`, `"Shoulder"`, `"Wrist"`, `"Knee"`, `"Hip"`, `"Ankle"`, `"Lower back"` | Pre-selects injuries (empty array = none); the injuries screen is skipped when provided |
| planOnboardingPrefill.duration | number | `15`, `30`, `60`, `90` | Preferred workout duration in minutes. Only relevant for the `"automatic"` goal path |
| planOnboardingPrefill.lifestyle | string | `"Sedentary"`, `"Lightly active"`, `"Moderately active"`, `"Very active"` | Activity level (note: capitalized with spaces — this screen's vocabulary differs from the top-level `lifestyle` parameter) |
| planOnboardingPrefill.assessmentOnly | boolean | `true` / `false` | When `true`, all survey screens (goal, health issues, injuries, duration, lifestyle) are skipped and the user lands directly on the fitness assessment. Any previously stored plan ID is cleared so a fresh personalized plan is generated after the assessment completes. All other prefill fields are ignored when this is set |
**Gotcha:** answers the user already gave in a locally cached, in-progress onboarding session take precedence over prefill values, field by field.
**`assessmentOnly` use case:** Use this for **reassessment flows** — when the user already has a profile (goal, lifestyle, etc.) recorded in your host app and you only want them to perform a fresh fitness assessment to regenerate their personalized plan. Don't combine it with the other prefill fields; they will be ignored.
**`remind_me_later_clicked` event:** When the user is on the Assessment screen inside the plan-onboarding flow and taps "Remind me later", the SDK posts a `remind_me_later_clicked` PostMessage so your host app can dismiss the SDK and schedule a follow-up prompt. This event is only fired from the plan-onboarding page.
**Plan Onboarding Prefill Configuration**
_Swift (iOS)_
```swift
// Use createCustomComponentView with route "plan-onboarding"
kinestex.createCustomComponentView(
route: "plan-onboarding",
customParams: [
"planOnboardingPrefill": [
"goal": "lose_weight",
"healthIssues": ["High Blood Pressure"],
"injuries": [],
"duration": 30,
"lifestyle": "Sedentary"
]
]
)
// Reassessment-only flow (skips the entire survey)
kinestex.createCustomComponentView(
route: "plan-onboarding",
customParams: [
"planOnboardingPrefill": [
"assessmentOnly": true
]
]
)
```
_Kotlin (Android)_
```kotlin
// Use createCustomComponentView with route "plan-onboarding"
KinesteXSDK.createCustomComponentView(
route = "plan-onboarding",
customParams = mutableMapOf(
"planOnboardingPrefill" to mapOf(
"goal" to "lose_weight",
"healthIssues" to listOf("High Blood Pressure"),
"injuries" to emptyList(),
"duration" to 30,
"lifestyle" to "Sedentary"
)
)
)
```
_React Native_
```jsx
// Use CUSTOM_COMPONENT integration with route "plan-onboarding"
const postData: IPostData = {
key: 'YOUR_API_KEY',
userId: 'user-123',
company: 'YOUR_COMPANY',
customParameters: {
planOnboardingPrefill: {
goal: 'lose_weight',
healthIssues: ['High Blood Pressure'],
injuries: [],
duration: 30,
lifestyle: 'Sedentary',
},
},
};
```
_Flutter_
```dart
// Use createCustomComponentView with route "plan-onboarding"
KinesteXAIFramework.createCustomComponentView(
route: "plan-onboarding",
customParams: {
"planOnboardingPrefill": {
"goal": "lose_weight",
"healthIssues": ["High Blood Pressure"],
"injuries": [],
"duration": 30,
"lifestyle": "Sedentary",
},
},
);
```
_HTML / JavaScript_
```html
const srcURL = "https://ai.kinestex.com/plan-onboarding";
const postData = {
userId: "user-123",
company: "YOUR_COMPANY",
key: "YOUR_API_KEY",
planOnboardingPrefill: {
goal: "lose_weight",
healthIssues: ["High Blood Pressure"],
injuries: [],
duration: 30,
lifestyle: "Sedentary",
},
};
// Reassessment-only flow (skips the entire survey)
const reassessmentPostData = {
userId: "user-123",
company: "YOUR_COMPANY",
key: "YOUR_API_KEY",
planOnboardingPrefill: {
assessmentOnly: true,
},
};
```
_React (TypeScript)_
```tsx
// Use CUSTOM_COMPONENT integration with route "plan-onboarding"
const postData: IPostData = {
key: 'YOUR_API_KEY',
userId: 'user-123',
company: 'YOUR_COMPANY',
customParameters: {
planOnboardingPrefill: {
goal: 'lose_weight',
healthIssues: ['High Blood Pressure'],
injuries: [],
duration: 30,
lifestyle: 'Sedentary',
},
},
};
```
### AI Trainer Chat Parameters
Parameters specific to the [AI Trainer Chat](/docs/ai-trainer-chat) view. **Applies to: AI Trainer only** — every other integration option ignores them.
| Parameter | Type | Default | Applies to | What it does internally |
|-----------|------|---------|------------|-------------------------|
| isSubscribed | boolean | true | AI Trainer | Missing or `true` = subscribed. Only an explicit `false` (string `"false"` is also accepted for URL launches) gates workout generation and triggers the `open_subscription_flow` event — see the [subscription gating guide](/docs/guides/guide-subscription-gating). **For session-authenticated companies the backend's subscription status overrides this flag entirely** — the server is the source of truth and also gates generation server-side |
| subscriptionReturnUrl | string | — | AI Trainer | For link/standalone integrations with no host app: non-subscribed users are redirected here at the generation step instead of receiving `open_subscription_flow`. Accepts https:// URLs or app deep links; script-executing schemes are rejected |
| aiTrainerName | string | — | AI Trainer | Renames the AI trainer everywhere it's labelled (header title, reply labels). Shown verbatim in every language |
| aiTrainerColor | string | — | AI Trainer | Recolors the trainer's star icon — any CSS color, e.g. `"#7C3AED"`. Missing = the theme's brand color |
**Runtime subscription reply:** after your host handles `open_subscription_flow`, post `{ subscription_result: "purchased" }` (resumes the parked generation) or `{ subscription_result: "dismissed" }` back to the WebView.
**Fitness-profile prefill fields** (same names as the `trainer_profile_updated` event, so you can round-trip the schema; invalid values are silently dropped, never errored):
| Field | Accepted values |
|-------|-----------------|
| fitness_goals | string[] from: `weight_loss`, `muscle_gain`, `strength`, `general_fitness`, `wellness_flexibility`, `cardio_endurance` |
| injuries | Array of `{ body_part, preference, severity }`. `body_part`: Neck, Shoulders, Chest, Arms, Forearms, Abdomen, Knees, Hips, Ankles, Upper Back, Lower Back, Glutes, Hamstrings, Calves, Heels (case/underscore-insensitive). `preference`: `avoid` (default) or `include`. `severity` (optional): `light`, `moderate`, `severe`. Empty array = explicitly "no injuries" |
| health_conditions | string[] — standard values: Diabetes, Hypertension, Asthma, Osteoporosis, Other. Non-standard strings are kept as free-text "Other" details |
| fitness_level_pushups | `"0-5"`, `"6-15"`, `"16+"` |
| fitness_level_squats | `"0-10"`, `"11-21"`, `"22+"` |
| fitness_level_cardio | One of the cardio self-assessment sentences shown in the trainer wizard (unrecognized values make the wizard re-ask) |
| other_preferences | Free text |
See [Pre-filling user data](/docs/ai-trainer-chat#prefill) for the full guide. Prefill seeds the wizard's answers — the questions are still shown, pre-answered. Demographics (`age`/`height`/`weight`/`gender`/`lifestyle`) also prefill the trainer, but only when explicitly provided at launch.
### URL Parameters
Almost every customization parameter can also be passed via the URL query string — useful for standalone links and web embeds where postMessage isn't convenient. **Applies to: All integrations.**
**Two URL modes:**
1. **Single encoded payload (recommended for complex configs):** `?data=` — the entire parameter object, base64-encoded. When present, it replaces all other individual query params.
2. **Individual plain parameters:** `?age=25&gender=male&style=dark&showSilhouette=false` — each parameter as a plain key/value. Values are automatically type-converted: known boolean parameters accept `true/false/1/0`, known numeric parameters are parsed as numbers, and values that look like JSON (starting with `[` or `{`) are JSON-parsed.
**Precedence:** values sent via postMessage **override** URL values (URL acts as defaults) — with two exceptions that work the other way around:
| URL Parameter | Equivalent Config | Example | Precedence |
|---------------|-------------------|---------|------------|
| style | style | ?style=light | **URL wins** over postMessage |
| delegate | defaultDelegate | ?delegate=GPU | **URL wins** over postMessage |
| debug | showDebugRecording | ?debug=true | Combined with postMessage value (either enables) |
**Type-converted parameter names** (when passed individually in the URL):
- Booleans: `isHideHeaderMain`, `isDrawingPose`, `resetPlanProgress`, `hideFeelingDialog`, `hideOtherGender`, `includeRealtimeAccuracy`, `showSilhouette`, `showDebugRecording`, `showFormGuidance`, `streamFormGuidance`, `includePoseBorders`, `showLeaderboard`, `shouldAskCamera`, `shouldShowCameraSelector`, `showSettings`, `motionTrackingSettingOn`, `motionTrackingEnabled`, `start_from_rest`, `enableM4a`, `nativeParentScroll`, `isHorizontalMode`
- Numbers: `age`, `height`, `weight`, `countdown`, `reps`, `minPoseDetectionConfidence`, `minTrackingConfidence`, `minPosePresenceConfidence`, `tugMinRequiredSpace`
Parameters not in these lists arrive as strings (or JSON-parsed objects/arrays) — the same scoping rules from their sections apply.
**Exception — `includePoseData`:** this parameter is a string array and **cannot be passed as an individual URL query param** (the value would not survive URL type conversion). Pass it via postMessage or inside the `?data=` payload instead.
### Complete Example
Here's a comprehensive example combining multiple parameter categories:
**Complete Configuration Example**
_Swift (iOS)_
```swift
// Complete configuration example
let kinestex = KinesteXAIKit(
apiKey: "YOUR_API_KEY",
companyName: "MyFitnessApp",
userId: "user_12345"
)
let user = UserDetails(
age: 28,
height: 165,
weight: 60,
gender: .Female,
lifestyle: .Active
)
// Theme & Loading via IStyle class (hex values with #)
let customStyle = IStyle(
style: "dark",
loadingBackgroundColor: "#1A1A2E",
loadingStickmanColor: "#00FF88",
loadingTextColor: "#FFFFFF"
)
kinestex.createWorkoutView(
workout: "Fitness Lite",
user: user,
style: customStyle,
isLoading: $isLoading,
// Other customization via customParams
customParams: [
// Language
"language": "es",
// UI Controls
"disableGuide": true,
"hideFeelingDialog": true,
// Camera
"landmarkColor": "#00FF88",
"showSilhouette": true,
"shouldShowCameraSelector": true,
// Leaderboard
"showLeaderboard": true,
"username": "FitUser28"
],
onMessageReceived: { message in
// Handle messages
}
)
```
_Kotlin (Android)_
```kotlin
// Complete configuration example
KinesteXSDK.initialize(
context = this,
apiKey = "YOUR_API_KEY",
companyName = "MyFitnessApp",
userId = "user_12345"
)
val userDetails = UserDetails(
age = 28,
height = 165,
weight = 60,
gender = Gender.FEMALE,
lifestyle = Lifestyle.ACTIVE
)
KinesteXSDK.createWorkoutView(
context = this,
workoutName = workoutId,
user = userDetails,
// Theme & Loading via IStyle class (hex values without #)
style = IStyle(
style = "dark",
loadingBackgroundColor = "1A1A2E",
loadingStickmanColor = "e94560",
loadingTextColor = "FFFFFF"
),
isLoading = viewModel.isLoading,
// Other customization via customParams
customParams = mutableMapOf(
// Language
"language" to "es",
// UI Controls
"disableGuide" to true,
"hideFeelingDialog" to true,
// Camera
"landmarkColor" to "#00FF88",
"showSilhouette" to true,
// Leaderboard
"showLeaderboard" to true,
"username" to "FitUser28"
),
onMessageReceived = { message ->
handleWebViewMessage(message)
},
permissionHandler = this
)
```
_React Native_
```jsx
// Complete configuration example
const postData: IPostData = {
// Required
key: 'YOUR_API_KEY',
userId: 'user_12345',
company: 'MyFitnessApp',
// User Profile (direct support)
age: 28,
height: 165,
weight: 60,
gender: 'Female',
lifestyle: Lifestyle.Active,
// Theme (direct support)
style: {
style: 'dark',
loadingBackgroundColor: '1A1A2E',
loadingTextColor: 'FFFFFF',
},
// Additional parameters via customParameters
customParameters: {
// Language
language: 'es',
// UI Controls
disableGuide: true,
hideFeelingDialog: true,
// Camera
landmarkColor: '#00FF88',
showSilhouette: true,
shouldShowCameraSelector: true,
// Leaderboard
showLeaderboard: true,
username: 'FitUser28',
},
};
```
_Flutter_
```dart
// Complete configuration example
await KinesteXAIFramework.initialize(
apiKey: "YOUR_API_KEY",
companyName: "MyFitnessApp",
userId: "user_12345",
);
final userDetails = UserDetails(
age: 28,
height: 165,
weight: 60,
gender: Gender.Female,
lifestyle: Lifestyle.Active,
);
KinesteXAIFramework.createWorkoutView(
workoutName: "Fitness Lite",
user: userDetails,
isShowKinestex: showKinesteX,
isLoading: ValueNotifier(false),
// Theme & Loading via IStyle class
style: IStyle(
style: 'dark',
loadingBackgroundColor: '1A1A2E', // hex without #
loadingStickmanColor: 'e94560',
loadingTextColor: 'FFFFFF',
),
// Other customization via customParams
customParams: {
// Language
"language": "es",
// UI Controls
"disableGuide": true,
"hideFeelingDialog": true,
// Camera
"landmarkColor": "#00FF88",
"showSilhouette": true,
// Leaderboard
"showLeaderboard": true,
"username": "FitUser28",
},
onMessageReceived: (message) {
handleWebViewMessage(message);
},
);
```
_HTML / JavaScript_
```html
// Complete configuration example
// All parameters passed as flat object
const postData = {
// Required
userId: "user_12345",
company: "MyFitnessApp",
key: "YOUR_API_KEY",
// User Profile
age: 28,
height: 165,
weight: 60,
gender: "Female",
// Theme
style: "dark",
// Language
language: "es",
// UI Controls
disableGuide: true,
hideFeelingDialog: true,
// Camera
landmarkColor: "#00FF88",
showSilhouette: true,
shouldShowCameraSelector: true,
// Leaderboard
showLeaderboard: true,
username: "FitUser28",
// Loading
loadingBackgroundColor: "#1A1A2E",
loadingTextColor: "#FFFFFF",
};
// Send to iframe
webView.contentWindow.postMessage(postData, srcURL);
```
_React (TypeScript)_
```tsx
// Complete configuration example
const postData: IPostData = {
// Required
key: 'YOUR_API_KEY',
userId: 'user_12345',
company: 'MyFitnessApp',
// User Profile (direct support)
age: 28,
height: 165,
weight: 60,
gender: 'Female',
lifestyle: Lifestyle.Active,
// Theme (direct support)
style: {
style: 'dark',
loadingBackgroundColor: '1A1A2E',
loadingTextColor: 'FFFFFF',
},
// Additional parameters via customParameters
customParameters: {
// Language
language: 'es',
// UI Controls
disableGuide: true,
hideFeelingDialog: true,
// Camera
landmarkColor: '#00FF88',
showSilhouette: true,
shouldShowCameraSelector: true,
// Leaderboard
showLeaderboard: true,
username: 'FitUser28',
},
};
```
---
## Content API
The KinesteX Content API provides access to workout plans, individual workouts, and exercises. Use it to build custom content browsers, create personalized workout recommendations, or integrate KinesteX content into your app.
**SDK vs REST API:**
- **Swift, Kotlin, Flutter**: Use the built-in SDK convenience methods (recommended)
- **React Native, React TypeScript, HTML/JS**: Use direct REST API calls
**Base URL:** `https://admin.kinestex.com/api/v1/`
**Available Endpoints:**
| Endpoint | Description |
|----------|-------------|
| /workouts | Fetch workout content |
| /plans | Fetch workout plan content |
| /exercises | Fetch exercise content |
**Headers (REST API only):**
| Header | Description |
|--------|-------------|
| x-api-key | Your API key for authentication |
| x-company-name | The name of your company |
### Getting Started
Before using the Content API, ensure your SDK is properly initialized. The API methods are only available after initialization.
**For SDK platforms (Swift, Kotlin, Flutter):** Initialize the SDK with your credentials
**For REST platforms (React Native, React TypeScript, HTML/JS):** Set up your request headers
**Initialize SDK / Setup Headers**
_Swift (iOS)_
```swift
import KinesteXAIKit
// Initialize KinesteXAIKit with your credentials
let kinestex = KinesteXAIKit(
apiKey: "YOUR_API_KEY",
companyName: "YOUR_COMPANY",
userId: "user_123"
)
// Now you can use the Content API methods:
// - kinestex.fetchWorkouts()
// - kinestex.fetchPlans()
// - kinestex.fetchExercises()
// - kinestex.fetchWorkout(id:)
// - kinestex.fetchPlan(id:)
// - kinestex.fetchExercise(id:)
// - kinestex.fetchContent(contentType:, ...)
```
_Kotlin (Android)_
```kotlin
import com.kinestex.kinestexsdkkotlin.KinesteXSDK
// SDK must be initialized before using Content API
// This is typically done in your Application class or Activity
// Access Content API through KinesteXSDK.api
// Available method:
// KinesteXSDK.api.fetchAPIContentData(
// contentType: ContentType,
// id: String? = null,
// title: String? = null,
// category: String? = null,
// bodyParts: List? = null,
// lastDocId: String? = null,
// limit: Int? = null
// ): APIContentResult
```
_Flutter_
```dart
import 'package:kinestex_sdk_flutter/kinestex_sdk.dart';
// Initialize the SDK before using Content API
await KinesteXAIFramework.initialize(
apiKey: "YOUR_API_KEY",
companyName: "YOUR_COMPANY",
userId: "user_123",
);
// Access Content API through:
// KinesteXAIFramework.apiService.fetchContent(...)
```
_React Native_
```jsx
// Set up headers for REST API calls
const API_KEY = 'YOUR_API_KEY';
const COMPANY_NAME = 'YOUR_COMPANY';
const BASE_URL = 'https://admin.kinestex.com/api/v1';
const headers = {
'x-api-key': API_KEY,
'x-company-name': COMPANY_NAME,
};
// Use these headers in all fetch requests
```
_HTML / JavaScript_
```html
// Set up headers for REST API calls
const API_KEY = 'YOUR_API_KEY';
const COMPANY_NAME = 'YOUR_COMPANY';
const BASE_URL = 'https://admin.kinestex.com/api/v1';
const headers = {
'x-api-key': API_KEY,
'x-company-name': COMPANY_NAME,
};
// Use these headers in all fetch requests
```
_React (TypeScript)_
```tsx
// Set up headers for REST API calls
const API_KEY = 'YOUR_API_KEY';
const COMPANY_NAME = 'YOUR_COMPANY';
const BASE_URL = 'https://admin.kinestex.com/api/v1';
const headers: HeadersInit = {
'x-api-key': API_KEY,
'x-company-name': COMPANY_NAME,
};
// TypeScript interfaces for API responses
interface WorkoutModel {
id: string;
title: string;
category: string;
calories: number;
total_minutes: number;
body_parts: string[];
dif_level: string;
description: string;
workout_desc_img: string;
sequence: ExerciseModel[];
}
interface ExerciseModel {
id: string;
title: string;
body_parts: string[];
video_url: string;
thumbnail_url: string;
model_id: string;
}
interface PlanModel {
id: string;
title: string;
img_url: string;
category: Record;
levels: Record;
}
```
**Available SDK Methods** — Swift (iOS)
KinesteXAIKit provides convenient methods that handle all the complexity of API calls for you.
_Convenience Methods (Recommended)_
```swift
// Fetch lists with optional filters
func fetchWorkouts(category: String? = nil, bodyParts: [BodyPart]? = nil, limit: Int? = 10, lastDocId: String? = nil, lang: String = "en") async -> Result
func fetchExercises(bodyParts: [BodyPart]? = nil, limit: Int? = 10, lastDocId: String? = nil, lang: String = "en") async -> Result
func fetchPlans(category: String? = nil, limit: Int? = 10, lastDocId: String? = nil, lang: String = "en") async -> Result
// Fetch single items by ID
func fetchWorkout(id: String, lang: String = "en") async -> Result
func fetchExercise(id: String, lang: String = "en") async -> Result
func fetchPlan(id: String, lang: String = "en") async -> Result
```
_Advanced Method (Full Control)_
```swift
// Use fetchContent for advanced filtering or when you need the raw result type
func fetchContent(
contentType: ContentType, // .workout, .plan, .exercise
id: String? = nil,
title: String? = nil,
lang: String = "en",
category: String? = nil,
bodyParts: [BodyPart]? = nil,
lastDocId: String? = nil,
limit: Int? = nil
) async -> APIContentResult
```
**ContentType Enum** — Kotlin (Android)
Use these values to specify what type of content to fetch.
_Available Content Types_
```kotlin
enum class ContentType {
WORKOUT, // Fetch workouts
PLAN, // Fetch workout plans
EXERCISE // Fetch exercises
}
```
**ContentType Enum** — Flutter
Use these values to specify what type of content to fetch.
_Available Content Types_
```dart
enum ContentType {
workout, // Fetch workouts
plan, // Fetch workout plans
exercise // Fetch exercises
}
```
### Fetching Content Lists
Fetch lists of workouts, plans, or exercises with optional filtering.
**Parameters:**
| Parameter | Type | Description |
|-----------|------|-------------|
| category | String | Filter by category. **Required for plans** in Flutter, Swift, and Kotlin SDKs. Optional for workouts and exercises. |
| bodyParts | [BodyPart] | Filter by targeted body parts (optional) |
| include_kinestex | Bool | Include KinesteX workout library in results (default: true) |
| limit | Int | Number of results to return (default: 10) |
| lang | String | Language code (default: "en") |
| lastDocId | String | For pagination (optional) |
| translation_languages | String | Filter by available translations. Comma-separated language codes (e.g. "es,fr") or repeated query parameters (optional, REST API only) |
**Fetch Workouts**
_Swift (iOS)_
```swift
// Fetch workouts with optional filters
Task {
let result = await kinestex.fetchWorkouts(
category: "Fitness", // or "Rehabilitation"
limit: 10
)
switch result {
case .success(let response):
let workouts = response.workouts
print("Fetched \(workouts.count) workouts")
for workout in workouts {
print("- \(workout.title): \(workout.totalMinutes ?? 0) mins")
}
// Store lastDocId for pagination
let nextPageId = response.lastDocId
case .failure(let error):
print("Error: \(error.localizedDescription)")
}
}
```
_Kotlin (Android)_
```kotlin
// Fetch workouts using coroutines
lifecycleScope.launch {
val result = withContext(Dispatchers.IO) {
KinesteXSDK.api.fetchAPIContentData(
contentType = ContentType.WORKOUT,
category = "Fitness", // or "Rehabilitation"
limit = 10
)
}
when (result) {
is APIContentResult.Workouts -> {
val workouts = result.workouts
Log.d("API", "Fetched ${workouts.size} workouts")
workouts.forEach { workout ->
Log.d("API", "- ${workout.title}")
}
// Store lastDocId for pagination
val nextPageId = result.lastDocId
}
is APIContentResult.Error -> {
Log.e("API", "Error: ${result.message}")
}
else -> {
Log.w("API", "Unexpected result type")
}
}
}
```
_Flutter_
```dart
// Fetch workouts
Future fetchWorkouts() async {
final result = await KinesteXAIFramework.apiService.fetchContent(
contentType: ContentType.workout,
category: "Fitness", // or "Rehabilitation"
limit: 10,
);
switch (result) {
case WorkoutsResult(:final response):
final workouts = response.workouts;
print('Fetched ${workouts.length} workouts');
for (final workout in workouts) {
print('- ${workout.title}');
}
// Store lastDocId for pagination
final nextPageId = response.lastDocId;
case ErrorResult(:final message):
print('Error: $message');
default:
print('Unexpected result type');
}
}
```
_React Native_
```jsx
// Fetch workouts using fetch API
const fetchWorkouts = async (
category?: string,
limit: number = 10
): Promise => {
const params = new URLSearchParams({
limit: String(limit),
});
if (category) {
params.append('category', category);
}
const response = await fetch(
`${BASE_URL}/workouts?${params}`,
{ headers }
);
if (!response.ok) {
throw new Error(`API Error: ${response.status}`);
}
const data = await response.json();
return data.workouts;
};
// Usage
const workouts = await fetchWorkouts('Fitness', 10);
console.log(`Fetched ${workouts.length} workouts`);
```
_HTML / JavaScript_
```html
// Fetch workouts using fetch API
async function fetchWorkouts(category, limit = 10) {
const params = new URLSearchParams({ limit: String(limit) });
if (category) {
params.append('category', category);
}
const response = await fetch(
`${BASE_URL}/workouts?${params}`,
{ headers }
);
if (!response.ok) {
throw new Error(`API Error: ${response.status}`);
}
const data = await response.json();
return data.workouts;
}
// Usage
fetchWorkouts('Fitness', 10)
.then(workouts => console.log(`Fetched ${workouts.length} workouts`))
.catch(error => console.error('Error:', error));
```
_React (TypeScript)_
```tsx
// Fetch workouts with TypeScript
interface WorkoutsResponse {
workouts: WorkoutModel[];
lastDocId?: string;
}
const fetchWorkouts = async (
category?: string,
limit: number = 10
): Promise => {
const params = new URLSearchParams({
limit: String(limit),
});
if (category) {
params.append('category', category);
}
const response = await fetch(
`${BASE_URL}/workouts?${params}`,
{ headers }
);
if (!response.ok) {
throw new Error(`API Error: ${response.status}`);
}
return response.json();
};
// Usage
const { workouts, lastDocId } = await fetchWorkouts('Fitness', 10);
console.log(`Fetched ${workouts.length} workouts`);
```
**Fetch Plans**
_Swift (iOS)_
```swift
// Fetch workout plans
Task {
let result = await kinestex.fetchPlans(
category: "Strength", // Rehabilitation, Weight Management, Cardio, Strength
limit: 5
)
switch result {
case .success(let response):
let plans = response.plans
print("Fetched \(plans.count) plans")
case .failure(let error):
print("Error: \(error.localizedDescription)")
}
}
```
_Kotlin (Android)_
```kotlin
// Fetch workout plans
// IMPORTANT: Always provide 'category' when fetching plans.
lifecycleScope.launch {
val result = withContext(Dispatchers.IO) {
KinesteXSDK.api.fetchAPIContentData(
contentType = ContentType.PLAN,
category = "Strength", // Required: Rehabilitation, Weight Management, Cardio, Strength
limit = 5
)
}
when (result) {
is APIContentResult.Plans -> {
val plans = result.plans
Log.d("API", "Fetched ${plans.size} plans")
}
is APIContentResult.Error -> {
Log.e("API", "Error: ${result.message}")
}
else -> {}
}
}
```
_Flutter_
```dart
// Fetch workout plans
// IMPORTANT: Always provide 'category' when fetching plans.
// If category is null, the SDK may interpret the response as a single PlanResult
// instead of PlansResult (list).
Future fetchPlans() async {
final result = await KinesteXAIFramework.apiService.fetchContent(
contentType: ContentType.plan,
category: "Strength", // Required: Rehabilitation, Weight Management, Cardio, Strength
limit: 5,
);
switch (result) {
case PlansResult(:final response):
final plans = response.plans;
print('Fetched ${plans.length} plans');
case ErrorResult(:final message):
print('Error: $message');
default:
break;
}
}
```
_React Native_
```jsx
// Fetch workout plans
const fetchPlans = async (
category?: string,
limit: number = 5
): Promise => {
const params = new URLSearchParams({ limit: String(limit) });
if (category) {
params.append('category', category);
}
const response = await fetch(
`${BASE_URL}/plans?${params}`,
{ headers }
);
if (!response.ok) {
throw new Error(`API Error: ${response.status}`);
}
const data = await response.json();
return data.plans;
};
// Usage - Plan categories: Rehabilitation, Weight Management, Cardio, Strength
const plans = await fetchPlans('Strength', 5);
```
_HTML / JavaScript_
```html
// Fetch workout plans
async function fetchPlans(category, limit = 5) {
const params = new URLSearchParams({ limit: String(limit) });
if (category) {
params.append('category', category);
}
const response = await fetch(
`${BASE_URL}/plans?${params}`,
{ headers }
);
if (!response.ok) {
throw new Error(`API Error: ${response.status}`);
}
const data = await response.json();
return data.plans;
}
// Usage - Plan categories: Rehabilitation, Weight Management, Cardio, Strength
fetchPlans('Strength', 5).then(plans => console.log(plans));
```
_React (TypeScript)_
```tsx
// Fetch workout plans
interface PlansResponse {
plans: PlanModel[];
lastDocId?: string;
}
const fetchPlans = async (
category?: string,
limit: number = 5
): Promise => {
const params = new URLSearchParams({ limit: String(limit) });
if (category) {
params.append('category', category);
}
const response = await fetch(
`${BASE_URL}/plans?${params}`,
{ headers }
);
if (!response.ok) {
throw new Error(`API Error: ${response.status}`);
}
return response.json();
};
// Usage - Plan categories: Rehabilitation, Weight Management, Cardio, Strength
const { plans } = await fetchPlans('Strength', 5);
```
**Fetch Exercises**
_Swift (iOS)_
```swift
// Fetch exercises filtered by body parts
Task {
let result = await kinestex.fetchExercises(
bodyParts: [.abs, .glutes],
limit: 10
)
switch result {
case .success(let response):
let exercises = response.exercises
print("Fetched \(exercises.count) exercises")
for exercise in exercises {
print("- \(exercise.title): \(exercise.bodyParts.joined(separator: ", "))")
}
case .failure(let error):
print("Error: \(error.localizedDescription)")
}
}
```
_Kotlin (Android)_
```kotlin
// Fetch exercises filtered by body parts
lifecycleScope.launch {
val result = withContext(Dispatchers.IO) {
KinesteXSDK.api.fetchAPIContentData(
contentType = ContentType.EXERCISE,
bodyParts = listOf(BodyPart.ABS, BodyPart.GLUTES),
limit = 10
)
}
when (result) {
is APIContentResult.Exercises -> {
val exercises = result.exercises
Log.d("API", "Fetched ${exercises.size} exercises")
exercises.forEach { exercise ->
Log.d("API", "- ${exercise.title}")
}
}
is APIContentResult.Error -> {
Log.e("API", "Error: ${result.message}")
}
else -> {}
}
}
```
_Flutter_
```dart
// Fetch exercises filtered by body parts
Future fetchExercises() async {
final result = await KinesteXAIFramework.apiService.fetchContent(
contentType: ContentType.exercise,
bodyParts: [BodyPart.abs, BodyPart.glutes],
limit: 10,
);
switch (result) {
case ExercisesResult(:final response):
final exercises = response.exercises;
print('Fetched ${exercises.length} exercises');
for (final exercise in exercises) {
print('- ${exercise.title}');
}
case ErrorResult(:final message):
print('Error: $message');
default:
break;
}
}
```
_React Native_
```jsx
// Fetch exercises filtered by body parts
const fetchExercises = async (
bodyParts?: string[],
limit: number = 10
): Promise => {
const params = new URLSearchParams({ limit: String(limit) });
if (bodyParts && bodyParts.length > 0) {
params.append('body_parts', bodyParts.join(','));
}
const response = await fetch(
`${BASE_URL}/exercises?${params}`,
{ headers }
);
if (!response.ok) {
throw new Error(`API Error: ${response.status}`);
}
const data = await response.json();
return data.exercises;
};
// Usage
const exercises = await fetchExercises(['Abs', 'Glutes'], 10);
```
_HTML / JavaScript_
```html
// Fetch exercises filtered by body parts
async function fetchExercises(bodyParts, limit = 10) {
const params = new URLSearchParams({ limit: String(limit) });
if (bodyParts && bodyParts.length > 0) {
params.append('body_parts', bodyParts.join(','));
}
const response = await fetch(
`${BASE_URL}/exercises?${params}`,
{ headers }
);
if (!response.ok) {
throw new Error(`API Error: ${response.status}`);
}
const data = await response.json();
return data.exercises;
}
// Usage
fetchExercises(['Abs', 'Glutes'], 10).then(exercises => console.log(exercises));
```
_React (TypeScript)_
```tsx
// Fetch exercises filtered by body parts
interface ExercisesResponse {
exercises: ExerciseModel[];
lastDocId?: string;
}
const fetchExercises = async (
bodyParts?: string[],
limit: number = 10
): Promise => {
const params = new URLSearchParams({ limit: String(limit) });
if (bodyParts && bodyParts.length > 0) {
params.append('body_parts', bodyParts.join(','));
}
const response = await fetch(
`${BASE_URL}/exercises?${params}`,
{ headers }
);
if (!response.ok) {
throw new Error(`API Error: ${response.status}`);
}
return response.json();
};
// Usage
const { exercises } = await fetchExercises(['Abs', 'Glutes'], 10);
```
### Fetching Single Items
Fetch a specific workout, plan, or exercise by ID or title.
**By ID:** Use the unique document ID for exact match
**By Title:** Use the content title (case-insensitive, returns first match)
**Fetch by ID**
_Swift (iOS)_
```swift
// Fetch a specific workout by ID
Task {
let result = await kinestex.fetchWorkout(id: "9zE1kzOzpU5d5dAJrPOY")
switch result {
case .success(let workout):
print("Workout: \(workout.title)")
print("Duration: \(workout.totalMinutes ?? 0) minutes")
print("Calories: \(workout.totalCalories ?? 0)")
print("Exercises: \(workout.sequence.count)")
case .failure(let error):
print("Error: \(error.localizedDescription)")
}
}
// Fetch a specific exercise by ID
Task {
let result = await kinestex.fetchExercise(id: "jz73VFlUyZ9nyd64OjRb")
switch result {
case .success(let exercise):
print("Exercise: \(exercise.title)")
print("Model ID: \(exercise.modelId)")
case .failure(let error):
print("Error: \(error.localizedDescription)")
}
}
// Fetch a specific plan by ID
Task {
let result = await kinestex.fetchPlan(id: "22B3qRU2r75hVXHgGiGx")
switch result {
case .success(let plan):
print("Plan: \(plan.title)")
case .failure(let error):
print("Error: \(error.localizedDescription)")
}
}
```
_Kotlin (Android)_
```kotlin
// Fetch a specific workout by ID
lifecycleScope.launch {
val result = withContext(Dispatchers.IO) {
KinesteXSDK.api.fetchAPIContentData(
contentType = ContentType.WORKOUT,
id = "9zE1kzOzpU5d5dAJrPOY"
)
}
when (result) {
is APIContentResult.Workout -> {
val workout = result.workout
Log.d("API", "Workout: ${workout.title}")
Log.d("API", "Duration: ${workout.totalMinutes} minutes")
}
is APIContentResult.Error -> {
Log.e("API", "Error: ${result.message}")
}
else -> {}
}
}
// Fetch a specific exercise by ID
lifecycleScope.launch {
val result = withContext(Dispatchers.IO) {
KinesteXSDK.api.fetchAPIContentData(
contentType = ContentType.EXERCISE,
id = "jz73VFlUyZ9nyd64OjRb"
)
}
when (result) {
is APIContentResult.Exercise -> {
val exercise = result.exercise
Log.d("API", "Exercise: ${exercise.title}")
}
is APIContentResult.Error -> {
Log.e("API", "Error: ${result.message}")
}
else -> {}
}
}
```
_Flutter_
```dart
// Fetch a specific workout by ID
Future fetchWorkoutById() async {
final result = await KinesteXAIFramework.apiService.fetchContent(
contentType: ContentType.workout,
id: "9zE1kzOzpU5d5dAJrPOY",
);
switch (result) {
case WorkoutResult(:final workout):
print('Workout: ${workout.title}');
print('Duration: ${workout.totalMinutes} minutes');
print('Exercises: ${workout.sequence.length}');
case ErrorResult(:final message):
print('Error: $message');
default:
break;
}
}
// Fetch a specific exercise by ID
Future fetchExerciseById() async {
final result = await KinesteXAIFramework.apiService.fetchContent(
contentType: ContentType.exercise,
id: "jz73VFlUyZ9nyd64OjRb",
);
switch (result) {
case ExerciseResult(:final exercise):
print('Exercise: ${exercise.title}');
print('Model ID: ${exercise.modelId}');
case ErrorResult(:final message):
print('Error: $message');
default:
break;
}
}
```
_React Native_
```jsx
// Fetch a specific workout by ID
const fetchWorkoutById = async (id: string): Promise => {
const response = await fetch(
`${BASE_URL}/workouts/${id}`,
{ headers }
);
if (!response.ok) {
throw new Error(`API Error: ${response.status}`);
}
return response.json();
};
// Fetch a specific exercise by ID
const fetchExerciseById = async (id: string): Promise => {
const response = await fetch(
`${BASE_URL}/exercises/${id}`,
{ headers }
);
if (!response.ok) {
throw new Error(`API Error: ${response.status}`);
}
return response.json();
};
// Fetch a specific plan by ID
const fetchPlanById = async (id: string): Promise => {
const response = await fetch(
`${BASE_URL}/plans/${id}`,
{ headers }
);
if (!response.ok) {
throw new Error(`API Error: ${response.status}`);
}
return response.json();
};
// Usage
const workout = await fetchWorkoutById('9zE1kzOzpU5d5dAJrPOY');
const exercise = await fetchExerciseById('jz73VFlUyZ9nyd64OjRb');
const plan = await fetchPlanById('22B3qRU2r75hVXHgGiGx');
```
_HTML / JavaScript_
```html
// Fetch a specific workout by ID
async function fetchWorkoutById(id) {
const response = await fetch(
`${BASE_URL}/workouts/${id}`,
{ headers }
);
if (!response.ok) {
throw new Error(`API Error: ${response.status}`);
}
return response.json();
}
// Fetch a specific exercise by ID
async function fetchExerciseById(id) {
const response = await fetch(
`${BASE_URL}/exercises/${id}`,
{ headers }
);
if (!response.ok) {
throw new Error(`API Error: ${response.status}`);
}
return response.json();
}
// Fetch a specific plan by ID
async function fetchPlanById(id) {
const response = await fetch(
`${BASE_URL}/plans/${id}`,
{ headers }
);
if (!response.ok) {
throw new Error(`API Error: ${response.status}`);
}
return response.json();
}
// Usage
fetchWorkoutById('9zE1kzOzpU5d5dAJrPOY').then(workout => console.log(workout));
```
_React (TypeScript)_
```tsx
// Fetch a specific workout by ID
const fetchWorkoutById = async (id: string): Promise => {
const response = await fetch(
`${BASE_URL}/workouts/${id}`,
{ headers }
);
if (!response.ok) {
throw new Error(`API Error: ${response.status}`);
}
return response.json();
};
// Fetch a specific exercise by ID
const fetchExerciseById = async (id: string): Promise => {
const response = await fetch(
`${BASE_URL}/exercises/${id}`,
{ headers }
);
if (!response.ok) {
throw new Error(`API Error: ${response.status}`);
}
return response.json();
};
// Fetch a specific plan by ID
const fetchPlanById = async (id: string): Promise => {
const response = await fetch(
`${BASE_URL}/plans/${id}`,
{ headers }
);
if (!response.ok) {
throw new Error(`API Error: ${response.status}`);
}
return response.json();
};
// Usage
const workout = await fetchWorkoutById('9zE1kzOzpU5d5dAJrPOY');
console.log(`Workout: ${workout.title}`);
```
**Fetch by Title**
_Swift (iOS)_
```swift
// Fetch content by title (returns first match)
Task {
let result = await kinestex.fetchContent(
contentType: .workout,
title: "Fitness Lite"
)
switch result {
case .workout(let workout):
print("Found workout: \(workout.title)")
case .error(let message):
print("Error: \(message)")
default:
print("Unexpected result type")
}
}
```
_Kotlin (Android)_
```kotlin
// Fetch content by title (returns first match)
lifecycleScope.launch {
val result = withContext(Dispatchers.IO) {
KinesteXSDK.api.fetchAPIContentData(
contentType = ContentType.WORKOUT,
title = "Fitness Lite"
)
}
when (result) {
is APIContentResult.Workout -> {
Log.d("API", "Found workout: ${result.workout.title}")
}
is APIContentResult.Error -> {
Log.e("API", "Error: ${result.message}")
}
else -> {}
}
}
```
_Flutter_
```dart
// Fetch content by title (returns first match)
Future fetchByTitle() async {
final result = await KinesteXAIFramework.apiService.fetchContent(
contentType: ContentType.workout,
title: "Fitness Lite",
);
switch (result) {
case WorkoutResult(:final workout):
print('Found workout: ${workout.title}');
case ErrorResult(:final message):
print('Error: $message');
default:
break;
}
}
```
_React Native_
```jsx
// Fetch content by title (returns first match)
const fetchWorkoutByTitle = async (title: string): Promise => {
const response = await fetch(
`${BASE_URL}/workouts/${encodeURIComponent(title)}`,
{ headers }
);
if (!response.ok) {
throw new Error(`API Error: ${response.status}`);
}
return response.json();
};
// Usage
const workout = await fetchWorkoutByTitle('Fitness Lite');
```
_HTML / JavaScript_
```html
// Fetch content by title (returns first match)
async function fetchWorkoutByTitle(title) {
const response = await fetch(
`${BASE_URL}/workouts/${encodeURIComponent(title)}`,
{ headers }
);
if (!response.ok) {
throw new Error(`API Error: ${response.status}`);
}
return response.json();
}
// Usage
fetchWorkoutByTitle('Fitness Lite').then(workout => console.log(workout));
```
_React (TypeScript)_
```tsx
// Fetch content by title (returns first match)
const fetchWorkoutByTitle = async (title: string): Promise => {
const response = await fetch(
`${BASE_URL}/workouts/${encodeURIComponent(title)}`,
{ headers }
);
if (!response.ok) {
throw new Error(`API Error: ${response.status}`);
}
return response.json();
};
// Usage
const workout = await fetchWorkoutByTitle('Fitness Lite');
```
### Filtering & Parameters
Filter content by category and body parts for targeted results.
**Workout Categories:** Fitness, Rehabilitation
**Plan Categories:** Rehabilitation, Weight Management, Cardio, Strength
> **Important (Flutter, Swift & Kotlin SDKs):** When fetching plans, always provide the `category` parameter.
**Include KinesteX Library:**
| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| include_kinestex | Bool | true | When set to `true`, results include workouts, plans, and exercises from the KinesteX workout library. Set to `false` to exclude KinesteX library content and only return your own custom content. |
**Filter by Translation Languages (REST API):**
| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| translation_languages | String | — | Filter content by available translations. Pass a comma-separated list of language codes (e.g. `es,fr`) or repeat the parameter for each language (e.g. `translation_languages=es&translation_languages=fr`). Only content with translations in **all** specified languages is returned. Optional — omit to return all content regardless of translations. |
**Body Parts (BodyPart enum):**
| SDK Value (Swift) | SDK Value (Kotlin) | SDK Value (Flutter) | REST API Value |
|-------------------|--------------------|--------------------|----------------|
| .abs | ABS | BodyPart.abs | Abs |
| .biceps | BICEPS | BodyPart.biceps | Biceps |
| .calves | CALVES | BodyPart.calves | Calves |
| .chest | CHEST | BodyPart.chest | Chest |
| .externalOblique | EXTERNAL_OBLIQUE | BodyPart.externalOblique | External Oblique |
| .forearms | FOREARMS | BodyPart.forearms | Forearms |
| .glutes | GLUTES | BodyPart.glutes | Glutes |
| .hamstrings | HAMSTRINGS | BodyPart.hamstrings | Hamstrings |
| .lats | LATS | BodyPart.lats | Lats |
| .lowerBack | LOWER_BACK | BodyPart.lowerBack | Lower Back |
| .neck | NECK | BodyPart.neck | Neck |
| .quads | QUADS | BodyPart.quads | Quads |
| .shoulders | SHOULDERS | BodyPart.shoulders | Shoulders |
| .traps | TRAPS | BodyPart.traps | Traps |
| .triceps | TRICEPS | BodyPart.triceps | Triceps |
| .fullBody | FULL_BODY | BodyPart.fullBody | Full Body |
**Filter by Category and Body Parts**
_Swift (iOS)_
```swift
// Combine category and body parts filters
Task {
let result = await kinestex.fetchContent(
contentType: .workout,
category: "Fitness",
bodyParts: [.abs, .glutes, .quads],
limit: 10
)
switch result {
case .workouts(let response):
let workouts = response.workouts
print("Found \(workouts.count) workouts targeting abs, glutes, and quads")
case .error(let message):
print("Error: \(message)")
default:
break
}
}
```
_Kotlin (Android)_
```kotlin
// Combine category and body parts filters
lifecycleScope.launch {
val result = withContext(Dispatchers.IO) {
KinesteXSDK.api.fetchAPIContentData(
contentType = ContentType.WORKOUT,
category = "Fitness",
bodyParts = listOf(BodyPart.ABS, BodyPart.GLUTES, BodyPart.QUADS),
limit = 10
)
}
when (result) {
is APIContentResult.Workouts -> {
val workouts = result.workouts
Log.d("API", "Found ${workouts.size} workouts")
}
is APIContentResult.Error -> {
Log.e("API", "Error: ${result.message}")
}
else -> {}
}
}
```
_Flutter_
```dart
// Combine category and body parts filters
Future fetchFilteredWorkouts() async {
final result = await KinesteXAIFramework.apiService.fetchContent(
contentType: ContentType.workout,
category: "Fitness",
bodyParts: [BodyPart.abs, BodyPart.glutes, BodyPart.quads],
limit: 10,
);
switch (result) {
case WorkoutsResult(:final response):
print('Found ${response.workouts.length} workouts');
case ErrorResult(:final message):
print('Error: $message');
default:
break;
}
}
```
_React Native_
```jsx
// Combine category and body parts filters
const fetchFilteredWorkouts = async (
category: string,
bodyParts: string[],
limit: number = 10
): Promise => {
const params = new URLSearchParams({
category,
body_parts: bodyParts.join(','),
limit: String(limit),
});
const response = await fetch(
`${BASE_URL}/workouts?${params}`,
{ headers }
);
if (!response.ok) {
throw new Error(`API Error: ${response.status}`);
}
const data = await response.json();
return data.workouts;
};
// Usage
const workouts = await fetchFilteredWorkouts(
'Fitness',
['Abs', 'Glutes', 'Quads'],
10
);
```
_HTML / JavaScript_
```html
// Combine category and body parts filters
async function fetchFilteredWorkouts(category, bodyParts, limit = 10) {
const params = new URLSearchParams({
category,
body_parts: bodyParts.join(','),
limit: String(limit),
});
const response = await fetch(
`${BASE_URL}/workouts?${params}`,
{ headers }
);
if (!response.ok) {
throw new Error(`API Error: ${response.status}`);
}
const data = await response.json();
return data.workouts;
}
// Usage
fetchFilteredWorkouts('Fitness', ['Abs', 'Glutes', 'Quads'], 10)
.then(workouts => console.log(workouts));
```
_React (TypeScript)_
```tsx
// Combine category and body parts filters
const fetchFilteredWorkouts = async (
category: string,
bodyParts: string[],
limit: number = 10
): Promise => {
const params = new URLSearchParams({
category,
body_parts: bodyParts.join(','),
limit: String(limit),
});
const response = await fetch(
`${BASE_URL}/workouts?${params}`,
{ headers }
);
if (!response.ok) {
throw new Error(`API Error: ${response.status}`);
}
const data = await response.json();
return data.workouts;
};
// Usage
const workouts = await fetchFilteredWorkouts(
'Fitness',
['Abs', 'Glutes', 'Quads'],
10
);
```
**Exclude KinesteX Library Content**
_Swift (iOS)_
```swift
// Fetch only your custom workouts (exclude KinesteX library)
Task {
let result = await kinestex.fetchWorkouts(
category: "Fitness",
includeKinestex: false, // Exclude KinesteX library content
limit: 10
)
switch result {
case .success(let response):
let workouts = response.workouts
print("Fetched \(workouts.count) custom workouts")
case .failure(let error):
print("Error: \(error.localizedDescription)")
}
}
```
_Kotlin (Android)_
```kotlin
// Fetch only your custom workouts (exclude KinesteX library)
lifecycleScope.launch {
val result = withContext(Dispatchers.IO) {
KinesteXSDK.api.fetchAPIContentData(
contentType = ContentType.WORKOUT,
category = "Fitness",
includeKinestex = false, // Exclude KinesteX library content
limit = 10
)
}
when (result) {
is APIContentResult.Workouts -> {
val workouts = result.workouts
Log.d("API", "Fetched ${workouts.size} custom workouts")
}
is APIContentResult.Error -> {
Log.e("API", "Error: ${result.message}")
}
else -> {}
}
}
```
_Flutter_
```dart
// Fetch only your custom workouts (exclude KinesteX library)
Future fetchCustomWorkouts() async {
final result = await KinesteXAIFramework.apiService.fetchContent(
contentType: ContentType.workout,
category: "Fitness",
queryParameters: {'include_kinestex': false}, // Exclude KinesteX library content
limit: 10,
);
switch (result) {
case WorkoutsResult(:final response):
print('Fetched ${response.workouts.length} custom workouts');
case ErrorResult(:final message):
print('Error: $message');
default:
break;
}
}
```
_React Native_
```jsx
// Fetch only your custom workouts (exclude KinesteX library)
const fetchCustomWorkouts = async (
category: string,
limit: number = 10
): Promise => {
const params = new URLSearchParams({
category,
include_kinestex: 'false', // Exclude KinesteX library content
limit: String(limit),
});
const response = await fetch(
`${BASE_URL}/workouts?${params}`,
{ headers }
);
if (!response.ok) {
throw new Error(`API Error: ${response.status}`);
}
const data = await response.json();
return data.workouts;
};
// Usage - returns only your custom content
const customWorkouts = await fetchCustomWorkouts('Fitness', 10);
```
_HTML / JavaScript_
```html
// Fetch only your custom workouts (exclude KinesteX library)
async function fetchCustomWorkouts(category, limit = 10) {
const params = new URLSearchParams({
category,
include_kinestex: 'false', // Exclude KinesteX library content
limit: String(limit),
});
const response = await fetch(
`${BASE_URL}/workouts?${params}`,
{ headers }
);
if (!response.ok) {
throw new Error(`API Error: ${response.status}`);
}
const data = await response.json();
return data.workouts;
}
// Usage - returns only your custom content
fetchCustomWorkouts('Fitness', 10)
.then(workouts => console.log(workouts));
```
_React (TypeScript)_
```tsx
// Fetch only your custom workouts (exclude KinesteX library)
const fetchCustomWorkouts = async (
category: string,
limit: number = 10
): Promise => {
const params = new URLSearchParams({
category,
include_kinestex: 'false', // Exclude KinesteX library content
limit: String(limit),
});
const response = await fetch(
`${BASE_URL}/workouts?${params}`,
{ headers }
);
if (!response.ok) {
throw new Error(`API Error: ${response.status}`);
}
const data = await response.json();
return data.workouts;
};
// Usage - returns only your custom content
const customWorkouts = await fetchCustomWorkouts('Fitness', 10);
```
### Pagination
For large result sets, use pagination with the `lastDocId` parameter to fetch subsequent pages.
**How it works:**
1. **First request:** Omit `lastDocId` to get the first page
2. **Store the ID:** Save the `lastDocId` from the response
3. **Next request:** Pass the saved ID as `lastDocId` to get the next page
4. **Repeat:** Continue until no more results are returned
**Paginated Fetching**
_Swift (iOS)_
```swift
// Fetch all workouts with pagination
func fetchAllWorkouts() async throws -> [WorkoutModel] {
var allWorkouts: [WorkoutModel] = []
var lastDocId: String? = nil
repeat {
let result = await kinestex.fetchWorkouts(
category: "Fitness",
limit: 10,
lastDocId: lastDocId
)
switch result {
case .success(let response):
allWorkouts.append(contentsOf: response.workouts)
lastDocId = response.lastDocId
// If lastDocId is empty or nil, we've reached the end
if lastDocId?.isEmpty ?? true {
lastDocId = nil
}
print("Fetched page with \(response.workouts.count) workouts")
case .failure(let error):
throw error
}
} while lastDocId != nil
print("Total workouts fetched: \(allWorkouts.count)")
return allWorkouts
}
```
_Kotlin (Android)_
```kotlin
// Fetch all workouts with pagination
suspend fun fetchAllWorkouts(): List {
val allWorkouts = mutableListOf()
var lastDocId: String? = null
do {
val result = KinesteXSDK.api.fetchAPIContentData(
contentType = ContentType.WORKOUT,
category = "Fitness",
limit = 10,
lastDocId = lastDocId
)
when (result) {
is APIContentResult.Workouts -> {
allWorkouts.addAll(result.workouts)
lastDocId = result.lastDocId?.takeIf { it.isNotEmpty() }
Log.d("API", "Fetched page with ${result.workouts.size} workouts")
}
is APIContentResult.Error -> {
throw Exception(result.message)
}
else -> {
lastDocId = null
}
}
} while (lastDocId != null)
Log.d("API", "Total workouts fetched: ${allWorkouts.size}")
return allWorkouts
}
```
_Flutter_
```dart
// Fetch all workouts with pagination
Future> fetchAllWorkouts() async {
final allWorkouts = [];
String? lastDocId;
do {
final result = await KinesteXAIFramework.apiService.fetchContent(
contentType: ContentType.workout,
category: "Fitness",
limit: 10,
lastDocId: lastDocId,
);
switch (result) {
case WorkoutsResult(:final response):
allWorkouts.addAll(response.workouts);
lastDocId = response.lastDocId.isNotEmpty ? response.lastDocId : null;
print('Fetched page with ${response.workouts.length} workouts');
case ErrorResult(:final message):
throw Exception(message);
default:
lastDocId = null;
}
} while (lastDocId != null);
print('Total workouts fetched: ${allWorkouts.length}');
return allWorkouts;
}
```
_React Native_
```jsx
// Fetch all workouts with pagination
const fetchAllWorkouts = async (category: string): Promise => {
const allWorkouts: WorkoutModel[] = [];
let lastDocId: string | undefined = undefined;
do {
const params = new URLSearchParams({
category,
limit: '10',
});
if (lastDocId) {
params.append('lastDocId', lastDocId);
}
const response = await fetch(
`${BASE_URL}/workouts?${params}`,
{ headers }
);
if (!response.ok) {
throw new Error(`API Error: ${response.status}`);
}
const data = await response.json();
allWorkouts.push(...data.workouts);
lastDocId = data.lastDocId || undefined;
console.log(`Fetched page with ${data.workouts.length} workouts`);
} while (lastDocId);
console.log(`Total workouts fetched: ${allWorkouts.length}`);
return allWorkouts;
};
```
_HTML / JavaScript_
```html
// Fetch all workouts with pagination
async function fetchAllWorkouts(category) {
const allWorkouts = [];
let lastDocId = null;
do {
const params = new URLSearchParams({
category,
limit: '10',
});
if (lastDocId) {
params.append('lastDocId', lastDocId);
}
const response = await fetch(
`${BASE_URL}/workouts?${params}`,
{ headers }
);
if (!response.ok) {
throw new Error(`API Error: ${response.status}`);
}
const data = await response.json();
allWorkouts.push(...data.workouts);
lastDocId = data.lastDocId || null;
console.log(`Fetched page with ${data.workouts.length} workouts`);
} while (lastDocId);
console.log(`Total workouts fetched: ${allWorkouts.length}`);
return allWorkouts;
}
```
_React (TypeScript)_
```tsx
// Fetch all workouts with pagination
const fetchAllWorkouts = async (category: string): Promise => {
const allWorkouts: WorkoutModel[] = [];
let lastDocId: string | undefined = undefined;
do {
const params = new URLSearchParams({
category,
limit: '10',
});
if (lastDocId) {
params.append('lastDocId', lastDocId);
}
const response = await fetch(
`${BASE_URL}/workouts?${params}`,
{ headers }
);
if (!response.ok) {
throw new Error(`API Error: ${response.status}`);
}
const data: WorkoutsResponse = await response.json();
allWorkouts.push(...data.workouts);
lastDocId = data.lastDocId || undefined;
console.log(`Fetched page with ${data.workouts.length} workouts`);
} while (lastDocId);
console.log(`Total workouts fetched: ${allWorkouts.length}`);
return allWorkouts;
};
```
### Error Handling
Handle API errors gracefully in your application.
**Response Codes:**
| Status | Description |
|--------|-------------|
| 200/201 | Request successful |
| 400 | Validation error (check parameters) |
| 401 | Unauthorized (invalid API key) |
| 404 | Content not found |
| 500 | Internal server error |
**Error Handling Patterns**
_Swift (iOS)_
```swift
// Comprehensive error handling with Swift SDK
Task {
let result = await kinestex.fetchWorkouts(category: "Fitness", limit: 10)
switch result {
case .success(let response):
// Handle successful response
let workouts = response.workouts
print("Success: Fetched \(workouts.count) workouts")
case .failure(let error):
// Handle different error types
if let urlError = error as? URLError {
switch urlError.code {
case .notConnectedToInternet:
print("No internet connection")
case .timedOut:
print("Request timed out")
default:
print("Network error: \(urlError.localizedDescription)")
}
} else {
print("Error: \(error.localizedDescription)")
}
}
}
// Using fetchContent for advanced error handling
Task {
let result = await kinestex.fetchContent(
contentType: .workout,
id: "invalid_id"
)
switch result {
case .workout(let workout):
print("Found: \(workout.title)")
case .error(let message):
// API returned an error message
print("API Error: \(message)")
case .rawData(let data, let errorMessage):
// Parsing failed, but raw data is available
print("Parse error: \(errorMessage ?? "Unknown")")
print("Raw data: \(data)")
default:
print("Unexpected result type")
}
}
```
_Kotlin (Android)_
```kotlin
// Comprehensive error handling with Kotlin SDK
lifecycleScope.launch {
try {
val result = withContext(Dispatchers.IO) {
KinesteXSDK.api.fetchAPIContentData(
contentType = ContentType.WORKOUT,
category = "Fitness",
limit = 10
)
}
when (result) {
is APIContentResult.Workouts -> {
// Handle successful response
val workouts = result.workouts
Log.d("API", "Success: Fetched ${workouts.size} workouts")
}
is APIContentResult.Error -> {
// API returned an error
Log.e("API", "API Error: ${result.message}")
// Show user-friendly message
Toast.makeText(
this@MainActivity,
"Failed to load workouts: ${result.message}",
Toast.LENGTH_LONG
).show()
}
else -> {
Log.w("API", "Unexpected result type")
}
}
} catch (e: Exception) {
// Handle network or other exceptions
Log.e("API", "Exception: ${e.message}")
when (e) {
is java.net.UnknownHostException -> {
Toast.makeText(this@MainActivity, "No internet connection", Toast.LENGTH_SHORT).show()
}
is java.net.SocketTimeoutException -> {
Toast.makeText(this@MainActivity, "Request timed out", Toast.LENGTH_SHORT).show()
}
else -> {
Toast.makeText(this@MainActivity, "Error: ${e.message}", Toast.LENGTH_SHORT).show()
}
}
}
}
```
_Flutter_
```dart
// Comprehensive error handling with Flutter SDK
// fetchContent never throws: network failures and timeouts are caught
// internally and surfaced as ErrorResult (e.g. "Network error: ...").
Future fetchWithErrorHandling() async {
final result = await KinesteXAIFramework.apiService.fetchContent(
contentType: ContentType.workout,
category: "Fitness",
limit: 10,
);
switch (result) {
case WorkoutsResult(:final response):
// Handle successful response
print('Success: Fetched ${response.workouts.length} workouts');
case ErrorResult(:final message):
// API error, network failure, or timeout
print('API Error: $message');
// Show user-friendly message
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Failed to load workouts: $message')),
);
case RawDataResult(:final data, :final errorMessage):
// Parsing failed, but raw data is available
print('Parse error: ${errorMessage ?? "Unknown"}');
print('Raw data keys: ${data.keys}');
default:
print('Unexpected result type');
}
}
```
_React Native_
```jsx
// Comprehensive error handling with fetch API
const fetchWithErrorHandling = async (): Promise => {
try {
const response = await fetch(
`${BASE_URL}/workouts?category=Fitness&limit=10`,
{ headers }
);
if (!response.ok) {
// Handle HTTP errors
switch (response.status) {
case 400:
throw new Error('Invalid request parameters');
case 401:
throw new Error('Invalid API key');
case 404:
throw new Error('Content not found');
case 500:
throw new Error('Server error - please try again later');
default:
throw new Error(`HTTP Error: ${response.status}`);
}
}
const data = await response.json();
// Check for API-level errors
if (data.error) {
throw new Error(data.error);
}
return data.workouts;
} catch (error) {
if (error instanceof TypeError && error.message === 'Network request failed') {
// No internet connection
console.error('No internet connection');
throw new Error('Please check your internet connection');
}
// Re-throw the error
throw error;
}
};
// Usage with error handling
try {
const workouts = await fetchWithErrorHandling();
console.log(`Fetched ${workouts.length} workouts`);
} catch (error) {
Alert.alert('Error', error.message);
}
```
_HTML / JavaScript_
```html
// Comprehensive error handling with fetch API
async function fetchWithErrorHandling() {
try {
const response = await fetch(
`${BASE_URL}/workouts?category=Fitness&limit=10`,
{ headers }
);
if (!response.ok) {
// Handle HTTP errors
switch (response.status) {
case 400:
throw new Error('Invalid request parameters');
case 401:
throw new Error('Invalid API key');
case 404:
throw new Error('Content not found');
case 500:
throw new Error('Server error - please try again later');
default:
throw new Error(`HTTP Error: ${response.status}`);
}
}
const data = await response.json();
// Check for API-level errors
if (data.error) {
throw new Error(data.error);
}
return data.workouts;
} catch (error) {
if (error instanceof TypeError && error.message === 'Failed to fetch') {
// No internet connection or CORS error
console.error('Network error');
throw new Error('Please check your internet connection');
}
// Re-throw the error
throw error;
}
}
// Usage with error handling
fetchWithErrorHandling()
.then(workouts => console.log(`Fetched ${workouts.length} workouts`))
.catch(error => alert(`Error: ${error.message}`));
```
_React (TypeScript)_
```tsx
// Comprehensive error handling with TypeScript
class APIError extends Error {
constructor(
message: string,
public statusCode?: number,
public originalError?: unknown
) {
super(message);
this.name = 'APIError';
}
}
const fetchWithErrorHandling = async (): Promise => {
try {
const response = await fetch(
`${BASE_URL}/workouts?category=Fitness&limit=10`,
{ headers }
);
if (!response.ok) {
// Handle HTTP errors
const errorMessages: Record = {
400: 'Invalid request parameters',
401: 'Invalid API key',
404: 'Content not found',
500: 'Server error - please try again later',
};
throw new APIError(
errorMessages[response.status] || `HTTP Error: ${response.status}`,
response.status
);
}
const data = await response.json();
// Check for API-level errors
if (data.error) {
throw new APIError(data.error);
}
return data.workouts;
} catch (error) {
if (error instanceof APIError) {
throw error;
}
if (error instanceof TypeError) {
throw new APIError('Please check your internet connection', undefined, error);
}
throw new APIError('An unexpected error occurred', undefined, error);
}
};
// Usage with error handling
try {
const workouts = await fetchWithErrorHandling();
console.log(`Fetched ${workouts.length} workouts`);
} catch (error) {
if (error instanceof APIError) {
console.error(`API Error (${error.statusCode}): ${error.message}`);
}
}
```
### Data Models
Reference for the data structures returned by the Content API.
**WorkoutModel:**
| Field | Type | Description |
|-------|------|-------------|
| id | String | Unique identifier |
| title | String | Workout name |
| category | String | Fitness or Rehabilitation |
| calories | Int? | Estimated calories burned |
| totalMinutes | Int? | Total duration in minutes |
| bodyParts | [String] | Targeted body parts |
| difficultyLevel | String? | Difficulty level |
| description | String | Workout description |
| imgURL | String | Workout thumbnail image |
| sequence | [ExerciseModel] | List of exercises |
**ExerciseModel:**
| Field | Type | Description |
|-------|------|-------------|
| id | String | Unique identifier |
| title | String | Exercise name |
| bodyParts | [String] | Targeted body parts |
| videoURL | String | Demo video URL |
| thumbnailURL | String | Thumbnail image URL |
| modelId | String | Motion tracking model ID (use in Camera Component) |
| description | String | Exercise description |
| steps | [String] | Step-by-step instructions |
| commonMistakes | String | Common mistakes to avoid |
| tips | String | Tips for proper form |
**PlanModel:**
| Field | Type | Description |
|-------|------|-------------|
| id | String | Unique identifier |
| title | String | Plan name |
| imgURL | String | Plan thumbnail image |
| category | PlanModelCategory | Category with description and levels |
| levels | [String: PlanLevel] | Dictionary of levels (1, 2, 3, etc.) |
| createdBy | String | Creator identifier |
**PlanLevel:**
| Field | Type | Description |
|-------|------|-------------|
| title | String | Level title |
| description | String | Level description |
| days | [String: PlanDay] | Dictionary of days |
**PlanDay:**
| Field | Type | Description |
|-------|------|-------------|
| title | String | Day title |
| description | String | Day description |
| workouts | [WorkoutSummary]? | List of workouts for this day |
**Working with Models**
_Swift (iOS)_
```swift
// Accessing workout model properties
Task {
let result = await kinestex.fetchWorkout(id: "9zE1kzOzpU5d5dAJrPOY")
switch result {
case .success(let workout):
// Basic properties
print("Title: \(workout.title)")
print("Category: \(workout.category ?? "N/A")")
print("Duration: \(workout.totalMinutes ?? 0) minutes")
print("Calories: \(workout.totalCalories ?? 0)")
print("Difficulty: \(workout.difficultyLevel ?? "N/A")")
// Body parts
print("Targets: \(workout.bodyParts.joined(separator: ", "))")
// Exercise sequence
print("\nExercises (\(workout.sequence.count)):")
for (index, exercise) in workout.sequence.enumerated() {
print("\(index + 1). \(exercise.title)")
print(" Model ID: \(exercise.modelId)") // Use for Camera Component
print(" Reps: \(exercise.workoutReps ?? exercise.averageReps ?? 0)")
}
// Access raw JSON if needed
if let rawJSON = workout.rawJSON {
print("\nRaw JSON available: \(rawJSON.keys.count) keys")
}
case .failure(let error):
print("Error: \(error.localizedDescription)")
}
}
// Working with plan structure
Task {
let result = await kinestex.fetchPlan(id: "22B3qRU2r75hVXHgGiGx")
switch result {
case .success(let plan):
print("Plan: \(plan.title)")
print("Category: \(plan.category.description)")
// Iterate through levels
for (levelKey, level) in plan.levels {
print("\nLevel \(levelKey): \(level.title)")
// Iterate through days
for (dayKey, day) in level.days {
print(" Day \(dayKey): \(day.title)")
// List workouts for this day
if let workouts = day.workouts {
for workout in workouts {
print(" - \(workout.title) (\(workout.totalMinutes) min)")
}
}
}
}
case .failure(let error):
print("Error: \(error.localizedDescription)")
}
}
```
_Kotlin (Android)_
```kotlin
// Accessing workout model properties
lifecycleScope.launch {
val result = withContext(Dispatchers.IO) {
KinesteXSDK.api.fetchAPIContentData(
contentType = ContentType.WORKOUT,
id = "9zE1kzOzpU5d5dAJrPOY"
)
}
when (result) {
is APIContentResult.Workout -> {
val workout = result.workout
// Basic properties
Log.d("API", "Title: ${workout.title}")
Log.d("API", "Category: ${workout.category}")
Log.d("API", "Duration: ${workout.totalMinutes} minutes")
Log.d("API", "Calories: ${workout.calories}")
// Body parts
Log.d("API", "Targets: ${workout.bodyParts.joinToString(", ")}")
// Exercise sequence
Log.d("API", "Exercises (${workout.sequence.size}):")
workout.sequence.forEachIndexed { index, exercise ->
Log.d("API", "${index + 1}. ${exercise.title}")
Log.d("API", " Model ID: ${exercise.modelId}") // Use for Camera Component
}
// Pretty print as JSON
val gson = GsonBuilder().setPrettyPrinting().create()
val prettyJson = gson.toJson(workout)
Log.d("API", "JSON:\n$prettyJson")
}
is APIContentResult.Error -> {
Log.e("API", "Error: ${result.message}")
}
else -> {}
}
}
```
_Flutter_
```dart
// Accessing workout model properties
Future workWithModels() async {
final result = await KinesteXAIFramework.apiService.fetchContent(
contentType: ContentType.workout,
id: "9zE1kzOzpU5d5dAJrPOY",
);
switch (result) {
case WorkoutResult(:final workout):
// Basic properties
print('Title: ${workout.title}');
print('Category: ${workout.category}');
print('Duration: ${workout.totalMinutes} minutes');
print('Calories: ${workout.totalCalories}');
print('Difficulty: ${workout.difficultyLevel}');
// Body parts
print('Targets: ${workout.bodyParts.join(", ")}');
// Exercise sequence
print('\nExercises (${workout.sequence.length}):');
for (var i = 0; i < workout.sequence.length; i++) {
final exercise = workout.sequence[i];
print('${i + 1}. ${exercise.title}');
print(' Model ID: ${exercise.modelId}'); // Use for Camera Component
}
// Access raw JSON if needed
if (workout.rawJSON != null) {
print('\nRaw JSON available: ${workout.rawJSON!.keys.length} keys');
}
case ErrorResult(:final message):
print('Error: $message');
default:
break;
}
}
// Working with plan structure
Future workWithPlan() async {
final result = await KinesteXAIFramework.apiService.fetchContent(
contentType: ContentType.plan,
id: "22B3qRU2r75hVXHgGiGx",
);
switch (result) {
case PlanResult(:final plan):
print('Plan: ${plan.title}');
print('Category: ${plan.category.description}');
// Iterate through levels
plan.levels.forEach((levelKey, level) {
print('\nLevel $levelKey: ${level.title}');
// Iterate through days
level.days.forEach((dayKey, day) {
print(' Day $dayKey: ${day.title}');
// List workouts for this day
day.workouts?.forEach((workout) {
print(' - ${workout.title} (${workout.totalMinutes} min)');
});
});
});
case ErrorResult(:final message):
print('Error: $message');
default:
break;
}
}
```
_React Native_
```jsx
// TypeScript interfaces for Content API models
interface WorkoutModel {
id: string;
title: string;
category: string;
calories: number;
total_minutes: number;
body_parts: string[];
dif_level: string;
description: string;
workout_desc_img: string;
sequence: ExerciseModel[];
}
interface ExerciseModel {
id: string;
title: string;
body_parts: string[];
video_url: string;
male_video_url: string;
thumbnail_url: string;
male_thumbnail_url: string;
model_id: string;
description: string;
steps: string[];
common_mistakes: string;
tips: string;
workout_reps?: number;
workout_countdown?: number;
average_reps?: number;
average_countdown?: number;
rest_duration?: number;
}
interface PlanModel {
id: string;
title: string;
img_url: string;
category: {
description: string;
levels: Record;
};
levels: Record;
created_by: string;
}
interface PlanLevel {
title: string;
description: string;
days: Record;
}
interface PlanDay {
title: string;
description: string;
workouts?: WorkoutSummary[];
}
interface WorkoutSummary {
id: string;
img_url: string;
title: string;
calories?: number;
total_minutes: number;
}
// Working with fetched data
const displayWorkout = (workout: WorkoutModel) => {
console.log(`Title: ${workout.title}`);
console.log(`Duration: ${workout.total_minutes} minutes`);
console.log(`Calories: ${workout.calories}`);
console.log(`Targets: ${workout.body_parts.join(', ')}`);
console.log(`\nExercises (${workout.sequence.length}):`);
workout.sequence.forEach((exercise, index) => {
console.log(`${index + 1}. ${exercise.title}`);
console.log(` Model ID: ${exercise.model_id}`); // Use for Camera Component
});
};
```
_HTML / JavaScript_
```html
// Working with fetched workout data
function displayWorkout(workout) {
console.log(`Title: ${workout.title}`);
console.log(`Duration: ${workout.total_minutes} minutes`);
console.log(`Calories: ${workout.calories}`);
console.log(`Targets: ${workout.body_parts.join(', ')}`);
console.log(`\nExercises (${workout.sequence.length}):`);
workout.sequence.forEach((exercise, index) => {
console.log(`${index + 1}. ${exercise.title}`);
console.log(` Model ID: ${exercise.model_id}`); // Use for Camera Component
});
}
// Working with plan structure
function displayPlan(plan) {
console.log(`Plan: ${plan.title}`);
console.log(`Category: ${plan.category.description}`);
// Iterate through levels
Object.entries(plan.levels).forEach(([levelKey, level]) => {
console.log(`\nLevel ${levelKey}: ${level.title}`);
// Iterate through days
Object.entries(level.days).forEach(([dayKey, day]) => {
console.log(` Day ${dayKey}: ${day.title}`);
// List workouts for this day
if (day.workouts) {
day.workouts.forEach(workout => {
console.log(` - ${workout.title} (${workout.total_minutes} min)`);
});
}
});
});
}
```
_React (TypeScript)_
```tsx
// Full TypeScript interfaces for Content API models
interface WorkoutModel {
id: string;
title: string;
category: string;
calories: number;
total_minutes: number;
body_parts: string[];
dif_level: string;
description: string;
workout_desc_img: string;
sequence: ExerciseModel[];
}
interface ExerciseModel {
id: string;
title: string;
body_parts: string[];
video_url: string;
male_video_url: string;
thumbnail_url: string;
male_thumbnail_url: string;
model_id: string;
description: string;
steps: string[];
common_mistakes: string;
tips: string;
workout_reps?: number;
workout_countdown?: number;
average_reps?: number;
average_countdown?: number;
rest_duration?: number;
}
interface PlanModel {
id: string;
title: string;
img_url: string;
category: PlanCategory;
levels: Record;
created_by: string;
}
interface PlanCategory {
description: string;
levels: Record;
}
interface PlanLevel {
title: string;
description: string;
days: Record;
}
interface PlanDay {
title: string;
description: string;
workouts?: WorkoutSummary[];
}
interface WorkoutSummary {
id: string;
img_url: string;
title: string;
calories?: number;
total_minutes: number;
}
// Helper function to display workout
const displayWorkout = (workout: WorkoutModel): void => {
console.log(`Title: ${workout.title}`);
console.log(`Duration: ${workout.total_minutes} minutes`);
console.log(`Calories: ${workout.calories}`);
console.log(`Targets: ${workout.body_parts.join(', ')}`);
console.log(`\nExercises (${workout.sequence.length}):`);
workout.sequence.forEach((exercise, index) => {
console.log(`${index + 1}. ${exercise.title}`);
console.log(` Model ID: ${exercise.model_id}`); // Use for Camera Component
});
};
```
---
## 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
```
**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 " -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 " -H "Content-Type: application/json" \
-d '{ "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 " -H "Content-Type: application/json" \
-d '{ "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 " -H "Content-Type: application/json" \
-d '{
"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 "
# 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 "
```
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 your own statistics page.
Two read endpoints share the same authentication:
| Endpoint | Returns |
|----------|---------|
| `GET /api/workout-sessions` | Paginated session history for a user |
| `GET /api/workout-sessions/{id}` | One session in full detail, including per-exercise results |
**Headers (both endpoints):**
| 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` |
Optionally pass `Language: es` (or `Accept-Language`) to have `planned_exercises[].title` returned in that language.
**List sessions**
```
GET https://data.kinestex.com/api/workout-sessions
```
**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.
**Get a single session**
```
GET https://data.kinestex.com/api/workout-sessions/{id}
```
Returns one workout session in full detail — including the per-exercise breakdown (`exercises[]`) that the list endpoint doesn't include. Use it to build your own session-detail or statistics page. `{id}` is the numeric session id from the list response, or from the SDK's `workout_session_saved` event (`session_id`).
```bash
curl "https://data.kinestex.com/api/workout-sessions/5124" \
-H "x-api-key: YOUR_API_KEY" \
-H "x-user-id: user-abc-123"
```
**Response** — the session object is returned directly (no wrapper):
```json
{
"id": 5124,
"created_at": "2026-08-04T09:47:31Z",
"updated_at": "2026-08-04T09:47:31Z",
"company_user_id": "user-abc-123",
"company_id": 42,
"integration_option": "workout",
"client_session_id": "9f4b6c2a-1d3e-4c5b-8a7f-2e6d9c0b4a11",
"content_id": "261",
"content_title": "Full Body Strength",
"content_image_url": "https://cdn.kinestex.com/uploads%2Fworkouts%2Ffull-body.webp",
"content_difficulty": "Medium",
"planned_exercises": [
{
"exercise_id": "122",
"title": "Squats",
"thumbnail_url": "https://cdn.kinestex.com/exercises/squat-thumbnail.jpg",
"reps_target": 12,
"time_target_seconds": null,
"exercise_type": "rep",
"language": "en"
},
{
"exercise_id": "87",
"title": "Plank",
"thumbnail_url": "https://cdn.kinestex.com/exercises/plank-thumbnail.jpg",
"reps_target": null,
"time_target_seconds": 60,
"exercise_type": "timer",
"language": "en"
},
{
"exercise_id": "95",
"title": "Push Ups",
"thumbnail_url": "https://cdn.kinestex.com/exercises/push-ups-thumbnail.jpg",
"reps_target": 12,
"time_target_seconds": null,
"exercise_type": "rep",
"language": "en"
}
],
"started_at": "2026-08-04T09:30:00Z",
"completed_at": "2026-08-04T09:47:30Z",
"actual_duration_seconds": 1050,
"target_duration_seconds": 1080,
"completed_reps_count": 22,
"target_reps_count": 24,
"calories_burned": 10.8,
"completion_percentage": 94.44,
"accuracy_score": 88,
"efficiency_score": 84,
"total_mistakes": 6,
"total_exercises": 3,
"actual_hold_time_seconds": 52,
"target_hold_time_seconds": 60,
"readiness": null,
"post_workout_feedback": null,
"has_rep_based_exercises": true,
"has_ai_model": true,
"exercises": [
{
"id": 18211,
"created_at": "2026-08-04T09:47:31Z",
"updated_at": "2026-08-04T09:47:31Z",
"workout_session_id": 5124,
"exercise_index": 0,
"exercise_id": 122,
"reps_done": 12,
"reps_target": 12,
"time_spent_seconds": 45,
"time_target_seconds": null,
"calories": 4.5,
"average_accuracy": 91,
"rep_accuracy": [95, 88, 92, 90, 94, 89, 93, 91, 90, 92, 88, 90],
"mistakes": [{ "mistake": "straighten back", "count": 2 }],
"mistake_count": 2,
"perfect_position_time": 0,
"mistake_time_spent": 0,
"has_ai_model": true,
"exercise_type": "rep"
},
{
"id": 18212,
"created_at": "2026-08-04T09:47:31Z",
"updated_at": "2026-08-04T09:47:31Z",
"workout_session_id": 5124,
"exercise_index": 1,
"exercise_id": 87,
"reps_done": 0,
"reps_target": null,
"time_spent_seconds": 60,
"time_target_seconds": 60,
"calories": 3.2,
"average_accuracy": 87,
"rep_accuracy": null,
"mistakes": [{ "mistake": "hips too low", "count": 1 }],
"mistake_count": 1,
"perfect_position_time": 52,
"mistake_time_spent": 8,
"has_ai_model": true,
"exercise_type": "timer"
},
{
"id": 18213,
"created_at": "2026-08-04T09:47:31Z",
"updated_at": "2026-08-04T09:47:31Z",
"workout_session_id": 5124,
"exercise_index": 2,
"exercise_id": 95,
"reps_done": 10,
"reps_target": 12,
"time_spent_seconds": 40,
"time_target_seconds": null,
"calories": 3.1,
"average_accuracy": 85,
"rep_accuracy": [88, 84, 86, 85, 87, 83, 85, 84, 86, 82],
"mistakes": [{ "mistake": "elbows flaring", "count": 3 }],
"mistake_count": 3,
"perfect_position_time": 0,
"mistake_time_spent": 0,
"has_ai_model": true,
"exercise_type": "rep"
}
]
}
```
**Session fields:**
| Field | Type | Description |
|-------|------|-------------|
| `id` | int | Session id |
| `created_at` / `updated_at` | timestamp | When the record was saved / last updated (ISO 8601) |
| `company_user_id` | string | The user's id — same value as `x-user-id` |
| `company_id` | int | Internal id of your company |
| `integration_option` | string | Which integration surface launched the workout (`"workout"` by default) |
| `client_session_id` | string \| null | SDK-generated idempotency key for the save; `null` on sessions from older SDKs |
| `content_id` / `content_title` / `content_image_url` / `content_difficulty` | string \| null | The workout content that was played |
| `planned_exercises` | object[] \| null | The workout **plan** as launched — per exercise: `exercise_id` (string), `title`, `thumbnail_url`, `reps_target`, `time_target_seconds`, `exercise_type` (`"rep"` \| `"timer"` \| `"none"`), `language` |
| `started_at` / `completed_at` | timestamp \| null | Session start / completion time |
| `actual_duration_seconds` / `target_duration_seconds` | int | Real wall-clock time spent vs. planned duration, in seconds |
| `completed_reps_count` / `target_reps_count` | int | Total reps done vs. planned across rep-based exercises |
| `calories_burned` | float | Actual calories burned |
| `completion_percentage` | float | 0–100, can be fractional |
| `accuracy_score` / `efficiency_score` | int | 0–100 |
| `total_mistakes` | int | Form mistakes across all exercises |
| `total_exercises` | int | Planned exercise count (includes untouched exercises) |
| `actual_hold_time_seconds` / `target_hold_time_seconds` | int | Time held in correct position vs. target, for timer-based exercises |
| `readiness` | object \| null | Free-form pre-workout readiness answers, when collected |
| `post_workout_feedback` | object \| null | Free-form post-workout feedback, when collected |
| `has_rep_based_exercises` / `has_ai_model` | bool | Whether the session contained rep-based exercises / AI-tracked exercises |
| `ai_summary` | string | AI-generated session summary — present only when one has been generated; omitted otherwise |
| `exercises` | object[] | Per-exercise results (see below) |
**Exercise entry fields (`exercises[]`):**
| Field | Type | Description |
|-------|------|-------------|
| `id` | int | Entry id |
| `workout_session_id` | int | Parent session id |
| `exercise_index` | int | 0-based position in the workout |
| `exercise_id` | int \| null | Exercise library id (numeric here; `planned_exercises[].exercise_id` is a string) |
| `reps_done` | int | Reps completed |
| `reps_target` | int \| null | `null` for timer-based exercises |
| `time_spent_seconds` | int | Active time on this exercise |
| `time_target_seconds` | int \| null | `null` for rep-based exercises |
| `calories` | float | Calories burned on this exercise |
| `average_accuracy` | int | 0–100; `0` when AI tracking was off |
| `rep_accuracy` | int[] \| null | Per-rep accuracy scores (rep-based, AI-tracked exercises) |
| `mistakes` | object[] \| null | `[{ "mistake": "straighten back", "count": 2 }]` |
| `mistake_count` | int | Total mistake occurrences |
| `perfect_position_time` / `mistake_time_spent` | int | Seconds in correct / incorrect form (timer-based exercises) |
| `has_ai_model` | bool | AI form tracking was active for this exercise |
| `exercise_type` | string | `"rep"` \| `"timer"` \| `"none"` |
**Key rules:**
- **Rep vs. timer:** for `"rep"` exercises read `reps_done` / `reps_target` and `rep_accuracy`; for `"timer"` exercises read `time_spent_seconds` / `time_target_seconds` and `perfect_position_time`.
- `planned_exercises` is what was **planned**, `exercises` is what **actually happened** — an abandoned session has fewer `exercises[]` entries than `planned_exercises[]`. Match them by order (`exercise_index`).
- Sessions are user-scoped: requesting a session id that belongs to a different user returns `403`.
- The response may include additional internal fields not listed here — ignore unrecognized fields.
- **Errors:** `400` non-numeric `{id}` · `401` invalid `x-api-key` · `403` session belongs to a different user · `404` unknown session id, or `x-user-id` not found for your company.
Alternatively, to open a full session summary in your app without building a page, 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).
---
## Guides & Examples
Practical, end-to-end walkthroughs for common KinesteX integrations — each one combines the SDK views, REST APIs, and your host-app code into a complete working feature.
| Guide | What you build |
|-------|----------------|
| [Completed workouts list](/docs/guides/guide-completed-workouts) | History cards with progress, calories, and duration that open a full session summary |
| [Subscription gating for the AI Trainer](/docs/guides/guide-subscription-gating) | Gate workout generation behind your paywall with a two-message handshake |
| [Workout-recommendation chatbot](/docs/guides/guide-trainer-chatbot) | Your own AI agent on top of semantic exercise search |
| [Trainer chat lifecycle (REST)](/docs/guides/guide-trainer-rest-lifecycle) | Create → refine → confirm a workout through the Trainer Chat API |
### Completed Workouts List
Build a **"completed workouts" list** — cards that show each workout a user has finished, with completion progress, and that open the full session summary when tapped.
KinesteX keeps every session server-side, tied to the `userId` you initialized the SDK with. One API call gets everything the card needs; tapping the card opens a second view:
```
Workout Sessions API → card model → card UI → on tap: createCustomComponentView(route: "session/{id}")
```
**Field mapping — where each part of the card comes from:**
| Card element | Field | Notes |
|--------------|-------|-------|
| Title | `content_title` | |
| Thumbnail image | `content_image_url` | |
| Calories ("125 cal") | `calories_burned` | **Actual** calories burned (double — round it) |
| Duration ("12 min") | `actual_duration_seconds` | **Actual** seconds spent — format it ("12 sec", "15 min", "1 hr 5 min") |
| Difficulty badge | `content_difficulty` | |
| Progress bar + "% complete" | `completion_percentage` | 0–100, can be fractional (e.g. `0.67`) — authoritative, round for display |
| Exercise count | `total_exercises` | Total planned exercises (includes untouched ones) |
| Tap → open session | `id` → route `session/{id}` | Via `createCustomComponentView` |
**Prerequisites:**
1. SDK initialized with your `apiKey`, `companyName`, and `userId`.
2. Sessions exist — a session is only persisted if the workout was launched with `"shouldSendStats": true`.
3. The `x-user-id` you send to the Sessions API must equal the SDK's `userId` — sessions are tied to it.
**Design notes:**
- The card shows what the user **actually did** — a 15-minute workout abandoned after 12 seconds shows "12 sec" and ~1 cal, which is correct for a history view.
- **Don't derive completion from exercise counts** — `total_exercises` includes untouched exercises, so a barely-started workout would otherwise look finished. The API doesn't expose a "completed exercises" integer; if you want a "4 of 7" look, approximate `round(completionPercentage / 100 * totalExercises)` and treat it as visual only.
- Pagination: the response includes `pagination.has_more`; request the next `page` to load more (default limit 20, max 50).
Endpoint reference: [Workout Sessions API](/docs/trainer-api/trainer-api-workout-sessions).
**1. Fetch the completed sessions**
The session history is a plain REST endpoint (no SDK wrapper). Run requests off the main thread on Android.
_Swift (iOS)_
```swift
struct SessionsResponse: Decodable {
let sessions: [WorkoutSession]
}
struct WorkoutSession: Decodable {
let id: Int
let contentTitle: String
let contentImageUrl: String
let contentDifficulty: String
let caloriesBurned: Double // actual calories the user burned
let actualDurationSeconds: Int // actual time spent exercising
let completionPercentage: Double // 0–100, authoritative
let totalExercises: Int
enum CodingKeys: String, CodingKey {
case id
case contentTitle = "content_title"
case contentImageUrl = "content_image_url"
case contentDifficulty = "content_difficulty"
case caloriesBurned = "calories_burned"
case actualDurationSeconds = "actual_duration_seconds"
case completionPercentage = "completion_percentage"
case totalExercises = "total_exercises"
}
}
func fetchSessions(apiKey: String,
userId: String,
page: Int = 1,
limit: Int = 20) async throws -> [WorkoutSession] {
var components = URLComponents(string: "https://data.kinestex.com/api/workout-sessions")!
components.queryItems = [
URLQueryItem(name: "page", value: "\(page)"),
URLQueryItem(name: "limit", value: "\(limit)"),
URLQueryItem(name: "sort", value: "started_at_desc")
]
var request = URLRequest(url: components.url!)
request.setValue(apiKey, forHTTPHeaderField: "x-api-key")
request.setValue(userId, forHTTPHeaderField: "x-user-id")
let (data, response) = try await URLSession.shared.data(for: request)
guard let http = response as? HTTPURLResponse, (200...299).contains(http.statusCode) else {
throw URLError(.badServerResponse)
}
return try JSONDecoder().decode(SessionsResponse.self, from: data).sessions
}
```
_Kotlin (Android)_
```kotlin
// Uses OkHttp + Gson. Run the request off the main thread.
data class SessionsResponse(val sessions: List)
data class WorkoutSession(
val id: Int,
@SerializedName("content_title") val contentTitle: String,
@SerializedName("content_image_url") val contentImageUrl: String,
@SerializedName("content_difficulty") val contentDifficulty: String,
@SerializedName("calories_burned") val caloriesBurned: Double, // actual calories burned
@SerializedName("actual_duration_seconds") val actualDurationSeconds: Int, // actual time spent
@SerializedName("completion_percentage") val completionPercentage: Double, // 0–100
@SerializedName("total_exercises") val totalExercises: Int
)
suspend fun fetchSessions(
apiKey: String,
userId: String,
page: Int = 1,
limit: Int = 20
): List = withContext(Dispatchers.IO) {
val url = "https://data.kinestex.com/api/workout-sessions" +
"?page=$page&limit=$limit&sort=started_at_desc"
val request = Request.Builder()
.url(url)
.addHeader("x-api-key", apiKey)
.addHeader("x-user-id", userId)
.build()
OkHttpClient().newCall(request).execute().use { response ->
if (!response.isSuccessful) throw IOException("HTTP ${response.code}")
Gson().fromJson(response.body!!.string(), SessionsResponse::class.java).sessions
}
}
```
**2. Render the card**
Each session maps 1:1 into a card — no extra network calls, no merging. Duration is in seconds; calories can be fractional.
_Swift (iOS)_
```swift
struct CompletedWorkoutCardView: View {
let session: WorkoutSession
var onTap: () -> Void
private var durationText: String {
let s = session.actualDurationSeconds
switch s {
case ..<60: return "\(s) sec"
case ..<3600: return "\(s / 60) min"
default:
let h = s / 3600, m = (s % 3600) / 60
return m == 0 ? "\(h) hr" : "\(h) hr \(m) min"
}
}
var body: some View {
Button(action: onTap) {
HStack(spacing: 16) {
AsyncImage(url: URL(string: session.contentImageUrl)) {
$0.resizable().aspectRatio(contentMode: .fill)
} placeholder: { Color.gray.opacity(0.15) }
.frame(width: 96, height: 96)
.clipShape(RoundedRectangle(cornerRadius: 16))
VStack(alignment: .leading, spacing: 10) {
Text(session.contentTitle).font(.title3).bold()
HStack(spacing: 16) {
Label("\(Int(session.caloriesBurned.rounded())) cal", systemImage: "flame.fill")
Label(durationText, systemImage: "clock.fill")
Label(session.contentDifficulty, systemImage: "chart.bar.fill")
}
.font(.subheadline)
// Progress is driven by completion_percentage (authoritative)
ProgressView(value: session.completionPercentage, total: 100)
HStack {
Text("\(Int(session.completionPercentage.rounded()))% complete")
Spacer()
Text("\(session.totalExercises) exercises")
}
.font(.caption).foregroundColor(.secondary)
}
}
.padding()
.background(Color(.secondarySystemBackground))
.clipShape(RoundedRectangle(cornerRadius: 20))
}
.buttonStyle(.plain)
}
}
```
_Kotlin (Android)_
```kotlin
// Jetpack Compose; uses Coil (AsyncImage) for the thumbnail.
@Composable
fun CompletedWorkoutCard(session: WorkoutSession, onClick: () -> Unit) {
val durationText = when {
session.actualDurationSeconds < 60 -> "${session.actualDurationSeconds} sec"
session.actualDurationSeconds < 3600 -> "${session.actualDurationSeconds / 60} min"
else -> {
val h = session.actualDurationSeconds / 3600
val m = (session.actualDurationSeconds % 3600) / 60
if (m == 0) "$h hr" else "$h hr $m min"
}
}
Surface(onClick = onClick, shape = RoundedCornerShape(20.dp), modifier = Modifier.fillMaxWidth()) {
Row(Modifier.padding(16.dp), horizontalArrangement = Arrangement.spacedBy(16.dp)) {
AsyncImage(
model = session.contentImageUrl,
contentDescription = session.contentTitle,
contentScale = ContentScale.Crop,
modifier = Modifier.size(96.dp).clip(RoundedCornerShape(16.dp))
)
Column(verticalArrangement = Arrangement.spacedBy(10.dp)) {
Text(session.contentTitle, style = MaterialTheme.typography.titleLarge)
Row(horizontalArrangement = Arrangement.spacedBy(16.dp)) {
Text("🔥 ${session.caloriesBurned.roundToInt()} cal")
Text("⏱ $durationText")
Text("📊 ${session.contentDifficulty}")
}
// Progress is driven by completion_percentage (authoritative)
LinearProgressIndicator(
progress = { (session.completionPercentage / 100f).toFloat() },
modifier = Modifier.fillMaxWidth()
)
Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
Text("${session.completionPercentage.roundToInt()}% complete")
Text("${session.totalExercises} exercises")
}
}
}
}
}
```
**3. Open the session when the card is tapped**
Tapping a card opens its full summary screen via createCustomComponentView using the "session/{id}" route. Only the session id is needed — KinesteX holds the full record server-side. Handle exit_kinestex to dismiss.
_Swift (iOS)_
```swift
struct CompletedWorkoutsView: View {
@State private var sessions: [WorkoutSession] = []
@State private var openSessionId: Int? = nil
@State private var isLoading = true
let kinestex: KinesteXAIKit
let user: UserDetails
let apiKey: String
let userId: String
var body: some View {
ScrollView {
VStack(spacing: 16) {
ForEach(sessions, id: \.id) { session in
CompletedWorkoutCardView(session: session) {
openSessionId = session.id // ← open the session on tap
}
}
}
.padding()
}
.task {
sessions = (try? await fetchSessions(apiKey: apiKey, userId: userId)) ?? []
}
.fullScreenCover(item: Binding(
get: { openSessionId.map { SessionID(value: $0) } },
set: { openSessionId = $0?.value }
)) { wrapped in
kinestex.createCustomComponentView(
route: "session/\(wrapped.value)",
user: user,
style: IStyle(style: "light", themeName: "YOUR_THEME"),
isLoading: $isLoading,
onMessageReceived: { message in
switch message {
case .exit_kinestex(_): openSessionId = nil // dismiss
case .error_occurred(let data): print("Error: \(data)")
default: break
}
}
)
}
}
}
// Lets an Int drive .fullScreenCover(item:)
struct SessionID: Identifiable { let value: Int; var id: Int { value } }
```
_Kotlin (Android)_
```kotlin
@Composable
fun CompletedWorkoutsScreen(apiKey: String, userId: String) {
var sessions by remember { mutableStateOf>(emptyList()) }
var openSessionId by remember { mutableStateOf(null) }
LaunchedEffect(Unit) {
sessions = runCatching { fetchSessions(apiKey, userId) }.getOrDefault(emptyList())
}
LazyColumn(
contentPadding = PaddingValues(16.dp),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
items(sessions) { session ->
CompletedWorkoutCard(session) {
openSessionId = session.id // ← open the session on tap
}
}
}
// Render the KinesteX session view when a card is tapped. Build it exactly
// like your other KinesteX views — the only thing that changes is the
// route string: "session/$id". Handle exit_kinestex to dismiss.
openSessionId?.let { id ->
KinesteXSessionView(
route = "session/$id",
onExit = { openSessionId = null }
)
}
}
```
### Subscription Gating (AI Trainer)
Gate AI Trainer **workout generation** behind your app's subscription while everything else (onboarding, the free fitness assessment, Q&A) stays available. KinesteX never shows a paywall and never processes payments — **your app owns the subscription screen and the purchase flow**. The trainer only enforces the gate and tells you when the user hits it.
**The flow at a glance:**
1. Mount the trainer with `isSubscribed` in `customParams` (from your live entitlements state). Missing/`true` = subscribed; only an explicit `false` gates generation.
2. When a non-subscriber taps **Generate workout**, generation is parked and the SDK emits `open_subscription_flow`. Present your subscription flow **on top of** the trainer view — don't dismiss it; the pending generation is waiting underneath.
3. Every time your flow closes, post exactly **one** `subscription_result` back into the view — `"purchased"` (the parked workout generates immediately, no second tap) or `"dismissed"` (the request is dropped; the trainer resumes and the free assessment stays reachable). Send it from **every** exit path: purchase success, close button, swipe-down, back gesture, purchase failure.
**The event:**
```json
{ "type": "open_subscription_flow", "source": "generate_workout", "date": "06 07 2026 14:52:10" }
```
This event is an **instruction, not a status** — present your subscription flow now. It fires for new users (after onboarding) and returning users (after the readiness check-in) alike, and can also fire when the KinesteX backend rejects a generation mid-session (see backend-managed subscriptions below).
**Edge cases:**
| Scenario | What to do |
|----------|------------|
| App killed during the purchase flow | Pass the fresh `isSubscribed: true` in `customParams` at next mount. The pending generation is not persisted across launches — the user taps Generate again. |
| Purchase fails or is refunded mid-flow | Send `"dismissed"` — the user can retry from the same Generate button. |
| Duplicate messages | Idempotent — the first result wins; repeats are ignored. |
| Status unknown at launch | Omit `isSubscribed` (treated as subscribed). If your entitlements check later resolves to active, post `{ "subscription_result": "purchased" }` — a pending generation resumes, otherwise future generations are simply unlocked. |
**No host app listening?** Standalone/link integrations can pass `subscriptionReturnUrl` in the launch config instead — the trainer redirects the browser there instead of posting `open_subscription_flow`. Your subscription page then relaunches KinesteX with the same `userId` and `isSubscribed: true`. Accepted values: `https://` URLs or app deep links (e.g. `clientapp://subscribe`); script-executing schemes are rejected.
**Backend-managed subscriptions:** for server-side enforcement your backend can store the user's status with KinesteX and it becomes the source of truth, overriding the launch flag — see [Session Auth & Managed Subscriptions](/docs/trainer-api/trainer-api-subscriptions).
**Launch checklist:**
- `isSubscribed` passed in `customParams` on every mount, from your live entitlements state
- `open_subscription_flow` → your subscription screen, overlaid on top of the trainer view
- Every exit path of your flow sends `subscription_result` = `"purchased"` or `"dismissed"`
- Verified end-to-end: blocked generate → overlay → sandbox purchase + `"purchased"` → workout generates without relaunch; closed overlay + `"dismissed"` → trainer resumes
Minimum SDK versions: Swift `KinesteXAIKit` ≥ 1.1.4 (adds the `workoutAction` binding), Kotlin `KinesteX-SDK-Kotlin` ≥ 2.0.6.
**Mount the trainer with subscription status**
_Swift (iOS)_
```swift
// Set this binding to post real-time actions into the trainer.
@State private var trainerAction: [String: Any]? = nil
kit.createTrainerChatView(
user: UserDetails(age: 32, height: 178, weight: 76, gender: .Male, lifestyle: .Active), // or nil
style: IStyle(style: "dark"),
isLoading: $isLoading,
customParams: [
"isSubscribed": SubscriptionManager.shared.isActive
],
workoutAction: $trainerAction, // KinesteXAIKit ≥ 1.1.4
onMessageReceived: handleMessage
)
```
_Kotlin (Android)_
```kotlin
val webView = KinesteXSDK.createTrainerChatView(
context = this,
user = UserDetails(age = 32, height = 178, weight = 76, gender = Gender.MALE, lifestyle = Lifestyle.ACTIVE), // or null
style = IStyle(style = "dark"),
customParams = mapOf(
"isSubscribed" to subscriptionManager.isActive
),
isLoading = isLoading,
onMessageReceived = ::handleWebViewMessage,
permissionHandler = this
)
```
_HTML / JavaScript_
```html
// Include isSubscribed in the postData you send after kinestex_loaded.
const postData = {
userId: "user-123",
company: "YOUR_COMPANY",
key: "YOUR_API_KEY", // or session: "" (recommended)
integration: "AI_TRAINER_CHAT",
isSubscribed: subscriptionIsActive, // only an explicit false gates generation
};
iframe.contentWindow.postMessage(JSON.stringify(postData), "https://ai.kinestex.com");
```
**Handle the event and report the outcome**
The trainer waits under your overlay until a subscription_result arrives — send it from every exit path of your flow.
_Swift (iOS)_
```swift
private func handleMessage(_ message: KinestexMessage) {
switch message {
case .exit_kinestex:
showKinesteX = false
case .custom_type(let data):
guard let type = data["type"] as? String else { return }
if type == "open_subscription_flow" {
presentSubscriptionFlow()
}
default:
break
}
}
func presentSubscriptionFlow() {
let paywall = PaywallViewController()
paywall.onPurchaseCompleted = { [weak self] in
self?.trainerAction = ["subscription_result": "purchased"] // generation resumes
paywall.dismiss(animated: true)
}
paywall.onDismissed = { [weak self] in
self?.trainerAction = ["subscription_result": "dismissed"] // trainer resumes
}
present(paywall, animated: true)
}
```
_Kotlin (Android)_
```kotlin
private fun handleWebViewMessage(message: WebViewMessage) {
when (message) {
is WebViewMessage.ExitKinestex -> closeTrainer()
is WebViewMessage.CustomType -> when (message.data["type"] as? String) {
"open_subscription_flow" -> presentSubscriptionFlow()
else -> Unit
}
else -> Unit
}
}
private fun presentSubscriptionFlow() {
PaywallSheet(
onPurchaseCompleted = {
KinesteXWebViewController.getInstance()
.sendAction("subscription_result", "purchased") // generation resumes
},
onDismissed = {
KinesteXWebViewController.getInstance()
.sendAction("subscription_result", "dismissed") // trainer resumes
}
).show(supportFragmentManager, "paywall")
}
```
_HTML / JavaScript_
```html
window.addEventListener("message", (event) => {
if (event.origin !== "https://ai.kinestex.com") return;
const message = JSON.parse(event.data);
if (message.type === "open_subscription_flow") {
openPaywall({
onPurchased: () =>
iframe.contentWindow.postMessage(
JSON.stringify({ subscription_result: "purchased" }),
"https://ai.kinestex.com"
),
onDismissed: () =>
iframe.contentWindow.postMessage(
JSON.stringify({ subscription_result: "dismissed" }),
"https://ai.kinestex.com"
),
});
}
});
```
### Workout-Recommendation Chatbot
Pair the [Semantic Exercise Search API](/docs/trainer-api/trainer-api-search) with your own LLM to build a conversational fitness assistant with your own generation logic:
1. The user describes their goal or constraint in natural language.
2. Your app calls `/api/exercises/search` with the user's message as the `query`.
3. The returned exercises are passed as context to your LLM.
4. The LLM generates a personalized recommendation using real exercise data.
```
User message
│
▼
/api/exercises/search ◄── passes user message as query
│
│ returns matching exercises (title, description, media URLs, …)
▼
Your LLM ◄── receives exercises as context + user message
│
▼
Workout recommendation with exercise details
```
Use each exercise's `thumbnail_url` and `video_url` to show visual previews of recommended exercises directly in your chat UI alongside the LLM's text response.
If you'd rather have KinesteX handle the generation logic end-to-end (intents, plan state, undo/redo, progression), use the [Trainer Chat API](/docs/trainer-api/trainer-api-chat) instead.
**Minimal chatbot turn (TypeScript)**
KinesteX exercise search + an LLM (Claude shown here — any LLM works) → a grounded workout recommendation.
_React (TypeScript)_
```tsx
const KINESTEX_BASE_URL = "https://data.kinestex.com/api";
const CLAUDE_API_URL = "https://api.anthropic.com/v1/messages";
// --- Step 1: Search exercises using the user's message as the query ---
async function searchExercises(userMessage: string, jwtToken: string) {
const params = new URLSearchParams({
query: userMessage,
limit: "8",
basic_info: "true", // lightweight payload is sufficient for LLM context
});
const response = await fetch(
`${KINESTEX_BASE_URL}/exercises/search?${params}`,
{ headers: { Authorization: `Bearer ${jwtToken}` } }
);
if (!response.ok) {
throw new Error(`Exercise search failed: ${await response.text()}`);
}
const data = await response.json();
return data.exercises as Array<{
id: string;
title: string;
description: string;
thumbnail_url: string;
video_url: string;
}>;
}
// --- Step 2: Ask the LLM to build a recommendation from the results ---
async function getWorkoutRecommendation(
userMessage: string,
exercises: Awaited>,
claudeApiKey: string
): Promise {
const exerciseContext = exercises
.map((ex) => `- ${ex.title}: ${ex.description}`)
.join("\n");
const response = await fetch(CLAUDE_API_URL, {
method: "POST",
headers: {
"x-api-key": claudeApiKey,
"anthropic-version": "2023-06-01",
"content-type": "application/json",
},
body: JSON.stringify({
model: "claude-haiku-4-5-20251001",
max_tokens: 400,
system: `You are a friendly fitness coach.
The user will describe their fitness goal or constraint.
Recommend a short workout using only the exercises provided.
Be concise — 3-5 exercises, 2-3 sentences of explanation.`,
messages: [
{
role: "user",
content: `User request: "${userMessage}"
Available exercises:
${exerciseContext}
Please recommend a workout using these exercises.`,
},
],
}),
});
const result = await response.json();
return result.content[0].text;
}
// --- Step 3: Combine into a single chatbot turn ---
async function chatbotTurn(userMessage: string, jwtToken: string, claudeApiKey: string) {
const exercises = await searchExercises(userMessage, jwtToken);
const recommendation = await getWorkoutRecommendation(userMessage, exercises, claudeApiKey);
return {
recommendation,
exercises, // includes thumbnail_url and video_url for your chat UI
};
}
// --- Usage ---
// const result = await chatbotTurn(
// "I have bad knees and want a gentle 10-minute workout",
// "your_kinestex_jwt",
// "your_claude_api_key"
// );
```
### 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,
jwtToken: string
): Promise {
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;
}
```
---
## API Reference
Reference of KinesteX SDK integration methods, available on all platforms (Swift, Kotlin, React Native, Flutter, HTML/JS, React TypeScript). Each method embeds a KinesteX view in your app — open the guide for full integration steps with code for every platform.
**Integration views:**
| Method | Guide | Description |
|--------|-------|-------------|
| createMainView() | [Complete UX](/docs/integration/main-view) | Full KinesteX experience with personalized workout plans |
| createWorkoutView() | [Workout View](/docs/integration/workout-view) | A single workout |
| createPlanView() | [Plan View](/docs/integration/plan-view) | A specific workout plan |
| createChallengeView() | [Challenge View](/docs/integration/challenge-view) | Exercise challenge mode |
| createLeaderboardView() | [Leaderboard View](/docs/integration/leaderboard-view) | Ready-made challenge leaderboard |
| createExperienceView() | [AI Experiences](/docs/integration/experience-view) | AI-powered games and clinical assessments |
| createPersonalizedPlanView() | [Personalized Plan View](/docs/integration/personalized-plan-view) | AI-generated personalized workout plan |
| createTrainerChatView() | [AI Trainer Chat](/docs/ai-trainer-chat) | Conversational AI coaching session |
| createAdminWorkoutEditor() | [Admin Workout Editor](/docs/integration/admin-workout-editor) | Embedded workout & exercise editor |
| createCustomWorkoutView() | [Custom Workout](/docs/integration/custom-workout) | Custom exercise sequences you define |
| createCameraComponent() | [Camera Component](/docs/integration/camera-component) | Real-time motion recognition for fully custom UI |
**User details** (passed at initialization, see [Configuration](/docs/configuration)):
| Field | Type | Description |
|-------|------|-------------|
| age | number | User's age |
| height | number | Height in cm |
| weight | number | Weight in kg |
| gender | string | 'Male' or 'Female' |
| lifestyle | enum | Sedentary, Active, etc. |
**Related references:**
- [Customization Parameters](/docs/customization-parameters) — every parameter each view accepts
- [Data Points](/docs/data-points) — events the SDK sends back to your app
- [Content API](/docs/content-api) — fetch workouts, plans, and exercises programmatically
- [AI Trainer API](/docs/trainer-api) — REST access to the conversational trainer, semantic exercise search, workout-session history, and managed subscriptions
- [Guides & Examples](/docs/guides) — end-to-end walkthroughs (completed-workouts list, subscription gating, chatbot on semantic search)
---
## Docs for AI Agents
Everything in this documentation is machine-readable. If you're an AI agent — or you're pointing one at these docs to implement KinesteX — use the surfaces below instead of scraping HTML.
**Start here:** fetch [llms.txt](https://kinestex.com/llms.txt) for an index of every page with descriptions, or [llms-full.txt](https://kinestex.com/llms-full.txt) for the entire documentation as a single Markdown file.
### Read any page as Markdown
Append `.md` to any documentation URL to get the page as clean Markdown with code samples for **all platforms**:
```text
https://kinestex.com/docs/installation.md
https://kinestex.com/docs/data-points/workout-events.md
```
The response is `text/markdown` and always in sync with the page.
### Connect over MCP
The docs are exposed as a [Model Context Protocol](https://modelcontextprotocol.io) server with search and read tools at `https://kinestex.com/mcp` (Streamable HTTP transport).
```bash
# Claude Code
claude mcp add --transport http kinestex-docs https://kinestex.com/mcp
```
```json
// Cursor and other MCP clients (.mcp.json / mcp.json)
{
"mcpServers": {
"kinestex-docs": { "url": "https://kinestex.com/mcp" }
}
}
```
**Available tools:**
| Tool | Description |
|------|-------------|
| search_docs | Search the documentation, returns matching pages with excerpts |
| read_doc | Read a full page as Markdown by its path (e.g. `installation` or `data-points/workout-events`) |
| list_docs | List every documentation page with its path, title, and description |
### Structured JSON API
`GET /api/docs` returns the entire documentation as structured JSON. Filter with `?sections=` (comma-separated section IDs; nested paths supported):
```text
https://kinestex.com/api/docs?sections=installation,data-points/workout-events
```
Rate limit: 50 requests per minute per IP (see `X-RateLimit-Remaining` header).
### Recommended flow for implementing KinesteX
1. Fetch [llms.txt](https://kinestex.com/llms.txt) and identify the platform you're building for (Swift, Kotlin, React Native, Flutter, HTML & JS, or React TS).
2. Read `/docs/installation.md`, `/docs/configuration.md`, and the integration option you're implementing — every code block covers all six platforms.
3. Read `/docs/data-points/receiving-data.md` and the event pages under `/docs/data-points/` to handle SDK events.
4. Check `/docs/customization-parameters/` pages for the exact parameter names your integration needs.
**Note:** an API key is required to run the SDK. A human on your team can request one via the [contact form](https://kinestex.com/#contact-form).
**Product changelog** is machine-readable too: [updates.kinestex.com/api/changelogs](https://updates.kinestex.com/api/changelogs) returns all release notes as JSON.
---
## Support
**Contact:**
• support@kinestex.com — Technical support
• hello@kinestex.com — Sales & demos
**Repositories:**
• [Swift/iOS](https://github.com/KinesteX/KinesteX-Swift-Demo)
• [Kotlin/Android](https://github.com/KinesteX/KinesteX-SDK-Kotlin)
• [React Native](https://github.com/KinesteX/KinesteX-SDK-ReactNative)
• [Flutter](https://github.com/KinesteX/KinesteX-SDK-Flutter)
• [HTML/JS](https://github.com/KinesteX/KinesteX-SDK-HTML-JS)
---
## AI Trainer Chat
The **AI Personal Trainer** is a guided chat experience that walks the user through a personalized session: profile setup (with an optional camera-based fitness assessment) → readiness check-in → workout → post-workout check-in → scheduling the next session.
Your app only needs to mount the trainer view, optionally pre-fill user data, and listen for the events below — most importantly `trainer_schedule_next_workout`, which fires when the user picks a date/time for their next session and is the trigger for your reminder logic. Optionally, you can also gate workout generation behind your subscription flow and keep the fitness profile in sync with your app.
Prefer to build your own chat UI? The same intelligence is available over REST — see the [AI Trainer API](/docs/trainer-api).
### Requirements
Camera permission, an internet connection, and a recent SDK that supports the Trainer Chat integration:
| Platform | Minimum SDK |
| --- | --- |
| Swift (iOS) | `KinesteXAIKit` ≥ **1.1.3** — iOS 13+, Swift 5.5+, SwiftUI. ≥ **1.1.4** for subscription gating (adds the `workoutAction` binding) |
| Kotlin (Android) | `KinesteX-SDK-Kotlin` ≥ **2.0.6** |
| React Native | `kinestex-sdk-react-native` ≥ **1.2.9** |
| Flutter | `kinestex_sdk_flutter` ≥ **1.4.7** |
| React TS (Web) | `kinestex-sdk-react-ts` ≥ **0.0.3** |
| HTML & JS | iframe at `https://ai.kinestex.com/trainer` (HTTPS host page required) |
If you haven't installed the SDK yet, follow the [installation guide](/docs/installation) first — only trainer-specific concerns are covered below.
### Launching the Trainer
Mount the trainer with the **AI_TRAINER_CHAT** integration option (or, on Swift / Kotlin / Flutter, call `createTrainerChatView`). The trainer flow is rendered entirely inside the SDK.
_Swift (iOS)_
```swift
import SwiftUI
import KinesteXAIKit
struct TrainerScreen: View {
@State private var isLoading = false
@State private var showKinesteX = true
// KinesteXAIKit is configured per-instance — no global initialize call.
private let kit = KinesteXAIKit(
apiKey: "",
companyName: "",
userId: ""
)
var body: some View {
if showKinesteX {
kit.createTrainerChatView(
user: nil, // or your UserDetails — see below
style: IStyle(style: "dark"),
isLoading: $isLoading,
onMessageReceived: handleMessage
)
}
}
private func handleMessage(_ message: KinestexMessage) { /* see below */ }
}
```
_Kotlin (Android)_
```kotlin
import com.kinestex.kinestexsdkkotlin.KinesteXSDK
import com.kinestex.kinestexsdkkotlin.PermissionHandler
import com.kinestex.kinestexsdkkotlin.models.IStyle
import com.kinestex.kinestexsdkkotlin.models.WebViewMessage
import kotlinx.coroutines.flow.MutableStateFlow
// 1. Once at app start (Application.onCreate):
class MyApplication : Application() {
override fun onCreate() {
super.onCreate()
KinesteXSDK.initialize(
context = this,
apiKey = "",
companyName = "",
userId = ""
)
}
}
// 2. In your Activity / Fragment (must implement PermissionHandler):
private val isLoading = MutableStateFlow(false)
private fun mountTrainer() {
val webView = KinesteXSDK.createTrainerChatView(
context = this,
style = IStyle(style = "dark"),
isLoading = isLoading,
onMessageReceived = ::handleWebViewMessage,
permissionHandler = this
)
container.addView(webView) // attach to your view hierarchy
}
```
_React Native_
```jsx
import KinestexSDK from "kinestex-sdk-react-native";
import { IPostData, IntegrationOption } from "kinestex-sdk-react-native/src/types";
const postData: IPostData = {
key: "",
company: "",
userId: "",
style: { style: "dark" },
};
```
_Flutter_
```dart
// Once at app start (e.g. in main()):
await KinesteXAIFramework.initialize(
apiKey: '',
companyName: '',
userId: '',
);
// Then in your widget tree:
final showKinesteX = ValueNotifier(true);
final isLoading = ValueNotifier(false);
KinesteXAIFramework.createTrainerChatView(
style: IStyle(style: 'dark'),
isShowKinestex: showKinesteX,
isLoading: isLoading,
onMessageReceived: handleWebViewMessage,
);
```
_HTML / JavaScript_
```html
```
_React (TypeScript)_
```tsx
import KinestexSDK, { IntegrationOption } from "kinestex-sdk-react-ts";
export function TrainerChat() {
return (
// The component fills its parent — give the wrapper an explicit size.
",
company: "",
userId: "",
}}
handleMessage={handleMessage}
/>
);
}
```
### Pre-filling user data
The trainer asks profile questions on first use. You can skip the **basic demographics** by passing them when you mount the SDK — anything you omit, the trainer will simply ask for.
| Field | Unit / Allowed values |
| --- | --- |
| `age` | years |
| `height` | **cm** (UI lets users switch to imperial after) |
| `weight` | **kg** |
| `gender` | `"male"` or `"female"` |
Don't pass empty strings or zero — omit the field instead. On **Swift**, **Kotlin**, and **Flutter** the `UserDetails` initializer requires **all** fields, so either pass the complete value or skip `user:` entirely (`nil` on Swift, omit on Kotlin/Flutter).
**Fitness-profile prefill.** Beyond demographics, you can pre-fill the trainer's fitness profile via `customParams` so returning users skip questions you already have answers for. The field names match the `trainer_profile_updated` event exactly:
| Field | Type | Notes |
| --- | --- | --- |
| `fitness_goals` | string[] | `"weight_loss"`, `"muscle_gain"`, `"strength"`, `"general_fitness"`, `"wellness_flexibility"`, `"cardio_endurance"` |
| `injuries` | object[] | `{ "body_part", "preference": "avoid" \| "include", "severity": "light" \| "moderate" \| "severe" }` |
| `health_conditions` | string[] | e.g. `["Asthma"]` |
| `fitness_level_squats` / `_pushups` / `_cardio` | string | Descriptive levels; the in-app assessment sets the first two automatically |
| `other_preferences` | string | Free text, passed to the AI verbatim |
The trainer merges what you pass into the stored profile (your values win). Only send fields that hold real values — never placeholders, empty strings, or zeros.
_Swift (iOS)_
```swift
// UserDetails on Swift requires ALL five fields — pass the full value when
// you know everything, otherwise pass `user: nil` and let the trainer ask.
let user = UserDetails(
age: 32,
height: 178, // cm
weight: 76, // kg
gender: .Male,
lifestyle: .Active // not used in the trainer UI, but required by the initializer
)
kit.createTrainerChatView(
user: user,
style: IStyle(style: "dark"),
isLoading: $isLoading,
onMessageReceived: handleMessage
)
```
_Kotlin (Android)_
```kotlin
import com.kinestex.kinestexsdkkotlin.models.Gender
import com.kinestex.kinestexsdkkotlin.models.Lifestyle
import com.kinestex.kinestexsdkkotlin.models.UserDetails
// UserDetails on Kotlin requires ALL five fields — pass the full object when
// you know everything, otherwise omit `user:` and let the trainer ask.
val user = UserDetails(
age = 32,
height = 178, // cm
weight = 76, // kg
gender = Gender.MALE,
lifestyle = Lifestyle.ACTIVE // not used in the trainer UI, but required by the constructor
)
KinesteXSDK.createTrainerChatView(
context = this,
user = user,
style = IStyle(style = "dark"),
isLoading = isLoading,
onMessageReceived = ::handleWebViewMessage,
permissionHandler = this
)
```
_React Native_
```jsx
const postData: IPostData = {
key: "",
company: "",
userId: "",
age: 32,
height: 178, // cm
weight: 76, // kg
gender: "male",
style: { style: "dark" },
};
```
_Flutter_
```dart
// UserDetails on Flutter requires ALL fields — pass the full object when
// you know everything, otherwise omit `user:` and let the trainer ask.
final user = UserDetails(
age: 32,
height: 178, // cm
weight: 76, // kg
gender: Gender.Male,
lifestyle: Lifestyle.Active, // not used in the trainer UI, but required by the constructor
);
KinesteXAIFramework.createTrainerChatView(
user: user,
style: IStyle(style: 'dark'),
isShowKinestex: showKinesteX,
isLoading: isLoading,
onMessageReceived: handleWebViewMessage,
);
```
_HTML / JavaScript_
```html
// Add the demographic fields to the same postData you already send.
const postData = {
key: "",
company: "",
userId: "",
integration: "AI_TRAINER_CHAT",
age: 32,
height: 178, // cm
weight: 76, // kg
gender: "male",
style: "dark",
};
```
_React (TypeScript)_
```tsx
",
company: "",
userId: "",
age: 32,
height: 178, // cm
weight: 76, // kg
gender: "male",
}}
handleMessage={handleMessage}
/>
```
### Branding & appearance
Two optional `customParams` rebrand the trainer without any KinesteX involvement:
| Key | Type | Effect |
| --- | --- | --- |
| `aiTrainerName` | string | Renames the AI trainer everywhere it's labelled — the header title and the label above each reply. Shown verbatim in every language. Example: `"My Coach"` |
| `aiTrainerColor` | string | Recolors the trainer's star icon — any CSS color, e.g. `"#7C3AED"` |
The overall theme (colors, fonts) is configured by KinesteX per company — send your KinesteX contact the palette you want, or manage it via your [white-label theme](/docs/customization-parameters).
### Handling messages
The trainer emits the standard SDK events plus its own trainer-specific ones. All of them arrive as JSON with a snake_case `type` — on Swift/Kotlin/Flutter they arrive through the generic `custom_type` / `CustomType` case you already handle:
- `workout_exit_request` — the user exited a workout that was launched from the trainer.
- `trainer_schedule_next_workout` — the user picked a date/time for their next session or assessment (see below).
- `open_subscription_flow` — a non-subscribed user tried to generate a workout (see [Subscription gating](#subscriptions)).
- `trainer_assessment_started` / `trainer_assessment_completed` / `trainer_assessment_skipped` — in-app fitness assessment lifecycle (see [Assessment & profile sync](#profile-sync)).
- `trainer_profile_updated` — the user's fitness profile changed; persist it on your side (see [Assessment & profile sync](#profile-sync)).
Add the new cases to your existing handler.
_Swift (iOS)_
```swift
private func handleMessage(_ message: KinestexMessage) {
switch message {
case .exit_kinestex:
// User exited the trainer screen via the back button.
showKinesteX = false
case .error_occurred(let data):
print("KinesteX error:", data)
// `trainer_schedule_next_workout` is not yet a typed case — it
// arrives as .custom_type, so check the `type` field on the payload.
case .custom_type(let data):
guard let type = data["type"] as? String else { return }
switch type {
case "workout_exit_request":
// User exited a workout that was launched from the trainer.
break
case "trainer_schedule_next_workout":
// payload: { type, scheduledFor: "YYYY-MM-DDTHH:MM" }
if let scheduledFor = data["scheduledFor"] as? String {
scheduleNextWorkoutReminder(scheduledFor: scheduledFor)
}
default:
break
}
default:
break
}
}
```
_Kotlin (Android)_
```kotlin
private fun handleWebViewMessage(message: WebViewMessage) {
when (message) {
is WebViewMessage.ExitKinestex -> {
finish() // or hide your container
}
is WebViewMessage.ErrorOccurred -> {
Log.e("KinesteX", "error: ${message.data}")
}
is WebViewMessage.WorkoutExitRequest -> {
// User exited a workout that was launched from the trainer.
}
// `trainer_schedule_next_workout` is not yet a typed subclass — it
// arrives as CustomType, so check the `type` field on message.data.
is WebViewMessage.CustomType -> {
val type = message.data["type"] as? String
val scheduledFor = message.data["scheduledFor"] as? String
if (type == "trainer_schedule_next_workout" && scheduledFor != null) {
scheduleNextWorkoutReminder(this, scheduledFor)
}
}
else -> { /* other events */ }
}
}
```
_React Native_
```jsx
const handleMessage = (type: string, data: { [key: string]: any }) => {
switch (type) {
case "exit_kinestex":
// User exited the trainer screen via the back button.
break;
case "workout_exit_request":
// User exited a workout that was launched from the trainer.
break;
case "trainer_schedule_next_workout":
// NEW — see "Scheduling the next workout" below.
// data: { type, scheduledFor: "YYYY-MM-DDTHH:MM" }
scheduleNextWorkoutReminder(data.scheduledFor);
break;
case "error_occurred":
console.warn("KinesteX error", data);
break;
}
};
```
_Flutter_
```dart
void handleWebViewMessage(WebViewMessage message) {
if (message is ExitKinestex) {
showKinesteX.value = false;
return;
}
if (message is ErrorOccurred) {
debugPrint('KinesteX error: ${message.data}');
return;
}
// `trainer_schedule_next_workout` is not yet a typed subclass — it
// arrives as CustomType, so check the `type` field on message.data.
if (message is CustomType) {
final type = message.data['type'] as String?;
final scheduledFor = message.data['scheduledFor'] as String?;
if (type == 'trainer_schedule_next_workout' && scheduledFor != null) {
scheduleNextWorkoutReminder(scheduledFor);
}
}
}
```
_HTML / JavaScript_
```html
function handleMessage(type, data) {
switch (type) {
case "exit_kinestex":
// User exited the trainer (e.g. close the modal hosting the iframe).
break;
case "workout_exit_request":
// User exited a workout that was launched from the trainer.
break;
case "trainer_schedule_next_workout":
// NEW — see "Scheduling the next workout" below.
// data: { type, scheduledFor: "YYYY-MM-DDTHH:MM" }
scheduleNextWorkoutReminder(data.scheduledFor);
break;
case "error_occurred":
console.warn("KinesteX error", data);
break;
}
}
```
_React (TypeScript)_
```tsx
const handleMessage = (type: string, payload: Record) => {
switch (type) {
case "exit_kinestex":
setShowTrainer(false);
break;
case "workout_exit_request":
// User exited a workout that was launched from the trainer.
break;
case "trainer_schedule_next_workout":
// NEW — see "Scheduling the next workout" below.
// payload: { type, scheduledFor: "YYYY-MM-DDTHH:MM" }
scheduleNextWorkoutReminder(payload.scheduledFor);
break;
case "error_occurred":
console.warn("KinesteX error", payload);
break;
}
};
```
### Subscription gating
Workout **generation** can be gated behind your app's subscription; browsing, onboarding, the free fitness assessment, and Q&A stay available to everyone. KinesteX never shows a paywall and never processes payments — your app owns the purchase flow.
- Pass `isSubscribed` in `customParams` on every mount. Missing or `true` = subscribed; only an explicit `false` gates generation.
- When a non-subscriber taps **Generate workout**, the SDK parks the request and emits `open_subscription_flow` — overlay your subscription screen on top of the trainer view.
- When your flow closes, post `{ "subscription_result": "purchased" }` or `{ "subscription_result": "dismissed" }` back into the SDK (Swift: the `workoutAction` binding, ≥ 1.1.4; Kotlin: `sendAction`). On `"purchased"` the parked generation resumes automatically — no second tap.
- Standalone/link integrations with no host app can pass `subscriptionReturnUrl` instead — the trainer redirects the browser there rather than posting the event.
Full walkthrough with per-platform code, edge cases, and a launch checklist: [Subscription gating guide](/docs/guides/guide-subscription-gating). To enforce the status server-side (so clients can't bypass it), see [Session Auth & Managed Subscriptions](/docs/trainer-api/trainer-api-subscriptions).
### Assessment & profile sync
**In-app fitness assessment.** During onboarding the trainer offers a guided, camera-based squat and push-up assessment (30 seconds each) instead of self-reporting fitness levels — results set the user's measured fitness level automatically. Users can also tap **Set manually** or schedule the assessment for later. The lifecycle is reported to your app (dates are `DD MM YYYY HH:mm:ss` in device-local time — informational, don't parse them for logic):
```json
{ "type": "trainer_assessment_started", "date": "09 07 2026 14:52:10" }
{ "type": "trainer_assessment_skipped", "reason": "set_manually", "date": "09 07 2026 14:52:10" }
{
"type": "trainer_assessment_completed",
"date": "09 07 2026 14:52:10",
"results": {
"squats": { "reps": 21, "level": "intermediate" },
"pushups": { "reps": 14, "level": "intermediate" }
},
"fitnessLevel": "intermediate"
}
```
`level` and `fitnessLevel` are `"beginner"`, `"intermediate"`, or `"advanced"`.
**Profile sync (KinesteX → your app).** Every profile change — onboarding answers, assessment results, edits the user makes in chat ("my knee hurts", "I'm 70kg now") — fires `trainer_profile_updated` with the **full current profile**:
```json
{
"type": "trainer_profile_updated",
"source": "assessment",
"profile": {
"age": 29,
"height": 172,
"weight": 68,
"gender": "female",
"fitness_goals": ["weight_loss", "cardio_endurance"],
"fitness_level_squats": "I can do 21 squats in 30 seconds",
"fitness_level_pushups": "I can do 14 push ups in 30 seconds",
"fitness_level_cardio": "I can run 1.6km (1 mile) with walking breaks",
"injuries": [
{ "body_part": "lower_back", "preference": "avoid", "severity": "moderate" }
],
"health_conditions": [],
"other_preferences": "Prefers morning workouts",
"updated_at": "2026-07-06T14:52:10Z"
}
}
```
`source` is `"onboarding"`, `"assessment"`, or `"chat"`. Persist the whole `profile` object keyed by user and store `updated_at` — last write wins if both sides edit the same field. Empty arrays/strings are always included so you receive the complete current profile.
**Your app → KinesteX.** No runtime call is needed — pass current values the next time you mount the trainer: demographics via `user:` / `UserDetails`, everything else via `customParams` using the same field names as the event (see [Pre-filling user data](#prefill)).
### Scheduling the next workout
When the user schedules their next workout — or the in-app fitness assessment — for later, the SDK fires:
```json
{
"type": "trainer_schedule_next_workout",
"scheduledFor": "2026-04-29T14:30",
"sessionType": "workout"
}
```
`scheduledFor` is **`YYYY-MM-DDTHH:MM`** with **no timezone** — interpret it as the device's local time (`new Date(scheduledFor)` in JS and `DateTime.parse(scheduledFor)` in Dart both do this correctly). The event is **not** sent if the user skips scheduling.
`sessionType` is `"workout"` or `"assessment"` — treat a missing value as `"workout"`. Use it to word the reminder and to pick a **stable notification identifier per session type** so re-scheduling replaces instead of stacking.
The SDK does **not** schedule the reminder for you — it just tells you when. **The host app delivers the reminder.** Two options:
- **Local notification (recommended for native).** No backend, works offline, fires even if the app is force-quit. Example below.
- **Server-side push.** POST `{ userId, scheduledFor, timezone }` to your backend and queue a push (APNs / FCM / web-push / OneSignal). Use this when the user has multiple devices, you want delivery analytics, or the platform has no reliable local-notification path (e.g. plain web).
Whichever you pick, **use a stable identifier per user** (e.g. `"trainer-next-workout"`) so a re-schedule replaces the previous reminder instead of stacking, **skip past timestamps**, and request notification permission early.
**Local-notification example**
_Swift (iOS)_
```swift
import UserNotifications
private let trainerReminderId = "kinestex.trainer.next_workout" // stable per user
func scheduleNextWorkoutReminder(scheduledFor: String) {
// Parse "YYYY-MM-DDTHH:MM" as local time. Don't use ISO8601DateFormatter —
// it expects seconds and/or a timezone designator.
let formatter = DateFormatter()
formatter.dateFormat = "yyyy-MM-dd'T'HH:mm"
formatter.timeZone = .current
formatter.locale = Locale(identifier: "en_US_POSIX")
guard let date = formatter.date(from: scheduledFor),
date.timeIntervalSinceNow > 5 else { return }
let center = UNUserNotificationCenter.current()
center.requestAuthorization(options: [.alert, .sound, .badge]) { granted, _ in
guard granted else { return }
// Replace the previous reminder so re-schedules don't stack.
center.removePendingNotificationRequests(withIdentifiers: [trainerReminderId])
let content = UNMutableNotificationContent()
content.title = "Time for your workout"
content.body = "Your AI Trainer has your next session ready."
content.sound = .default
let components = Calendar.current.dateComponents(
[.year, .month, .day, .hour, .minute], from: date
)
let trigger = UNCalendarNotificationTrigger(
dateMatching: components, repeats: false
)
center.add(UNNotificationRequest(
identifier: trainerReminderId,
content: content,
trigger: trigger
))
}
}
```
_Kotlin (Android)_
```kotlin
// AndroidManifest.xml:
//
//
//
import android.app.AlarmManager
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.os.Build
import androidx.core.content.getSystemService
import java.time.LocalDateTime
import java.time.ZoneId
const val TRAINER_REMINDER_ID = 1001 // stable per user
fun scheduleNextWorkoutReminder(context: Context, scheduledFor: String) {
// Parse "YYYY-MM-DDTHH:MM" as device-local time, then convert to epoch millis.
val triggerAtMillis = runCatching {
LocalDateTime.parse(scheduledFor)
.atZone(ZoneId.systemDefault())
.toInstant()
.toEpochMilli()
}.getOrNull() ?: return
if (triggerAtMillis - System.currentTimeMillis() < 5_000L) return
val alarmManager = context.getSystemService() ?: return
val canExact = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S)
alarmManager.canScheduleExactAlarms() else true
val intent = Intent(context, TrainerReminderReceiver::class.java)
val pi = PendingIntent.getBroadcast(
context, TRAINER_REMINDER_ID, intent,
// FLAG_UPDATE_CURRENT replaces any previously scheduled reminder with this id.
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
alarmManager.cancel(pi) // explicit cancel for safety
if (canExact) {
alarmManager.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, triggerAtMillis, pi)
} else {
// Fall back to inexact if SCHEDULE_EXACT_ALARM hasn't been granted on Android 12+.
alarmManager.setAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, triggerAtMillis, pi)
}
}
// Receiver that posts the actual notification when the alarm fires.
class TrainerReminderReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
val launch = context.packageManager
.getLaunchIntentForPackage(context.packageName)
?.apply { putExtra("openTrainer", true) }
val contentIntent = PendingIntent.getActivity(
context, 0, launch,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
val notif = NotificationCompat.Builder(context, "trainer_reminders")
.setSmallIcon(R.drawable.ic_notification) // your icon
.setContentTitle("Time for your workout")
.setContentText("Your AI Trainer has your next session ready.")
.setPriority(NotificationCompat.PRIORITY_HIGH)
.setContentIntent(contentIntent)
.setAutoCancel(true)
.build()
context.getSystemService()
?.notify(TRAINER_REMINDER_ID, notif)
}
}
```
_React Native_
```jsx
import * as Notifications from "expo-notifications";
const TRAINER_REMINDER_ID = "trainer-next-workout";
async function scheduleNextWorkoutReminder(scheduledFor: string) {
const date = new Date(scheduledFor); // local time, no timezone in the string
if (Number.isNaN(date.getTime()) || date.getTime() - Date.now() < 5_000) return;
const { granted } = await Notifications.getPermissionsAsync();
if (!granted && !(await Notifications.requestPermissionsAsync()).granted) return;
// Replace the previous reminder so re-schedules don't stack.
await Notifications.cancelScheduledNotificationAsync(TRAINER_REMINDER_ID).catch(() => {});
await Notifications.scheduleNotificationAsync({
identifier: TRAINER_REMINDER_ID,
content: {
title: "Time for your workout",
body: "Your AI Trainer has your next session ready.",
sound: "default",
},
trigger: { type: Notifications.SchedulableTriggerInputTypes.DATE, date },
});
}
```
_Flutter_
```dart
// pubspec.yaml: flutter_local_notifications + timezone
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:timezone/timezone.dart' as tz;
const trainerReminderId = 1001; // stable per user
final notifications = FlutterLocalNotificationsPlugin();
Future scheduleNextWorkoutReminder(String scheduledFor) async {
final date = DateTime.tryParse(scheduledFor); // local time
if (date == null || date.difference(DateTime.now()).inSeconds < 5) return;
// Replace the previous reminder so re-schedules don't stack.
await notifications.cancel(trainerReminderId);
await notifications.zonedSchedule(
trainerReminderId,
'Time for your workout',
'Your AI Trainer has your next session ready.',
tz.TZDateTime.from(date, tz.local),
const NotificationDetails(
android: AndroidNotificationDetails(
'trainer_reminders', 'Trainer reminders',
importance: Importance.high, priority: Priority.high,
),
iOS: DarwinNotificationDetails(),
),
androidScheduleMode: AndroidScheduleMode.exactAllowWhileIdle,
uiLocalNotificationDateInterpretation:
UILocalNotificationDateInterpretation.absoluteTime,
);
}
```
_HTML / JavaScript_
```html
// Browser notifications fire only while the page (or a service worker) is alive.
// For reminders that need to wake the user up later, POST to your backend and
// schedule a real push notification (web-push / FCM) instead.
async function scheduleNextWorkoutReminder(scheduledFor) {
const delay = new Date(scheduledFor).getTime() - Date.now();
if (Number.isNaN(delay) || delay < 5_000) return;
if (Notification.permission !== "granted") {
if ((await Notification.requestPermission()) !== "granted") return;
}
// Replace any previously queued in-page reminder.
clearTimeout(window.__trainerReminderTimer);
window.__trainerReminderTimer = setTimeout(() => {
new Notification("Time for your workout", {
body: "Your AI Trainer has your next session ready.",
tag: "trainer-next-workout",
});
}, delay);
}
```
_React (TypeScript)_
```tsx
// Browser notifications fire only while the page (or a service worker) is alive.
// For reminders that need to wake the user up later, POST to your backend and
// schedule a real push notification (web-push / FCM) instead.
export async function scheduleNextWorkoutReminder(scheduledFor: string) {
const delay = new Date(scheduledFor).getTime() - Date.now();
if (Number.isNaN(delay) || delay < 5_000) return;
if (Notification.permission !== "granted") {
if ((await Notification.requestPermission()) !== "granted") return;
}
const w = window as unknown as { __trainerReminderTimer?: number };
if (w.__trainerReminderTimer) clearTimeout(w.__trainerReminderTimer);
w.__trainerReminderTimer = window.setTimeout(() => {
new Notification("Time for your workout", {
body: "Your AI Trainer has your next session ready.",
tag: "trainer-next-workout",
});
}, delay);
}
```
---