Skip to content

Usage

This page covers common Web SDK integration patterns, including React examples for launching one workout or listing workouts from GraphQL.

Vanilla JavaScript example

Use this pattern in a plain HTML or server-rendered page:

js
workoutkit.init('https://cloud.YourCompany.fizzup.com')

workoutkit.start({
  workoutId: '4RGZ7S46',
  soundpack: 'none',
  onWorkoutQuit: function () {
    console.log('Event received: workout quit')
  },
  onWorkoutSave: function (workoutData) {
    console.log('Event received: workout save', workoutData)
  },
  onWorkoutEvent: function (action, payload) {
    console.log('Event received:', action, payload)
  },
})

ReactJS component example

This component loads the SDK script once, initializes WorkoutKit, starts the workout on button click, and lets you keep token retrieval logic outside the component.

Use accessTokenProvider as the external token resolver name. It maps cleanly to the SDK's internal getAccessToken option while keeping your authentication flow in your own application layer.

tsx
import { useEffect, useState } from 'react'

declare global {
  interface Window {
    workoutkit: {
      init: (baseUrl: string) => void
      start: (options: {
        workoutId: string
        soundpack?: string
        getAccessToken?: () => Promise<string>
        onWorkoutQuit?: () => void
        onWorkoutSave?: (workoutData: unknown) => void
        onWorkoutEvent?: (action: string, payload: unknown) => void
      }) => void
      close: () => void
    }
  }
}

type WorkoutKitPlayerProps = {
  baseUrl: string
  workoutId: string
  soundpack?: 'none' | 'zen' | 'original' | 'new' | 'whistle'
  accessTokenProvider?: () => Promise<string>
  onWorkoutQuit?: () => void
  onWorkoutSave?: (workoutData: unknown) => void
  onWorkoutEvent?: (action: string, payload: unknown) => void
}

const SDK_URL = 'https://cdn.fizzup.com/js/workoutkit-sdk/1.0.0/sdk.js'

export function WorkoutKitPlayer({
  baseUrl,
  workoutId,
  soundpack = 'original',
  accessTokenProvider,
  onWorkoutQuit,
  onWorkoutSave,
  onWorkoutEvent,
}: WorkoutKitPlayerProps) {
  const [isReady, setIsReady] = useState(false)

  useEffect(() => {
    const existingScript = document.querySelector<HTMLScriptElement>(`script[src="${SDK_URL}"]`)

    const initialize = () => {
      window.workoutkit.init(baseUrl)
      setIsReady(true)
    }

    if (existingScript) {
      if (window.workoutkit) {
        initialize()
      } else {
        existingScript.addEventListener('load', initialize, { once: true })
      }

      return
    }

    const script = document.createElement('script')
    script.src = SDK_URL
    script.async = true
    script.onload = initialize
    document.body.appendChild(script)

    return () => {
      script.onload = null
    }
  }, [baseUrl])

  const startWorkout = () => {
    window.workoutkit.start({
      workoutId,
      soundpack,
      getAccessToken: accessTokenProvider,
      onWorkoutQuit,
      onWorkoutSave,
      onWorkoutEvent,
    })
  }

  return (
    <button type="button" onClick={startWorkout} disabled={!isReady}>
      {isReady ? 'Start workout' : 'Loading WorkoutKit...'}
    </button>
  )
}

Example usage in a React page

tsx
import { WorkoutKitPlayer } from './WorkoutKitPlayer'

async function accessTokenProvider() {
  const response = await fetch('/api/workoutkit/token', {
    method: 'POST',
    credentials: 'include',
  })

  if (!response.ok) {
    throw new Error('Unable to retrieve WorkoutKit access token')
  }

  const data: { accessToken?: string } = await response.json()

  if (!data.accessToken) {
    throw new Error('WorkoutKit token response is missing accessToken')
  }

  return data.accessToken
}

export default function WorkoutPage() {
  return (
    <WorkoutKitPlayer
      baseUrl="https://cloud.YourCompany.fizzup.com"
      workoutId="4RGZ7S46"
      soundpack="none"
      accessTokenProvider={accessTokenProvider}
      onWorkoutQuit={() => {
        console.log('Event received: workout quit')
      }}
      onWorkoutSave={(workoutData) => {
        console.log('Event received: workout save', workoutData)
      }}
      onWorkoutEvent={(action, payload) => {
        console.log('Event received:', action, payload)
      }}
    />
  )
}

React workout list with Apollo Client

This example shows a React page that lists WorkoutKit sessions directly from the browser with Apollo Client, fetches the selected session content on click, then starts the selected workout with the Web SDK.

WorkoutKit is designed so client applications can query the WorkoutKit GraphQL backend from the browser. If your integration requires end-user authorization, provide your own access token through getAccessToken; the sample keeps that part intentionally minimal.

The GraphQL query is adapted from Workout.graphql in the iOS sample app.

Files

This sample is split into small files:

  • .env: WorkoutKit environment base URL
  • WorkoutKitGraphQLProvider.tsx: Apollo Client setup and provider
  • queries.ts: WorkoutKit GraphQL query and shared types
  • WorkoutPreviewItem.tsx: one clickable workout card that starts the Web SDK
  • App.tsx: fetches and renders the workout list

1. Environment

In a Vite application, expose the WorkoutKit base URL with a VITE_-prefixed variable:

bash
VITE_WORKOUTKIT_BASE_URL=https://cloud.YourCompany.fizzup.com

The GraphQL endpoint is always the WorkoutKit base URL plus /graphql, so the sample derives it in code instead of duplicating it in the environment.

2. Apollo Client provider

Install Apollo Client in your React application:

bash
npm install @apollo/client graphql

Create an Apollo Client that targets your WorkoutKit GraphQL endpoint.

tsx
import { ApolloClient, HttpLink, InMemoryCache } from '@apollo/client'
import { ApolloProvider } from '@apollo/client/react'
import type { ReactNode } from 'react'

export const WORKOUTKIT_BASE_URL = import.meta.env.VITE_WORKOUTKIT_BASE_URL
const WORKOUTKIT_GRAPHQL_ENDPOINT = `${WORKOUTKIT_BASE_URL}/graphql`

const client = new ApolloClient({
  link: new HttpLink({
    uri: WORKOUTKIT_GRAPHQL_ENDPOINT,
  }),
  cache: new InMemoryCache(),
})

type WorkoutKitGraphQLProviderProps = {
  children: ReactNode
}

export function WorkoutKitGraphQLProvider({ children }: WorkoutKitGraphQLProviderProps) {
  return <ApolloProvider client={client}>{children}</ApolloProvider>
}

If your GraphQL endpoint requires an end-user token, add your own authorization header in this file. The token can come from your existing application auth flow.

3. WorkoutKit query

Keep the list query focused on preview fields. Fetch the full publicWorkoutSession payload only when your launch flow needs it.

ts
import { gql } from '@apollo/client'

export type WorkoutPreview = {
  id: string
  type: string
  duration: number
  name: string
  format?: string | null
  picture?: string | null
}

export type GetSessionsData = {
  publicWorkouts: {
    edges: Array<{
      node: WorkoutPreview
    }>
  }
}

export type GetSessionsVariables = {
  tag: string
  difficulty: 'EASY' | 'NORMAL' | 'HARD'
}

export type GetSessionContentData = {
  publicWorkoutSession: {
    __typename: 'WorkoutBlockSession' | 'WorkoutVideoSession'
    id: string
    name: string
    duration: number
  }
}

export type GetSessionContentVariables = {
  id: string
}

export const GET_SESSIONS = gql`
  query GetSessions($tag: String!, $difficulty: Difficulty!) {
    publicWorkouts(filters: { tag: $tag }, configuration: { difficulty: $difficulty }) {
      edges {
        node {
          ...WorkoutPreviewItem
        }
      }
    }
  }

  fragment WorkoutPreviewItem on WorkoutPreview {
    id: globalId
    type
    duration
    name
    format
    picture
  }
`

export const GET_SESSION_CONTENT = gql`
  query GetSessionContent($id: ID!) {
    publicWorkoutSession(globalId: $id, configuration: { difficulty: EASY }) {
      __typename
      ... on WorkoutBlockSession {
        ...WorkoutBlockSessionItem
      }
      ... on WorkoutVideoSession {
        ...WorkoutVideoSessionItem
      }
    }
  }

  fragment WorkoutBlockSessionItem on WorkoutBlockSession {
    audioSets {
      ...AudioSetItem
    }
    duration
    exercises {
      ...WorkoutExerciseItem
    }
    id: globalId
    sections {
      ...WorkoutSectionItem
    }
    name
    type
  }

  fragment AudioSetItem on AudioSet {
    duration
    audioFile
    id
    text
    textPhonetic
  }

  fragment WorkoutExerciseItem on WorkoutExercise {
    id
    name
    execution
    duration
    cover
  }

  fragment WorkoutSectionItem on WorkoutSection {
    id
    type
    duration
    calories
    executionMode
    name
    optional
    premium
    tasks {
      ... on TaskExercise {
        audioSets { ...AudioSetsItem }
        effortType
        noSeries
        nbSeries
        exerciseId
        side
        nbRepetition
        duration
        asymmetricType
        progressionSegment
        video { ...WorkoutVideoItem }
      }
      ... on TaskRest {
        audioSets { ...AudioSetsItem }
        restTaskType
        restTime
        video { ...WorkoutVideoItem }
      }
    }
  }

  fragment WorkoutVideoItem on WorkoutVideo {
    cover
    textPrimaryColor
    textSecondaryColor
    tintColor
    video
  }

  fragment AudioSetsItem on AudioSetTask {
    audioSetsId
    timestamp
    type
  }

  fragment WorkoutVideoSessionItem on WorkoutVideoSession {
    duration
    id
    name
    reference
    sections {
      ...WorkoutVideoSessionSectionItem
    }
    video {
      ...WorkoutVideoSessionVideoItem
    }
  }

  fragment WorkoutVideoSessionSectionItem on WorkoutVideoSessionSection {
    id
    type
    start
    end
    calories
    optional
    name
    skipLabel
  }

  fragment WorkoutVideoSessionVideoItem on WorkoutVideoSessionVideo {
    cover
    end
    start
    textPrimaryColor
    url
  }
`

4. Workout preview item

This component renders one workout preview. When the user clicks the card, it runs GetSessionContent with network-only fetch behavior, then starts the selected workout with the Web SDK. This mirrors the iOS sample app flow, where GetSessions fills the list and GetSessionContent is fetched right before launch.

tsx
import { useLazyQuery } from '@apollo/client/react'
import { useEffect, useState } from 'react'
import {
  GET_SESSION_CONTENT,
  type GetSessionContentData,
  type GetSessionContentVariables,
  type WorkoutPreview,
} from './queries'
import { WORKOUTKIT_BASE_URL } from './WorkoutKitGraphQLProvider'

declare global {
  interface Window {
    workoutkit: {
      init: (baseUrl: string) => void
      start: (options: {
        workoutId: string
        soundpack?: string
        getAccessToken?: () => Promise<string>
        onWorkoutQuit?: () => void
        onWorkoutSave?: (workoutData: unknown) => void
        onWorkoutEvent?: (action: string, payload: unknown) => void
      }) => void
    }
  }
}

type WorkoutPreviewItemProps = {
  workout: WorkoutPreview
}

const SDK_URL = 'https://cdn.fizzup.com/js/workoutkit-sdk/1.0.0/sdk.js'

async function accessTokenProvider() {
  // Replace with your own end-user token retrieval when your integration needs it.
  return 'yourEndUserAccessToken'
}

function formatDuration(seconds: number) {
  const minutes = Math.round(seconds / 60)
  return `${minutes} min`
}

export function WorkoutPreviewItem({ workout }: WorkoutPreviewItemProps) {
  const [isReady, setIsReady] = useState(false)
  const [hasClickedStart, setHasClickedStart] = useState(false)
  const [loadWorkoutSession, { loading: isLoadingSession }] = useLazyQuery<
    GetSessionContentData,
    GetSessionContentVariables
  >(GET_SESSION_CONTENT, {
    fetchPolicy: 'network-only',
  })

  useEffect(() => {
    const existingScript = document.querySelector<HTMLScriptElement>(`script[src="${SDK_URL}"]`)

    const initialize = () => {
      window.workoutkit.init(WORKOUTKIT_BASE_URL)
      setIsReady(true)
    }

    if (existingScript) {
      if (window.workoutkit) {
        initialize()
      } else {
        existingScript.addEventListener('load', initialize, { once: true })
      }

      return
    }

    const script = document.createElement('script')
    script.src = SDK_URL
    script.async = true
    script.onload = initialize
    document.body.appendChild(script)

    return () => {
      script.onload = null
    }
  }, [])

  const startWorkout = async () => {
    setHasClickedStart(true)

    if (!window.workoutkit || !isReady) {
      return
    }

    const response = await loadWorkoutSession({
      variables: { id: workout.id },
    })

    if (!response.data?.publicWorkoutSession) {
      throw new Error('WorkoutKit session content is missing')
    }

    window.workoutkit.start({
      workoutId: workout.id,
      soundpack: 'none',
      getAccessToken: accessTokenProvider,
      onWorkoutQuit: () => {
        console.log('Workout quit', workout.id)
      },
      onWorkoutSave: (workoutData) => {
        console.log('Workout saved', workout.id, workoutData)
      },
      onWorkoutEvent: (action, payload) => {
        console.log('Workout event', action, payload)
      },
    })
  }

  const isStarting = hasClickedStart && (!isReady || isLoadingSession)

  return (
    <button type="button" onClick={startWorkout} disabled={isLoadingSession}>
      {workout.picture ? <img src={workout.picture} alt="" width="160" /> : null}

      <span>{workout.name}</span>
      <span>
        {formatDuration(workout.duration)}
        {workout.format ? ` - ${workout.format}` : ''}
      </span>

      <span>{isStarting ? 'Loading...' : 'Start'}</span>
    </button>
  )
}

5. App

Wrap the app with WorkoutKitGraphQLProvider, run GetSessions with Apollo Client, and render each workout.

tsx
import { useQuery } from '@apollo/client/react'
import { WorkoutKitGraphQLProvider } from './WorkoutKitGraphQLProvider'
import { GET_SESSIONS, type GetSessionsData, type GetSessionsVariables } from './queries'
import { WorkoutPreviewItem } from './WorkoutPreviewItem'

type WorkoutCatalogProps = GetSessionsVariables

function WorkoutCatalog({ tag, difficulty }: WorkoutCatalogProps) {
  const { data, loading, error } = useQuery<GetSessionsData, GetSessionsVariables>(GET_SESSIONS, {
    variables: {
      tag,
      difficulty,
    },
  })

  if (loading) {
    return <p>Loading workouts...</p>
  }

  if (error) {
    return <p role="alert">Unable to load workouts.</p>
  }

  const workouts = data?.publicWorkouts.edges.map((edge) => edge.node) ?? []

  return (
    <main>
      <h1>Choose a workout</h1>

      <div>
        {workouts.map((workout) => (
          <WorkoutPreviewItem key={workout.id} workout={workout} />
        ))}
      </div>
    </main>
  )
}

export default function App() {
  return (
    <WorkoutKitGraphQLProvider>
      <WorkoutCatalog tag="demo" difficulty="EASY" />
    </WorkoutKitGraphQLProvider>
  )
}

Integration notes

  • Call workoutkit.init() before the first start() call.
  • Keep your token retrieval logic outside the component and pass it with accessTokenProvider.
  • Keep event handlers wired to your own analytics or workout persistence flow.
  • Call workoutkit.close() if your UI needs to close the iframe programmatically.
  • Replace VITE_WORKOUTKIT_BASE_URL with the value provided for your environment.
  • Keep the catalog query small; the list page only needs preview fields.
  • Fetch GetSessionContent on click, close to launch, instead of relying only on the list payload.
  • Use getAccessToken only when your integration requires end-user authorization.
  • For complete launch-session retrieval, start from GetSessionContent in Queries.