Integrating FittyAI Virtual Trainer

The Full FittyAI workout integration lets you drop a fully‑featured virtual trainer into any React (or React Native WebView) app in minutes. All that’s required is an <iframe> and a handful of event listeners.


1 · Prerequisites

Credential How to obtain
clientId Provided during onboarding
secret Provided during onboarding (used to create a JWT token)

To get workoutId either fetch possible workouts using Workouts API or store enabled for your client workouts in your storage.


2 · Project Setup

yarn create react-app fitty‑demo
cd fitty‑demo
yarn add uuid            # generate sessionId

Create .env at the project root:

REACT_APP_FITTY_ENV=prod        
REACT_APP_FITTY_CLIENT=<clientId>


3 · Acquire JWT for User Authorization

⚠️ Important: Acquiring the JWT must be managed on your backend server. It’s essential because this process involves sending your secret. For security, never store or send secrets from a client device.

Read the instructions to receive your JWT here.

Upon retrieval, the JWT will resemble:

    const userToken = "eyJhbGciOiJIUzI1NiIsInR5cCI...";

4 · Generate session id

    import { v4 as uuidv4 } from "uuid";
    const sessionId = uuidv4();

5 · Embedding the Iframe

Below is the complete minimal component—no extras, no optional query parameters—so the integration looks as simple as it is:

import React, { useEffect, useRef } from "react";

export default function FittyWorkout() {
  const iframeRef = useRef(null);

  // Keep the iframe 100 vh
  useEffect(() => {
    const resize = () => {
      if (iframeRef.current) iframeRef.current.style.height = window.innerHeight + "px";
    };
    window.addEventListener("resize", resize);
    resize();
    return () => window.removeEventListener("resize", resize);
  }, []);

  /**
   * Listen for FittyAI Event Bridge messages
   */
  useEffect(() => {
    const allowed = `https://${process.env.REACT_APP_FITTY_ENV}.fittyai.com`;
    const onMessage = (e) => {
      if (e.origin !== allowed) return;
      let data = typeof e.data === "string" ? JSON.parse(e.data || "{}") : e.data;
      switch (data.type) {
        case "initiationCompleted":
          // you can use this even to hide your custom spinner while iframe is being loaded
          break;
        case "indexUpdated":
          // data.index holds the 0‑based exercise index
          // you can store user's progress in the workout so that next time you load the workout from the exercise
          // that the user ended by adding ?eventIdex={exerciseIndex} where exerciseIndex came from the previous session
          // indexUpdate event
          break;
        case "closeRequested":
          // this event is sent when user presses close button in the workout
          break;
        case "sessionEnd":
          // after workout session is completed use our sessions API endpoint to fetch user's results here (see section 7)
          break;
        default:
          break;
      }
    };
    window.addEventListener("message", onMessage);
    return () => window.removeEventListener("message", onMessage);
  }, []);

  const url = `https://${process.env.REACT_APP_FITTY_ENV}.fittyai.com/web/${process.env.REACT_APP_FITTY_CLIENT}/${process.env.REACT_APP_WORKOUT_ID}/${sessionId}?JWT=${userToken}`;

  return <iframe ref={iframeRef}
                 id="fittyai"
                 src={url}
                 width="100%"
                 height="100%"
                 style=
                 allow="camera" />;
}

That’s it—the workout runs full‑screen, counts reps and talks to the user. Everything else is optional.


6 · Event Bridge

During the session the iframe posts JSON messages to its parent window:

Event Payload When
initiationCompleted { type: "initiationCompleted" } After internal bootstrap
indexUpdated { type: "indexUpdated", index: number } Whenever the active exercise changes
closeRequested { type: "closeRequested" } User hit X and the URL contained suppressFinish=true
sessionEnd { type: "sessionEnd" } Workout finished or host called nativeBridge.close()

Why sessionEnd? Internally the player calls window.parent.postMessage("sessionEnd", "*") when the workout is over. Treat it as the final “all done”.


7 · Fetching the Results

After you receive sessionEnd (or decide to treat closeRequested as the end) call the Get Session endpoint to pull the full workout analytics:

GET /v1/{clientName}/sessions/{sessionId}
Path parameter Description
clientName Your clientId (set in REACT_APP_FITTY_CLIENT)
sessionId The UUID you generated for this workout

Example fetch (browser)

const { REACT_APP_FITTY_CLIENT: clientName, REACT_APP_JWT: jwt } = process.env;

if (data.type === "sessionEnd") {
  fetch(`/v1/${clientName}/sessions/${sessionId}`, {
    headers: { Authorization: `Bearer ${jwt}` }
  })
    .then(r => r.json())
    .then(console.log);
}

Successful response

{
  "kcal": 98,
  "score": 23660,
  "avg_accuracy": 76,
  "exercises": [
    {
      "name": "jumping_jack",
      "reps": 50,
      "nr_mistakes": 15,
      "avg_accuracy": 79,
      "all_mistakes": [
        "stretch your arms out more",
        "make sure to stretch your arms out more"
      ]
    }
  ]
}

See the full schema and error codes in the Get Session docs.


8 · Optional URL Query Parameters

Add these only when you need them—they all work independently.

Parameter Purpose Example
eventIndex Load the workout starting from a specific exercise ?eventIndex=5
suppressFinish Use suppressFinish=true to disable our workout summary screen and show your custom summary screen; emits closeRequested instead ?suppressFinish=true

Simply append one or more to the iframe URL (order doesn’t matter).


9 · Security Checklist

  • Origin check – Always compare event.origin with https://<env>.fittyai.com before trusting data.
  • Permissions Policy – Keep allow="camera" on the iframe.