# 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); } }} /> ) : ( ``` _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); } }} /> ) : ( )}
); }; 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); } }} /> ) : ( ``` _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); } }} /> ) : ( )}
); }; 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); } }} /> ) : ( ``` _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); } }} /> ) : ( )}
); }; 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); } }} /> ) : ( ``` _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); } }} /> ) : ( )}
); }; 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 ( ``` _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 ; } ``` #### 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 ``` _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 ? ( ) : ( )} ); }; 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
``` #### 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 ``` _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); }} />
); } ``` --- ## 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 `