Appearance
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 URLWorkoutKitGraphQLProvider.tsx: Apollo Client setup and providerqueries.ts: WorkoutKit GraphQL query and shared typesWorkoutPreviewItem.tsx: one clickable workout card that starts the Web SDKApp.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.comThe 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 graphqlCreate 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 firststart()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_URLwith the value provided for your environment. - Keep the catalog query small; the list page only needs preview fields.
- Fetch
GetSessionContenton click, close to launch, instead of relying only on the list payload. - Use
getAccessTokenonly when your integration requires end-user authorization. - For complete launch-session retrieval, start from
GetSessionContentin Queries.
