Complete Guide Production-Ready May 17, 2026 · 18 min read Pro & Ultra

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.

API Version: v1 · Last tested: May 17, 2026 · Languages: JavaScript, Python · Base URL: api.workoutxapp.com
1,324
Exercises in DB
<100ms
Avg response time
9
Workout splits
5
Goal presets

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 →

schema

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.

  1. Filter the exercise pool — the 1,324-exercise database is narrowed to exercises matching the requested equipment and level. Adjacent fitness levels are included to avoid empty pools (e.g., requesting beginner also includes intermediate exercises 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.
  2. Determine target muscle groups — the split parameter maps to a predefined set of body parts (e.g., push → chest, shoulders, upper arms). If bodyFocus is provided, it overrides the split entirely for fully custom muscle targeting.
  3. 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.
  4. Prioritise, randomise, and assign prescriptions — within each body part, compound movements are ranked first for muscle_gain and strength goals. 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.
account_tree

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
goalstringmuscle_gainmuscle_gain · strength · fat_loss · endurance · mobility
durationinteger4520–120 (minutes). Exercise count auto-calculated from this.
levelstringintermediatebeginner · intermediate · advanced
splitstringfull_bodyfull_body · upper · lower · push · pull · legs · core · push_pull_legs · upper_lower
equipmentstringanyComma-separated: barbell,dumbbell,body weight,cable,machine,kettlebell,band,ez barbell
bodyFocusstringComma-separated body parts. Overrides split entirely.
excludestringComma-separated exercise IDs to skip (user dislikes / recently done).

Response Schema

GET /v1/workout/generate — 200 OK
{
  "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 GET /v1/workout/generate
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 — fat loss / beginner / bodyweight
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 — push day / advanced / exclude recent
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.

workout-generator.js — production version
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

render-workout.js
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>
            &nbsp;·&nbsp; ${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:

workout_generator.py — production version
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:

  1. Authenticates your user (your own auth)
  2. Validates the request parameters
  3. Forwards the request to WorkoutX with your server-side API key
  4. 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.

device_hub

Architecture diagram: Mobile app → your backend proxy → WorkoutX API

alt="Mobile fitness app backend API proxy architecture diagram for workout generator"

Express.js — backend proxy route
// 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.

Node.js — daily workout cache (server-side)
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:

exercise-history.js
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 exclude param 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.

JavaScript — equipment mode profiles
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:

JavaScript — split 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.

JavaScript — reading quota headers
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
401UnauthorizedNoInvalid or missing API key
403Plan RequiredNoUpgrade to Pro or Ultra
422Invalid ParamsNoFix request parameters
429Rate LimitedYes, after Retry-AfterRespect Retry-After header
5xxServer ErrorYes, with backoffExponential 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_gain4 / 36-8 / 8-1090s / 60sCompound-first, hypertrophy range
strength5 / 43-5 / 4-6120s / 90sCompound-first, heavy load
fat_loss312-15 / 15-2030s / 45sHigh volume, metabolic stress
endurance3 / 215-20 / 20-2520s / 30sHigh rep, minimal rest
mobility210-12 / 12-1530sIsolation, 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_bodyChest, back, shoulders, legs, armsBeginners, 2–3 day/week programmes
upperChest, back, shoulders, armsUpper/lower split — 4 day/week
lowerQuads, hamstrings, glutes, lower backUpper/lower split — leg days
pushChest, shoulders, tricepsPPL split — push days
pullBack, biceps, rear deltoidsPPL split — pull days
legsQuads, hamstrings, calvesPPL split — leg days
coreAbs, lower back, obliquesCore finishers, injury rehab
push_pull_legsFull compound — all major groupsSingle PPL session with full coverage
upper_lowerUpper body focus with carry-overUpper-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.

JavaScript — user profile → API params
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?

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.

auto_awesome

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.

Get API Key arrow_forward
Tutorial May 17, 2026 · 9 min read Pro & Ultra

How to Add an AI Workout Generator to Your Fitness App

One API 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. No ML infrastructure, no complex logic. Just an HTTP request.

What Is a Workout Generator API?

A workout generator API is a server-side endpoint that accepts user preferences and returns a structured training session. Instead of manually building exercise selection and programming logic inside your app, you delegate the entire process to an API call.

This matters for three reasons:

  • No static data downloads: The workout is assembled server-side on every call — users cannot cache and reuse a pre-downloaded database to replace the API.
  • Infinite variety: The same parameters produce different exercise combinations on each call, making "today's workout" feel genuinely fresh every session.
  • Zero exercise programming knowledge required: Sets, reps, rest periods, and compound vs. isolation prioritisation are handled by the API, not your frontend.

Available on: Pro ($15.99/month) and Ultra ($24.99/month) plans. Each call to /v1/workout/generate counts as one API request against your monthly quota. View pricing →

How the Generator Works

The WorkoutX generator runs a four-step algorithm on every request:

  1. Filter the exercise pool — the 1,300+ exercise database is narrowed to exercises matching the requested equipment and fitness level (adjacent levels are included to avoid empty pools).
  2. Determine muscle groups — the split parameter maps to a set of target body parts (e.g. push → chest, shoulders, triceps). If bodyFocus is provided, it overrides the split for fully custom selection.
  3. Calculate exercise count — using your requested duration, the engine estimates how many exercises fit given average set time, rest periods, and a warm-up/cool-down buffer.
  4. Prioritise and randomise — within each body part, compound movements are ranked first for muscle_gain and strength goals. The final selection is shuffled, so every call returns a different combination.

The result is a list of exercises with pre-assigned sets, reps, and rest periods based on the goal preset — no programming knowledge needed on the developer side.

Endpoint Reference

All parameters are optional — the defaults produce a sensible 45-minute intermediate muscle gain full-body session.

Parameter Default Options
goalmuscle_gainmuscle_gain · strength · fat_loss · endurance · mobility
duration4520–120 (minutes) — exercise count auto-calculated
levelintermediatebeginner · intermediate · advanced
splitfull_bodyfull_body · upper · lower · push · pull · legs · core · push_pull_legs · upper_lower
equipmentanyComma-separated list: barbell,dumbbell,body+weight,cable,machine…
bodyFocusComma-separated body parts — overrides split entirely
excludeComma-separated exercise IDs to skip (user dislikes / already done)

Quickstart: Your First Generated Workout

The simplest possible call — no parameters, sensible defaults:

curl GET /v1/workout/generate
curl 'https://api.workoutxapp.com/v1/workout/generate' \
  -H "X-WorkoutX-Key: wx_your_key_here"
JSON Response
{
  "goal": "muscle_gain",
  "level": "intermediate",
  "split": "full_body",
  "bodyFocus": ["chest", "back", "shoulders", "upper legs", "lower legs", "upper arms"],
  "equipment": ["any"],
  "totalExercises": 6,
  "estimatedDurationMinutes": 44,
  "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",
        "effortLevel": "intermediate"
      }
    },
    // ... 5 more exercises
  ]
}

JavaScript Integration

Here's a complete "Today's Workout" component in vanilla JavaScript. A user picks their preferences and clicks Generate — the result renders as an exercise card list:

workout-generator.js
const API_KEY = 'wx_your_key_here';
const BASE = 'https://api.workoutxapp.com/v1';

/**
 * Generate a workout plan from user preferences.
 * @param {Object} opts - User preferences
 */
async function generateWorkout(opts = {}) {
  const params = new URLSearchParams({
    goal:      opts.goal      || 'muscle_gain',
    duration:  opts.duration  || 45,
    level:     opts.level     || 'intermediate',
    split:     opts.split     || 'full_body',
    ...(opts.equipment?.length  && { equipment: opts.equipment.join(',') }),
    ...(opts.bodyFocus?.length  && { bodyFocus: opts.bodyFocus.join(',') }),
    ...(opts.exclude?.length    && { exclude:   opts.exclude.join(',') }),
  });

  const res = await fetch(`${BASE}/workout/generate?${params}`, {
    headers: { 'X-WorkoutX-Key': API_KEY }
  });

  if (res.status === 403) {
    throw new Error('AI Workout Generator requires Pro or Ultra plan');
  }
  if (!res.ok) {
    throw new Error(`API error: ${res.status}`);
  }

  return res.json();
}

/**
 * Render a workout plan into an HTML element.
 */
function renderWorkout(workout, containerId) {
  const container = document.getElementById(containerId);

  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 exercises = workout.exercises.map(item => `
    <div class="exercise-card">
      <span class="order">${item.order}</span>
      <img
        src="${item.exercise.gifUrl}"
        alt="${item.exercise.name}"
        loading="lazy"
        width="120" height="120"
      />
      <div class="details">
        <h3>${item.exercise.name}</h3>
        <p class="target">${item.exercise.target} · ${item.exercise.equipment}</p>
        <p class="prescription">
          <strong>${item.sets} sets × ${item.reps} reps</strong>
          &nbsp;·&nbsp; ${item.restSeconds}s rest
        </p>
        <span class="badge">${item.note}</span>
      </div>
    </div>
  `).join('');

  container.innerHTML = header + exercises;
}

// ── Example 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: ['barbell', 'dumbbell'],
      });

      renderWorkout(workout, 'workout-result');
    } catch (err) {
      console.error(err);
    } finally {
      btn.textContent = 'Regenerate Workout';
      btn.disabled = false;
    }
  });

Python Integration

Here's the same logic in Python — useful for server-side rendering or a Flask/FastAPI backend that generates workouts for your users:

workout_generator.py
import requests

API_KEY = 'wx_your_key_here'
BASE_URL = 'https://api.workoutxapp.com/v1'

def generate_workout(
    goal='muscle_gain',
    duration=45,
    level='intermediate',
    split='full_body',
    equipment=None,
    body_focus=None,
    exclude=None
):
    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)

    response = requests.get(
        f'{BASE_URL}/workout/generate',
        headers={'X-WorkoutX-Key': API_KEY},
        params=params
    )
    response.raise_for_status()
    return response.json()


# Example 1: 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")


# Example 2: Advanced push day, barbell + dumbbell
push_day = generate_workout(
    goal='strength',
    duration=60,
    level='advanced',
    split='push',
    equipment=['barbell', 'dumbbell']
)


# Example 3: Exclude exercises user has already done today
fresh_workout = generate_workout(
    goal='muscle_gain',
    exclude=['0025', '0034', '0101']
)

Use Cases & Common Patterns

Daily "Today's Workout" Feature

The most popular use case. Call /v1/workout/generate on page load or when the user opens the app. Because the response is freshly randomised, users see a different workout every day without you storing any session history. Each load = 1 API call.

Regenerate Button

Add a "Regenerate" or "Shuffle" button that calls the endpoint again with the same parameters. Users who don't like a particular exercise selection can instantly get a fresh variation. This is the highest-engagement pattern — power users tap regenerate multiple times.

Home vs. Gym Mode

Let users toggle between "Home" (bodyweight only) and "Gym" (full equipment). Pass the corresponding equipment value and the generator filters to only include exercises achievable with what the user has available.

JavaScript — Home vs. Gym toggle
const EQUIPMENT_MODES = {
  home:    ['body weight'],
  minimal: ['body weight', 'dumbbell', 'resistance band'],
  gym:     ['barbell', 'dumbbell', 'cable', 'machine'],
};

const mode = 'home'; // from user preference
const workout = await generateWorkout({
  equipment: EQUIPMENT_MODES[mode]
});

PPL / Upper-Lower Split Selector

For users following a structured programme, show a day picker. The user selects "Push Day" and gets a fresh push session each time:

JavaScript — Split day picker
// User is on Day 1 of their Push/Pull/Legs split
const DAY_MAP = {
  'Monday':    'push',
  'Wednesday': 'pull',
  'Friday':    'legs',
};

const today = new Date().toLocaleDateString('en-US', { weekday: 'long' });
const split = DAY_MAP[today] || 'full_body';

const workout = await generateWorkout({ split });

Exclude Already-Done Exercises

If your app tracks which exercises a user performed this week, exclude them from new workouts to guarantee variety. Store completed exercise IDs in localStorage or your backend, then pass them in the exclude parameter:

JavaScript — Exclude recent exercises
// Get IDs of exercises done this week from localStorage
const recentIds = getRecentExerciseIds(); // e.g. ['0025', '0034', '0101']

const freshWorkout = await generateWorkout({
  goal: 'muscle_gain',
  exclude: recentIds,
});

// After workout, save the new IDs
freshWorkout.exercises.forEach(item => {
  markExerciseAsDone(item.exercise.id);
});

Understanding Goal Presets

Each goal maps to a different training prescription. Here's what the API returns for each:

Goal Sets Reps Rest Focus
muscle_gain4 / 36-8 / 8-1090s / 60sCompound-first
strength5 / 43-5 / 4-6120s / 90sCompound-first, heavy
fat_loss312-15 / 15-2030-45sHigh volume, short rest
endurance3 / 215-20 / 20-2520-30sHigh rep, minimal rest
mobility210-12 / 12-1530sIsolation, range of motion

Frequently Asked Questions

Is this actually AI, or just filtering?

The endpoint uses a rule-based exercise programming algorithm, not a large language model. It applies real exercise science principles — periodisation, progressive overload rep ranges, compound-before-isolation ordering — encoded as server-side logic. The result is deterministic fitness programming, not probabilistic text generation. This makes it fast, free of hallucinations, and consistently structured.

Can I cache the response?

Technically yes, but it defeats the purpose. The value of the generator is that every call produces a fresh workout — caching it removes the variety and means users will see the same session repeatedly. For a "Today's Workout" feature, call the API fresh each day or each session.

What if no exercises match my filters?

If the equipment + level combination returns an empty pool, the generator automatically falls back to the unfiltered exercise pool. You'll always get a usable response, never an empty array.

Does each call count as one request?

Yes — one call to /v1/workout/generate counts as one request against your monthly quota. The response includes multiple exercises, but it's a single HTTP request.

Next Steps

Ready to add a workout generator to your app?

Quick test: Try the endpoint live in your browser or with curl using your API key. A full workout plan comes back in under 100ms — no setup, no warmup, no infrastructure.

auto_awesome

Add the AI Workout Generator to your app

Available on Pro ($15.99/month) and Ultra ($24.99/month). Upgrade your existing key or get a new one in 30 seconds.

Get API Key arrow_forward