AI Workout Generator API:
The Complete Developer Guide (2026)
One call to /v1/workout/generate returns a complete, structured workout plan — exercises, sets, reps, and rest periods — personalised by goal, duration, fitness level, and available equipment. This guide covers everything from basic integration through production-scale patterns: caching, rate limiting, deduplication, mobile proxying, and quota optimisation.
What Is a Workout Generator API?
A workout generator API is a server-side endpoint that accepts user preferences and returns a fully structured training session — exercises, sets, reps, rest periods, and ordering — without the client needing to implement any fitness programming logic. You send parameters, you get a workout back.
This sounds simple, but the engineering value is significant. Building even a basic workout planner from scratch means maintaining an exercise database, writing split logic, implementing equipment filters, handling progressive overload principles, and making sure compound movements are sequenced before isolation work. That's weeks of domain-specific development that has nothing to do with your actual app's differentiator.
The workout generator API pattern delegates all of that to a purpose-built service. Your frontend stays lean; the exercise programming logic lives server-side where it can be iterated independently of your app releases.
Static Exercise Database vs. Dynamic Workout Generation
Most exercise APIs return a static list: you query exercises by body part, equipment, or muscle, and you get back raw data. That's a valid use case — browseable exercise libraries, search features, informational content. But it puts the workout programming burden entirely on your app.
A workout generator API is different. It doesn't just return exercises; it returns a programme: an ordered sequence of movements with specific training prescriptions, calibrated to a goal and duration. The distinction matters for user retention — a static database can be downloaded once and cached forever, while a generator produces a fresh session on every call. Users keep opening the app because the workout changes.
Available on: Pro ($15.99/month) and Ultra ($24.99/month) plans. Each call to /v1/workout/generate counts as one API request. View pricing →
Diagram: Static exercise database vs. dynamic workout generation API flow
alt="Static exercise database API vs AI workout generator API comparison diagram"
How the Generator Works
The WorkoutX generator runs a four-step algorithm on every request. It's not a large language model — there's no probabilistic text generation involved. It's deterministic fitness programming logic encoded as a server-side engine, which makes it fast, hallucination-free, and consistently structured.
-
Filter the exercise pool — the 1,324-exercise database is narrowed to exercises matching the requested
equipmentandlevel. Adjacent fitness levels are included to avoid empty pools (e.g., requestingbeginneralso includesintermediateexercises when the beginner pool for a specific body part is sparse). If equipment filtering produces zero results, the engine falls back to the unfiltered pool so you always get a response. -
Determine target muscle groups — the
splitparameter maps to a predefined set of body parts (e.g.,push→ chest, shoulders, upper arms). IfbodyFocusis provided, it overrides the split entirely for fully custom muscle targeting. - Calculate exercise count from duration — the engine estimates how many exercises fit in the requested time window using a formula based on average set duration, rest periods from the goal preset, transition time between exercises, and a 4-minute warm-up/cool-down buffer.
-
Prioritise, randomise, and assign prescriptions — within each body part, compound movements are ranked first for
muscle_gainandstrengthgoals. The final pool is shuffled, so every call returns a different combination. Sets, reps, and rest periods are assigned from the goal preset — not generated, not approximated.
Diagram: Request → equipment filter → split mapping → duration calc → exercise selection → response
alt="AI workout generator API request filtering and generation pipeline diagram"
The Duration Model
One detail worth understanding for UI purposes: the estimatedDurationMinutes field in the response is a calculation, not a guarantee. It's based on average set time (40s of active lifting) + rest period from the goal preset + 30s transition between exercises + 4 minutes for warm-up/cool-down. Real workout duration varies based on how long your users actually rest. Fat loss workouts have 30s rest, so estimatedDurationMinutes will be shorter; strength workouts have 120s rest, so estimates run longer.
Display this value as "approximately X minutes" in your UI, not as a precise countdown. Users who follow rest periods strictly may finish early; users who chat between sets will run over.
Endpoint Reference
GET https://api.workoutxapp.com/v1/workout/generate
Authentication: X-WorkoutX-Key: wx_your_key_here
Plan required: Pro or Ultra
Quota cost: 1 request per call
All parameters are optional. Omitting them produces a sensible 45-minute intermediate muscle gain full-body session.
| Parameter | Type | Default | Options / Range |
|---|---|---|---|
goal | string | muscle_gain | muscle_gain · strength · fat_loss · endurance · mobility |
duration | integer | 45 | 20–120 (minutes). Exercise count auto-calculated from this. |
level | string | intermediate | beginner · intermediate · advanced |
split | string | full_body | full_body · upper · lower · push · pull · legs · core · push_pull_legs · upper_lower |
equipment | string | any | Comma-separated: barbell,dumbbell,body weight,cable,machine,kettlebell,band,ez barbell |
bodyFocus | string | — | Comma-separated body parts. Overrides split entirely. |
exclude | string | — | Comma-separated exercise IDs to skip (user dislikes / recently done). |
Response Schema
{
"goal": "muscle_gain",
"level": "intermediate",
"split": "push",
"bodyFocus": ["chest", "shoulders", "upper arms"],
"equipment": ["barbell", "dumbbell"],
"totalExercises": 5,
"estimatedDurationMinutes": 42,
"exercises": [
{
"order": 1,
"sets": 4,
"reps": "6-8",
"restSeconds": 90,
"note": "Primary compound movement",
"exercise": {
"id": "0025",
"name": "Barbell Bench Press",
"bodyPart": "chest",
"target": "pectorals",
"equipment": "barbell",
"gifUrl": "https://api.workoutxapp.com/v1/gifs/0025.gif",
"difficulty": "intermediate",
"mechanic": "compound",
"secondaryMuscles": ["triceps", "anterior deltoid"]
}
}
// ... more exercises
]
}
Heads up: The gifUrl in each exercise requires authentication to access — it points to /v1/gifs/:id.gif which validates your API key. Embedding the raw URL in an <img> tag won't work directly without proxying through your backend or using the GIF endpoint with your key in the request headers. See the GIF serving docs for details.
Quickstart
The simplest possible call. No parameters — you get a sensible 45-minute intermediate full-body muscle gain session:
curl 'https://api.workoutxapp.com/v1/workout/generate' \ -H "X-WorkoutX-Key: wx_your_key_here"
A more targeted call — 30-minute beginner fat loss session, bodyweight only:
curl 'https://api.workoutxapp.com/v1/workout/generate?goal=fat_loss&duration=30&level=beginner&equipment=body+weight&split=full_body' \ -H "X-WorkoutX-Key: wx_your_key_here"
Advanced push day with specific equipment and excluded exercises:
curl 'https://api.workoutxapp.com/v1/workout/generate?goal=strength&duration=60&level=advanced&split=push&equipment=barbell,dumbbell&exclude=0025,0034,0101' \ -H "X-WorkoutX-Key: wx_your_key_here"
JavaScript Integration
Here's a production-grade generateWorkout() function with proper error handling, retry logic, and structured response typing. This is what you'd actually ship — not a minimal snippet that ignores 403s and network errors.
const API_KEY = process.env.WORKOUTX_API_KEY; // never hardcode in client-side code const BASE = 'https://api.workoutxapp.com/v1'; /** * Generate a workout plan. Call this from your backend — do not expose * your API key in client-side JavaScript or mobile app bundles. * * @param {Object} opts * @param {string} opts.goal - muscle_gain | strength | fat_loss | endurance | mobility * @param {number} opts.duration - 20–120 minutes * @param {string} opts.level - beginner | intermediate | advanced * @param {string} opts.split - full_body | push | pull | legs | upper | lower | core | push_pull_legs | upper_lower * @param {string[]} opts.equipment - ['barbell','dumbbell','body weight',...] * @param {string[]} opts.bodyFocus - overrides split — ['chest','back',...] * @param {string[]} opts.exclude - exercise IDs to skip — ['0025','0034'] * @param {number} opts.retries - max retries on network errors (default: 2) * @returns {Promise<Object>} */ async function generateWorkout(opts = {}, retries = 2) { const params = new URLSearchParams({ goal: opts.goal || 'muscle_gain', duration: opts.duration || 45, level: opts.level || 'intermediate', split: opts.split || 'full_body', }); if (opts.equipment?.length) params.set('equipment', opts.equipment.join(',')); if (opts.bodyFocus?.length) params.set('bodyFocus', opts.bodyFocus.join(',')); if (opts.exclude?.length) params.set('exclude', opts.exclude.join(',')); for (let attempt = 0; attempt <= retries; attempt++) { try { const res = await fetch(`${BASE}/workout/generate?${params}`, { headers: { 'X-WorkoutX-Key': API_KEY }, signal: AbortSignal.timeout(8000), // 8s timeout — safe for mobile }); if (res.status === 403) { // Plan gate — no point retrying throw { code: 'PLAN_REQUIRED', message: 'Workout Generator requires Pro or Ultra plan', status: 403 }; } if (res.status === 429) { // Rate limited — respect Retry-After if present const retryAfter = parseInt(res.headers.get('Retry-After')) || 60; if (attempt < retries) { await new Promise(r => setTimeout(r, retryAfter * 1000)); continue; } throw { code: 'RATE_LIMITED', message: 'Rate limit hit — retry later', retryAfter, status: 429 }; } if (res.status === 422) { // Invalid params — don't retry, surface the message const body = await res.json(); throw { code: 'INVALID_PARAMS', message: body.message, status: 422 }; } if (!res.ok) { if (attempt < retries) { await new Promise(r => setTimeout(r, 500 * (attempt + 1))); // linear backoff continue; } throw { code: 'API_ERROR', message: `Unexpected status: ${res.status}`, status: res.status }; } return res.json(); } catch (err) { if (err.code) throw err; // structured error from above — rethrow if (attempt < retries) { await new Promise(r => setTimeout(r, 500 * (attempt + 1))); continue; } throw { code: 'NETWORK_ERROR', message: err.message, original: err }; } } }
Rendering the Workout
function renderWorkout(workout, containerId) { const container = document.getElementById(containerId); if (!container) return; const header = ` <div class="workout-header"> <h2>Today's Workout</h2> <p>${workout.goal.replace(/_/g,' ')} · ${workout.level} · ~${workout.estimatedDurationMinutes} min</p> <p>${workout.totalExercises} exercises · ${workout.bodyFocus.join(', ')}</p> </div> `; const cards = workout.exercises.map(item => { const ex = item.exercise; // Note: gifUrl requires auth — serve it through your backend proxy // or use the GIF endpoint with a server-side token exchange. // Don't embed the gifUrl directly if your API key is not public. return ` <div class="exercise-card" data-exercise-id="${ex.id}"> <span class="order">${item.order}</span> <img src="/api/gif-proxy/${ex.id}" alt="${ex.name} exercise demonstration" loading="lazy" width="120" height="120" /> <div class="details"> <h3>${ex.name}</h3> <p class="target">${ex.target} · ${ex.equipment}</p> <p class="prescription"> <strong>${item.sets} sets × ${item.reps} reps</strong> · ${item.restSeconds}s rest </p> <span class="badge ${item.note.includes('compound') ? 'compound' : 'accessory'}"> ${item.note} </span> </div> </div> `; }).join(''); container.innerHTML = header + cards; } // Usage document.getElementById('generate-btn') .addEventListener('click', async () => { const btn = document.getElementById('generate-btn'); btn.textContent = 'Generating...'; btn.disabled = true; try { const workout = await generateWorkout({ goal: document.getElementById('goal').value, duration: +document.getElementById('duration').value, level: document.getElementById('level').value, split: document.getElementById('split').value, equipment: selectedEquipment, // from your equipment picker state exclude: getRecentExerciseIds(), // from localStorage or DB }); renderWorkout(workout, 'workout-result'); saveCompletedExercises(workout.exercises.map(e => e.exercise.id)); } catch (err) { if (err.code === 'PLAN_REQUIRED') showUpgradeModal(); else if (err.code === 'RATE_LIMITED') showRateLimitMessage(err.retryAfter); else console.error('Workout generation failed:', err); } finally { btn.textContent = 'Regenerate Workout'; btn.disabled = false; } });
Python Integration
For server-side generation — useful in Flask/FastAPI backends, scheduled workout delivery, or batch plan generation for multiple users:
import os, time import requests from requests.exceptions import HTTPError, Timeout, ConnectionError API_KEY = os.environ['WORKOUTX_API_KEY'] # load from env, never hardcode BASE_URL = 'https://api.workoutxapp.com/v1' TIMEOUT = 8 # seconds def generate_workout( goal='muscle_gain', duration=45, level='intermediate', split='full_body', equipment=None, body_focus=None, exclude=None, max_retries=2, ): """ Generate a workout plan via the WorkoutX API. Raises: PermissionError: Plan upgrade required (403) ValueError: Invalid parameters (422) RuntimeError: Rate limited or server error """ params = {'goal': goal, 'duration': duration, 'level': level, 'split': split} if equipment: params['equipment'] = ','.join(equipment) if body_focus: params['bodyFocus'] = ','.join(body_focus) if exclude: params['exclude'] = ','.join(exclude) headers = {'X-WorkoutX-Key': API_KEY} for attempt in range(max_retries + 1): try: r = requests.get( f'{BASE_URL}/workout/generate', headers=headers, params=params, timeout=TIMEOUT, ) if r.status_code == 403: raise PermissionError('Workout Generator requires Pro or Ultra plan') if r.status_code == 422: body = r.json() raise ValueError(f"Invalid params: {body.get('message', r.text)}") if r.status_code == 429: retry_after = int(r.headers.get('Retry-After', 60)) if attempt < max_retries: time.sleep(retry_after) continue raise RuntimeError(f'Rate limited. Retry after {retry_after}s') r.raise_for_status() return r.json() except (Timeout, ConnectionError) as e: if attempt < max_retries: time.sleep(0.5 * (attempt + 1)) continue raise RuntimeError(f'Network error after {max_retries + 1} attempts: {e}') # ── Example usage ────────────────────────────────────────────────────────── # 30-min beginner fat loss, bodyweight only workout = generate_workout( goal='fat_loss', duration=30, level='beginner', split='full_body', equipment=['body weight'] ) print(f"Generated {workout['totalExercises']} exercises (~{workout['estimatedDurationMinutes']} min)") for item in workout['exercises']: ex = item['exercise'] print(f" {item['order']}. {ex['name']} — {item['sets']}×{item['reps']} | {item['restSeconds']}s rest") # 60-min advanced push day with exclusion done_this_week = ['0025', '0034', '0101'] push_day = generate_workout( goal='strength', duration=60, level='advanced', split='push', equipment=['barbell', 'dumbbell'], exclude=done_this_week )
Production Patterns
Getting the API responding locally is straightforward. Making it work well at scale — or even just for a small app with real users — involves a handful of non-obvious decisions.
Mobile Apps: Always Proxy Through Your Backend
If you're building an iOS or Android app (React Native, Flutter, Swift, Kotlin), do not call the WorkoutX API directly from the mobile client. Any API key embedded in a mobile binary can be extracted from the compiled app package — it's a well-known attack vector.
The correct architecture is a thin backend route on your own server that:
- Authenticates your user (your own auth)
- Validates the request parameters
- Forwards the request to WorkoutX with your server-side API key
- Returns the response to the mobile client
This also lets you add your own rate limiting per user, log workout generation events, inject custom business logic (e.g. enforce the user's subscription tier before forwarding), and cache responses when appropriate.
Architecture diagram: Mobile app → your backend proxy → WorkoutX API
alt="Mobile fitness app backend API proxy architecture diagram for workout generator"
// Your backend: POST /workout/generate // Client sends user prefs, server holds the API key app.post('/workout/generate', requireAuth, async (req, res) => { const { goal, duration, level, split, equipment, exclude } = req.body; // Validate user's subscription before forwarding if (!req.user.hasFeature('workoutGenerator')) { return res.status(403).json({ error: 'Upgrade required' }); } const params = new URLSearchParams({ goal, duration, level, split }); if (equipment?.length) params.set('equipment', equipment.join(',')); if (exclude?.length) params.set('exclude', exclude.join(',')); try { const upstream = await fetch( `https://api.workoutxapp.com/v1/workout/generate?${params}`, { headers: { 'X-WorkoutX-Key': process.env.WORKOUTX_API_KEY } } ); if (!upstream.ok) { const body = await upstream.json().catch(() => ({})); return res.status(upstream.status).json(body); } const workout = await upstream.json(); res.json(workout); } catch (err) { res.status(502).json({ error: 'Upstream unavailable' }); } });
Caching: When to Cache, When Not To
The short answer: don't cache workout responses for reuse. The value of the generator is freshness — the same user hitting "Generate" twice in a row should see different exercises.
That said, there's a reasonable caching pattern for "today's workout": generate once when the user opens the app for the day, cache the result with a TTL that expires at midnight local time, and serve the cached version for the rest of the day. This avoids repeated quota hits for users who open and close the app multiple times, while still ensuring a fresh workout each day.
const cache = new Map(); // replace with Redis in production function getDailyCacheKey(userId, params) { const today = new Date().toISOString().split('T')[0]; // YYYY-MM-DD return `workout:${userId}:${today}:${params.goal}:${params.split}`; } function msUntilMidnight() { const now = new Date(); const midnight = new Date(now).setHours(24, 0, 0, 0); return midnight - now; } async function getTodaysWorkout(userId, params) { const key = getDailyCacheKey(userId, params); const cached = cache.get(key); if (cached) return { workout: cached, fromCache: true }; const workout = await generateWorkout(params); cache.set(key, workout); setTimeout(() => cache.delete(key), msUntilMidnight()); // auto-expire at midnight return { workout, fromCache: false }; }
The "Regenerate" button — which calls the API again with the same or slightly modified params — should bypass this cache intentionally. That's the whole point of regeneration.
Preventing Workout Repetition
The generator randomises on every call, but with a large enough exercise exclusion list you can guarantee variety across sessions. Store the IDs of exercises a user has seen in the last 7–14 days, then pass them as exclude. The engine's fallback logic ensures you always get a valid response even if the exclusion list covers most of the filtered pool.
A practical implementation using localStorage for a client-side or React Native app:
const HISTORY_KEY = 'wx_exercise_history'; const MAX_DAYS = 14; function loadHistory() { try { return JSON.parse(localStorage.getItem(HISTORY_KEY) || '[]'); } catch { return []; } } function saveHistory(entries) { // Prune entries older than MAX_DAYS const cutoff = Date.now() - MAX_DAYS * 24 * 60 * 60 * 1000; const fresh = entries.filter(e => e.ts > cutoff); localStorage.setItem(HISTORY_KEY, JSON.stringify(fresh)); } export function getRecentExerciseIds() { return loadHistory().map(e => e.id); } export function markExercisesAsDone(exerciseIds) { const history = loadHistory(); const now = Date.now(); exerciseIds.forEach(id => history.push({ id, ts: now })); saveHistory(history); } // After workout completes: markExercisesAsDone(workout.exercises.map(e => e.exercise.id));
Quota Optimisation
If you're on the Pro plan (10,000 requests/month), each workout generation costs one request. A user who generates once per day uses 30 requests per month — meaning you can support ~333 daily-active users on a single Pro key. The Ultra plan (35,000 requests/month) scales to ~1,166 DAU at the same usage rate.
A few patterns that reduce wasted requests:
- Daily cache per user — generate once per day per user, cache until midnight (see above). Users who reopen the app multiple times don't cost multiple requests.
- Debounce the Regenerate button — add a short cooldown (2–5 seconds) between successive regenerate calls. Power users tapping rapidly would otherwise consume their own session quota quickly.
- Don't generate on app cold start — only call the API when the user actively navigates to the workout tab, not on every app launch.
- Use the
excludeparam efficiently — passing the full 14-day history every call adds URL length but doesn't change quota cost. Trim to the last 7 days in production to keep URLs short.
Use Cases and Common Patterns
"Today's Workout" Feature
The highest-ROI integration. Call /v1/workout/generate when the user opens the workout tab. Because the response is freshly randomised (and you're using the exclude param for deduplication), users see a different routine every day without you building any session history logic beyond a simple localStorage array.
Regenerate Button
A "Shuffle" or "Don't like this workout" button that calls the endpoint again with the same base parameters. This is the highest-engagement pattern — users who dislike a particular exercise selection can instantly get a variation. Add a small loading state (the response comes back in under 100ms, but UX polish matters) and debounce rapid taps.
Home vs. Gym Mode
Let users toggle between equipment profiles. Pass the corresponding equipment array and the generator handles the filtering — no exercise knowledge required on your side.
const EQUIPMENT_MODES = { home: ['body weight'], minimal: ['body weight', 'dumbbell', 'resistance band'], gym: ['barbell', 'dumbbell', 'cable', 'machine'], crossfit: ['barbell', 'kettlebell', 'body weight', 'pull-up bar'], any: [], // omitting equipment = no filter }; const workout = await generateWorkout({ equipment: EQUIPMENT_MODES[userProfile.gymMode] || [], });
PPL / Upper-Lower Programme
For users following a structured split programme, let them select today's session. The generator handles exercise selection per split — you just need a day picker:
// 3-day PPL split — auto-pick based on day of week const PPL_SCHEDULE = { Monday: 'push', Tuesday: 'pull', Wednesday: 'legs', Thursday: 'push', Friday: 'pull', Saturday: 'legs', Sunday: 'full_body' }; const today = new Date().toLocaleDateString('en-US', { weekday: 'long' }); const split = PPL_SCHEDULE[today] || 'full_body'; const workout = await generateWorkout({ split, goal: userProfile.goal });
Offline & Sync Considerations
If your app supports offline mode (common in mobile fitness apps where users work out in areas with poor connectivity), generate the workout while the user is online and cache it locally for offline playback. The exercise GIF URLs will still require a connection to load, so you'll need to either pre-fetch the GIFs or gracefully degrade to static thumbnails when offline.
A practical pattern: when the user opens the app with a connection, silently pre-generate tomorrow's workout in the background and store it. If they open the app offline the next day, serve the pre-generated plan.
Rate Limits and Error Handling
The WorkoutX API enforces both per-minute rate limits and monthly quota limits. Understanding both helps you design your integration to avoid user-facing errors.
Per-Minute Rate Limits
The Pro plan allows 200 requests/minute; Ultra allows 600/minute. For a workout generator called once per user action, you'd need to be serving multiple simultaneous users to hit these limits. That said, if you're generating workouts server-side in a batch job (e.g., pre-generating plans for all users nightly), you could hit the per-minute limit.
When you hit the rate limit, the API returns 429 Too Many Requests with a Retry-After header in seconds. Your client code should respect this — the production JS and Python examples above both implement this correctly.
Monthly Quota
Quota usage is tracked in the response headers: X-Quota-Remaining, X-Quota-Limit, and X-Quota-Reset. Log these in development to understand your usage pattern before going to production.
const res = await fetch(`${BASE}/workout/generate`, { headers: { ... } }); const remaining = res.headers.get('X-Quota-Remaining'); const limit = res.headers.get('X-Quota-Limit'); const reset = res.headers.get('X-Quota-Reset'); // Unix timestamp // Warn if under 10% remaining if (+remaining / +limit < 0.1) { console.warn(`Low quota: ${remaining}/${limit} remaining. Resets at ${new Date(+reset * 1000).toISOString()}`); }
Error codes summary
| Status | Code | Retry? | Action |
|---|---|---|---|
401 | Unauthorized | No | Invalid or missing API key |
403 | Plan Required | No | Upgrade to Pro or Ultra |
422 | Invalid Params | No | Fix request parameters |
429 | Rate Limited | Yes, after Retry-After | Respect Retry-After header |
5xx | Server Error | Yes, with backoff | Exponential backoff, max 3 attempts |
Goal Presets: The Training Science
Each goal maps to a specific training prescription derived from exercise science principles. These aren't arbitrary numbers — they reflect established hypertrophy, strength, and conditioning programming:
| Goal | Sets (compound / accessory) | Reps | Rest | Priority |
|---|---|---|---|---|
muscle_gain | 4 / 3 | 6-8 / 8-10 | 90s / 60s | Compound-first, hypertrophy range |
strength | 5 / 4 | 3-5 / 4-6 | 120s / 90s | Compound-first, heavy load |
fat_loss | 3 | 12-15 / 15-20 | 30s / 45s | High volume, metabolic stress |
endurance | 3 / 2 | 15-20 / 20-25 | 20s / 30s | High rep, minimal rest |
mobility | 2 | 10-12 / 12-15 | 30s | Isolation, range of motion focus |
For muscle_gain and strength, compound movements (bench press, squat, deadlift variants, rows) are always placed before isolation exercises in the response order. This is standard progressive overload programming — you want the primary movements performed fresh, not after the stabiliser muscles are fatigued from curls.
Workout Splits Reference
| Split | Target Body Parts | Best For |
|---|---|---|
full_body | Chest, back, shoulders, legs, arms | Beginners, 2–3 day/week programmes |
upper | Chest, back, shoulders, arms | Upper/lower split — 4 day/week |
lower | Quads, hamstrings, glutes, lower back | Upper/lower split — leg days |
push | Chest, shoulders, triceps | PPL split — push days |
pull | Back, biceps, rear deltoids | PPL split — pull days |
legs | Quads, hamstrings, calves | PPL split — leg days |
core | Abs, lower back, obliques | Core finishers, injury rehab |
push_pull_legs | Full compound — all major groups | Single PPL session with full coverage |
upper_lower | Upper body focus with carry-over | Upper-lower without isolation legs |
Personalisation Strategies
The generator's seven parameters give you more personalisation surface than it might first appear. Here are patterns used in production fitness apps:
Mapping User Profiles to Parameters
Most fitness apps collect a user profile on onboarding: goal, experience level, available equipment, preferred session length. Map these directly to generator parameters. No processing needed — the API parameters are designed to match exactly what users say about themselves.
function profileToWorkoutParams(user) { return { goal: user.fitnessGoal, // 'muscle_gain' | 'fat_loss' | ... level: user.experienceLevel, // 'beginner' | 'intermediate' | 'advanced' duration: user.preferredDuration, // 30 | 45 | 60 equipment: user.availableEquipment, // ['barbell','dumbbell'] or [] split: user.currentProgramSplit, // 'push' | 'pull' | 'full_body' | ... exclude: getRecentExerciseIds(user.id), }; }
Progressive Overload Hints
The API doesn't track individual user load progression (weight on the bar, RPE) — that's your app's domain. But you can use the level parameter to implement a basic progression model: start users on beginner, automatically promote to intermediate after 4–6 weeks of consistent activity, and to advanced after 6 months. This changes the exercise pool and the rep/set prescription in the generator, simulating progressive difficulty without you encoding any fitness programming rules.
WorkoutX vs. Static Exercise APIs
There are a handful of free and paid exercise APIs available (ExerciseDB on RapidAPI being the most popular). They're different tools for different use cases — it's worth understanding the trade-offs.
Static Database APIs
Free exercise database APIs return a list of exercises you can filter by body part or equipment. They're perfect for:
- Searchable exercise libraries in your app
- Exercise detail pages with GIFs and instructions
- Reference content (SEO-driven exercise guides)
The limitation: you get raw data, not a programme. You have to write the "which exercises should I do today?" logic yourself — picking exercises, ordering them, assigning sets/reps/rest. Most developers underestimate how much domain knowledge that requires.
The second limitation: static exercise data can be downloaded once and cached indefinitely. A developer who needs your exercise database can download it in a single paginated API crawl and never call your API again. This is a common pattern that makes exercise data APIs difficult to monetise beyond a one-time purchase.
Why a Generator API Is Different
A workout generator API is inherently stateful per call — the randomisation, the duration calculation, the compound-first ordering, the exclusion logic. The response cannot be pre-downloaded and cached, because its value is in the freshness of the selection. Every call is genuinely different. This makes it a better product both for your users (variety) and for API monetisation (sustained usage).
WorkoutX approach: Use the exercise list endpoint for your exercise library and detail pages (search, browse, reference content), and /v1/workout/generate for active workout sessions. The two complement each other — one is static data, the other is a dynamic service.
Frequently Asked Questions
Is this actually AI, or just filtering?
It's a rule-based fitness programming engine, not a large language model. The algorithm applies real exercise science principles — periodisation rep ranges, compound-before-isolation sequencing, progressive overload set/rep prescriptions — encoded as deterministic server-side logic. Fast, consistent, and free of hallucinations. The "AI" framing reflects the intelligent decision-making (equipment fallback, duration calculation, goal-aware selection), not the technology stack.
What happens if no exercises match my equipment filter?
The engine has a two-tier fallback. First, it tries to widen the level filter (e.g. beginner + intermediate instead of just beginner). If the equipment filter produces an empty pool after that, it falls back to the full exercise database ignoring equipment constraints, while still respecting the body part split and exclusion list. You will always get a valid, non-empty response.
Can I cache the response?
You can, but cache strategically. Caching for the duration of a user's workout session (to support offline mid-workout) is sensible. Caching for multiple days removes the variety that makes the generator valuable. See the caching section above for a daily-cache pattern that balances quota efficiency with freshness.
Is it safe to call from a mobile app?
No — always proxy through your backend. Embedding an API key in a mobile binary is a security risk. See the mobile proxy section for the recommended architecture.
Does each call count as one request?
Yes. One call to /v1/workout/generate = one request against your monthly quota, regardless of how many exercises are in the response.
Can I use bodyFocus and split together?
bodyFocus overrides split entirely. If you pass both, bodyFocus takes precedence and split is ignored. Use split for named programmes (PPL, upper/lower), and bodyFocus when you need to target specific muscle groups that don't map to a standard split.
Next Steps
Ready to ship a workout generator in your app?
- Get your API key — sign up free, upgrade to Pro for workout generation
- Full endpoint documentation — complete parameter reference and response schema
- Authentication guide — API key setup and header format
- Rate limits & quotas — per-plan limits and header reference
- Similar Exercises endpoint — show alternatives when a user can't do a specific movement
- How to Build a Workout App from Scratch — architecture guide covering auth, exercise data, workout logging, and progress tracking
Quick test: The endpoint responds in under 100ms on cold requests. Try it with curl using your API key — a complete workout plan comes back before you can blink. No warmup, no infrastructure, no ML setup.
Related Developer Resources
How to Build a Workout App
Full architecture guide — auth, data, logging, progress tracking
Best Exercise APIs in 2026
Comparison of exercise database and workout planner APIs
Exercise API in React
Step-by-step tutorial — hooks, components, GIF rendering
Similar Exercises API
Find substitutions for any exercise — same muscle, different equipment
Add the AI Workout Generator to your app
Available on Pro ($15.99/month) and Ultra ($24.99/month). Upgrade your existing key or sign up in 30 seconds.