SDKs / JavaScript
javascript Official SDK · TypeScript-first · API v2
● Live

WorkoutX JavaScript & Node.js SDK

The official JavaScript / TypeScript client for the WorkoutX API. API v2 brings 1,900+ exercises as 2,700+ clips: 720p MP4 animations at 30 fps with male and female models, step-by-step instructions, form tips and anatomical muscle names. The AI Workout Generator, supplements and the Body Scan API come in the same fully typed client, for Node.js and the browser. Zero runtime dependencies.

npm install @workoutx/sdk

✓ Live on npm — npmjs.com/package/@workoutx/sdk · works in Node.js 18+ and modern browsers.

Quick start

Create one client with your API key and use wx.v2. The SDK sends the key in the X-WorkoutX-Key header, the only place v2 accepts it. The same client also covers the v1 resources and the Body Scan API (user JWT).

import { WorkoutX } from "@workoutx/sdk"; const wx = new WorkoutX({ apiKey: process.env.WORKOUTX_API_KEY, // wx_... sent in the X-WorkoutX-Key header scan: { token: "eyJ..." }, // Body Scan (optional) }); const page = await wx.v2.list({ bodyPart: "chest", limit: 10 }); // Render on your server: the key stays here, the signed media links go to the page const html = page.results.map((ex) => ` <h3>${ex.name}</h3> <video src="${ex.media?.mp4}" poster="${ex.media?.poster}" autoplay loop muted playsinline></video>`).join("");
bolt

Fully typed

TypeScript types for every v2 response: ExerciseV2, MediaLinks, V2RankedPage, WorkoutV2 and the rest. Autocomplete in your editor means no more guessing field names.

autorenew

Media links, handled

freshMedia(id) reuses the signed links from earlier calls until about 5 minutes before they expire, then fetches new ones. You store exercise ids; the SDK keeps the links valid.

shield

Resilient by default

Automatic retries with backoff on rate limits (429) and 5xx, honoring Retry-After. A used-up quota is not retried. Structured WorkoutXError with status, code and tips.

Play the animation in the browser

Every v2 exercise has media.mp4, a 600×720 loop at 30 fps (usually 100–300 KB), and media.poster, a JPEG still frame. Play the MP4 muted and looping: browsers only autoplay muted video, and playsinline keeps it in the page on iPhone. On the Free plan both are 300×360 watermarked previews (media.watermarked is true).

<video src="{media.mp4}" poster="{media.poster}" autoplay loop muted playsinline></video>

Media links are signed, expire after about 24 hours and contain no API key, so your server can hand them to the browser. Store exercise ids, never media URLs, and call freshMedia(id) when you show an exercise: it reuses the links the SDK already has and asks the API again only when they're close to expiry.

// Your server (Express here): the browser never sees the API key app.get("/exercises/:id/media", async (req, res) => { const media = await wx.v2.freshMedia(req.params.id); // { mp4, poster, expiresAt, watermarked, mediaVersion } if (!media) return res.status(404).end(); // e.g. Starter's 1,000-exercise limit reached res.json(media); }); // The page const media = await fetch(`/exercises/${id}/media`).then((r) => r.json()); const video = document.querySelector("video"); video.poster = media.poster; video.src = media.mp4;

Downloading the files or serving them from your own servers or CDN isn't permitted on any plan. See Media in v2 for formats and limits.

Male and female models, seven languages

Male and female versions are separate exercises. counterpartId points at the same movement with the other model, so you can offer a switch, and model filters a list to one of them. lang returns names, instructions, tips and muscles in Turkish (tr), German (de), Spanish (es), French (fr), Simplified Chinese (zh-SG) or Traditional Chinese (zh-HK). English is the default; filter values stay in English.

const bench = await wx.v2.get("ex_SAWOOH"); // Barbell Bench Press, male model const female = bench.counterpartId // "ex_HYOSAB": the same movement, female model ? await wx.v2.get(bench.counterpartId) : null; const womenChest = await wx.v2.list({ bodyPart: "chest", model: "female" }); const germanChest = await wx.v2.list({ bodyPart: "chest", lang: "de" }); const spanish = await wx.v2.get("ex_SAWOOH", { lang: "es" });

Smart endpoints

All plans

Similar exercises, alternatives with other equipment and calorie estimates, on v2 ids. Ranked results are full v2 exercises with media links plus a score, and never include the exercise itself or its other-model clip. All three take lang.

const similar = await wx.v2.similar("ex_SAWOOH", { model: "any", limit: 5 }); const swaps = await wx.v2.alternatives("ex_SAWOOH", { equipment: "dumbbell", limit: 3 }); const burn = await wx.v2.calories("ex_SAWOOH", { weightKg: 80, minutes: 20 }); swaps.results[0].name; // "Dumbbell Bench Press" burn.totalKcal; // 139.4

Workouts and programs

Growth · Scale

v2.workout.generate() returns one session from the rule-based workout engine: a warm-up, exercises grouped into blocks (straight sets, supersets such as C1/C2, or one circuit) with sets, reps or a duration, rest, effort (rir) and swap alternatives, then a cool-down. Every exercise in it, warm-up and cool-down items included, is a full v2 exercise with media links, and model picks the male or female clips. avoidJoints (knee, shoulder, lumbar_spine, elbow, wrist, hip, ankle) leaves out movements that load those joints heavily; it is general guidance, not medical advice. format picks straight, superset or circuit (default auto). The response carries estimatedDurationMinutes and the seed it used; pass the seed back to get the same workout. v2.workout.program() returns a schedule, the week-by-week program and an exerciseLibrary that its exercises reference by exerciseId. The generator needs Growth or Scale (legacy Pro, Ultra); multi-week programs need Scale (legacy Ultra).

const workout = await wx.v2.workout.generate({ goal: "muscle_gain", // strength, fat_loss, endurance, mobility, general_fitness duration: 45, // minutes, 15–120 level: "intermediate", equipment: ["dumbbell", "bench"], // or a preset: "home", "bodyweight", "gym" avoidJoints: ["knee"], // general guidance, not medical advice format: "auto", // straight, superset, circuit model: "female", }); for (const item of workout.warmup.items) console.log("Warm-up:", item.exercise.name, `${item.durationSeconds}s`); for (const { block, exercise, sets, reps, durationSeconds, restSeconds } of workout.exercises) { // block "C1" + "C2" = a superset; each item has reps or durationSeconds console.log(block, exercise.name, `${sets} x ${reps ?? durationSeconds + "s"}, ${restSeconds}s rest`, exercise.media?.mp4); } const program = await wx.v2.workout.program({ goal: "strength", weeks: 8, daysPerWeek: 4 }); // Scale const library = new Map(program.exerciseLibrary.map((e) => [e.id, e])); program.schedule[0].name; // "Upper (horizontal push and pull)" for (const x of program.program[0].days[0].exercises) { // week 1, day 1 console.log(x.name, x.sets, x.reps, library.get(x.exerciseId)?.media?.mp4); }

Supplements

Growth · Scale

The evidence-graded supplement database in the v2 page shape, { total, limit, offset, results, disclaimer }. Growth and Scale (legacy Pro, Ultra); stacks need Scale (legacy Ultra). Informational content only, not medical advice. Supplement text is English.

const strong = await wx.v2.supplements.list({ goal: "muscle_gain", evidence: "strong" }); const filters = await wx.v2.supplements.filters(); const creatine = await wx.v2.supplements.get("creatine-monohydrate"); const forBench = await wx.v2.supplements.forExercise("ex_SAWOOH", { limit: 5 }); const stack = await wx.v2.supplements.stack({ goal: "muscle_gain", level: "essential" }); // Scale

Joint & Rehab filters

Growth · Scale

Narrow v2.list() by the joint a movement works through, an intensity band and descriptive tags, for physical therapy, mobility and senior-fitness apps. Growth and Scale (legacy Ultra). These are descriptive fields, not medical recommendations.

const gentleKnee = await wx.v2.list({ jointFocus: "knee", // shoulder, lumbar_spine, … intensityLevel: "gentle", // gentle, moderate, vigorous movementTag: "joint-friendly", // beginner-friendly, low-intensity, minimal-equipment, … limit: 10, });

Migrating from v1

v1 keeps working on the same key, and you'll get at least 30 days' notice before it's retired. The SDK's v1 resources (exercises, gifs, workout, supplements) are unchanged, so you can move one screen at a time. Map each v1 id you've stored once with v2.legacy() and save the v2 id: 1,324 of the 1,327 v1 exercises have a v2 match.

const { match, exercise } = await wx.v2.legacy("0025"); // "exact": ex_SAWOOH, Barbell Bench Press if (match === "exact" || match === "equivalent") { saveExerciseId("0025", exercise.id); // the same movement: replace automatically } else if (match === "alternative" || match === "closest") { suggestToUser("0025", exercise); // a substitute: let the user confirm } // "none": no v2 version yet, keep the v1 exercise const v1 = await wx.exercises.get("0025"); // v1 calls keep working meanwhile

Step by step, with code for each platform: the v2 migration guide.

Errors and plan limits

Every non-2xx response throws a WorkoutXError with status, code, message, tip and the parsed body. A feature your plan doesn't include answers 403 and names the plan that has it. A 429 is either a rate limit, which the SDK retries with backoff, or a used-up request quota, which it doesn't retry. The Free plan's 500 requests are a one-time allowance.

import { WorkoutXError } from "@workoutx/sdk"; try { await wx.v2.workout.generate({ goal: "strength" }); } catch (err) { if (!(err instanceof WorkoutXError)) throw err; if (err.status === 403) { // { error: "Plan Upgrade Required", feature: "workoutGenerator", requiredPlan: "growth" } console.log(`Upgrade to ${err.body.requiredPlan}`); } else if (err.isRateLimited) { console.log(err.code, err.message); // "Monthly Quota Exceeded", "You have used 500/500 requests…" } }

Frequently asked questions

How do I install the WorkoutX JavaScript SDK?

Run npm install @workoutx/sdk (or pnpm/yarn). It works in Node.js 18+ and modern browsers, ships with TypeScript types, and API v2 lives under wx.v2.

How do I play the exercise animations?

Put media.mp4 in a <video> element with autoplay loop muted playsinline and use media.poster as the poster. The MP4s are 600×720 at 30 fps; the Free plan gets 300×360 watermarked previews.

Can I save the media URLs?

No. Media links are signed and expire after about 24 hours. Store the exercise id and call wx.v2.freshMedia(id) when you show the exercise; it reuses links until they're close to expiry. Downloading or self-hosting the files isn't allowed on any plan.

Does it work in the browser as well as Node.js?

Yes. The SDK uses the native fetch API, so it runs in Node.js 18+, Deno, Bun, edge runtimes, and browsers without polyfills. For public web apps, keep the API key on your server and send the page only the media links.

Do my v1 calls keep working?

Yes. wx.exercises, wx.gifs, wx.workout and wx.supplements still call v1, which keeps working; you'll get at least 30 days' notice before it's retired. wx.v2.legacy(v1Id) maps the v1 ids you've stored to v2.

How do I get an API key?

Create a free account in the Developer Portal and copy your wx_... key. Pass it as apiKey when constructing the client. The Free plan includes 500 requests in total with watermarked previews; paid plans start at $19 a month.

How does authentication work for the Body Scan API?

Body Scan uses a logged-in user's JWT. Pass scan: { token }, or scan: { email, password } to let the SDK log in automatically and cache the token.

Security & API keys

vpn_key

Keep keys server-side

In public frontend code, anyone can read your API key. For browser apps, call WorkoutX from your own backend and send the page only the data and media links it needs. v2 accepts the key only in the X-WorkoutX-Key header, which the SDK sets for you.

movie

Media links carry no key

media.mp4 and media.poster are signed links that expire after about 24 hours. They contain no API key, so they're safe to put in a page. Store exercise ids, not links. (v1's gifUrl() still puts the key in the URL.)

lock

Credentials stay on the server

The Body Scan email/password auto-login should only run server-side. In clients, pass a short-lived scan.token obtained from your backend instead.

Start building with JavaScript

Start free with 500 requests and watermarked previews. 1,900+ exercises as 720p video, workouts, and Body Scan in one SDK.

Get your free API key