We run an app builder for trainers and gyms. That is our main business. To make it work we filmed our own exercise library, and then something we did not plan happened: people kept asking if they could use our videos in their own apps.

That is how the YMove Exercise API started.

A lot of exercise data out there comes as animated GIFs. Some people like them, and that is fine, it is partly a matter of taste. But we kept hearing the same complaint: they are not accurate. I agree. For a simple bicep curl a drawing is enough. For a Romanian deadlift, a Turkish get-up or a Bulgarian split squat, you want to see a real person do it: where the hips go, how the back stays straight, what the tempo looks like.

So we film everything with real people (physiotherapists, personal trainers and former gymnasts) in a white studio, so every clip has the same look next to the others in your app. No AI-generated movement. 1,413+ exercises so far.

Full disclosure: this is my own API. The patterns below (keeping the key on the server, portrait video, expiring URLs) apply to any exercise video source.

Here is how to go from zero to a page that plays exercise videos and generates a workout. Want to skip ahead? There are complete examples on GitHub: a React Native workout player built with Expo and a Next.js exercise browser. Both run with the free trial key.

A barbell back squat filmed in our studio, playing as a portrait video in a phone app

1. Get a key and keep it on the server

Sign up for a free trial key. Every request takes it as a header:

X-API-Key: your_key_here 

Don't put the key in browser code. Add a tiny proxy instead. Here is one in Express:

// server.js import express from "express"; const app = express(); const BASE = "https://exercise-api.ymove.app/api/v2"; app.get("/api/exercises", async (req, res) => { const params = new URLSearchParams(req.query); const r = await fetch(`${BASE}/exercises?${params}`, { headers: { "X-API-Key": process.env.YMOVE_API_KEY }, }); res.status(r.status).json(await r.json()); }); app.use(express.static("public")); app.listen(3000); 

The same idea works as a Next.js route handler or a Cloudflare Worker.

2. Search exercises

The /exercises endpoint takes filters like muscleGroup, equipment, difficulty and a free-text search:

const res = await fetch("/api/exercises?muscleGroup=glutes&equipment=dumbbell&difficulty=beginner&pageSize=6"); const { data, pagination } = await res.json(); 

Each exercise comes back with what you need to render a card:

{ "title": "Dumbbell Bench Press", "slug": "dumbbell-bench-press", "instructions": ["Lie on a flat bench...", "Press the dumbbells up..."], "importantPoints": ["Keep your feet flat..."], "muscleGroup": "chest", "secondaryMuscles": ["triceps", "shoulders"], "equipment": "dumbbell", "difficulty": "intermediate", "videoUrl": "https://...play_720p.mp4?token=...", "videoHlsUrl": "https://...playlist.m3u8?token=...", "thumbnailUrl": "https://...thumbnail.jpg?token=...", "videos": [{ "tag": "white-background", "orientation": "portrait", "isPrimary": true }] } 

3. Play the video (two gotchas)

Gotcha 1: most clips are portrait. They are filmed for phones, roughly 9:16. If you drop them in a default 16:9 player you get thick black bars and it looks broken. Read orientation and size the box to match:

<div class="clip"> <video id="v" muted loop playsinline autoplay></video> </div> <style> .clip { width: 240px; aspect-ratio: 9 / 16; } .clip video { width: 100%; height: 100%; object-fit: cover; border-radius: 12px; } </style> 
const ex = data[0]; const video = document.getElementById("v"); video.poster = ex.thumbnailUrl; video.src = ex.videoUrl; // MP4 is the simplest; use videoHlsUrl with hls.js for adaptive streaming 

Gotcha 2: video URLs expire after 48 hours. They are signed CDN links. Don't store them in your database. Store the exercise id or slug and fetch fresh data when you show the video.

4. Generate a whole workout

Searching is nice, but most apps really want "give me a 40 minute upper body session". That is one call:

app.get("/api/workout", async (req, res) => { const params = new URLSearchParams({ muscleGroup: "upper_body", difficulty: "beginner", exerciseCount: "5", }); const r = await fetch(`${BASE}/workouts/generate?${params}`, { headers: { "X-API-Key": process.env.YMOVE_API_KEY }, }); res.json(await r.json()); }); 

You get back the exercises in order, each with sets, reps and restSeconds, plus a warmup and a cooldown block and an estimatedMinutes. Render it as a list of the video cards from step 3 and you have a usable workout screen.

There is also /programs/generate for multi-week plans (4, 8 or 12 weeks) if you want to go further.

5. Browse without burning your quota

Plans cap how many distinct exercises you can load per month. While you build your UI, pass includeVideos=false:

fetch("/api/exercises?muscleGroup=core&includeVideos=false") 

You get all the text data and no video fields, and it doesn't count toward the cap. Switch it back on when you need the clips.

6. Four lessons from running this in production

Gyms have bad reception. Basements, concrete walls, thick steel. Your users will open a workout and stare at a spinning loader. What works for us:

  1. When the user opens a workout while still online, download the clips for that session in the background.
  2. Store each file under the exercise id (or the video id), never under the signed URL. The URL changes, the id does not.
  3. During the workout, play the local file first and only stream when there is no local copy.
  4. Log sets, reps and weights locally too, and sync them to your backend once the phone is back online.
  5. Next time the app is online, fetch fresh exercise data and replace any clips that changed.

In a native app you can do this with the platform's file download (for example expo-file-system in React Native). On the web, a service worker with the Cache API does the same job.

Video speed depends on where your users are. If you host the video files yourself, a single server in Europe feels fast in Amsterdam and painfully slow in Bali or Sydney. Put the files behind a CDN with edge locations close to your users. We serve every clip through a worldwide CDN for exactly this reason, and the HLS stream (videoHlsUrl) adapts the quality to a weak connection instead of stalling.

Preload the next exercise during the rest timer. Nothing kills the flow of a workout like a loader between sets. While the user rests, start loading the next clip in the background:

function preloadNext(nextExercise) { const v = document.createElement("video"); v.preload = "auto"; v.muted = true; v.src = nextExercise.videoUrl; } 

When the rest timer ends, the next video starts instantly.

Pick the look that fits your app. Some exercises come in two styles: a clean white-background studio clip and a gym-shot clip. Use videoTag to choose which one you get, so a minimal app gets the studio look and a hardcore lifting app gets the gym:

fetch("/api/exercises?muscleGroup=back&hasVideoGym=true&videoTag=gym-shot") 

Wrapping up

The whole thing is a small proxy, one fetch for search, one for workouts, and a portrait video box. The parts people usually get wrong are the ones that have nothing to do with the API: leaking the key, forcing portrait clips into a landscape player, caching URLs that expire, and forgetting that people train where the signal is bad.

The full React Native example is at ymove-app/ymove-mobile-fitness-expo. If you try it, I'd like to hear what you build. And if you prefer another source for exercise data, the tips above still apply.

P.S. Workouts are only half the picture, so we ended up adding a nutrition API as well: a food database, barcode lookup, recipes and meal plans. That one deserves its own post.