SDKs/Flutter / Dart
developer_board Official SDK · iOS · Android · Web · Desktop · API v2
Coming Soon

WorkoutX Flutter & Dart SDK

The official Dart package for the WorkoutX API. Ship cross-platform fitness apps from one codebase: API v2's 1,900+ exercises as 2,700+ 720p MP4 animations with male and female models, instructions, form tips and anatomical muscle names, plus the AI Workout Generator, supplements, and the Body Scan API. Runs on Flutter for iOS, Android, web, and desktop.

flutter pub add workoutx_sdk

📦 Publishing to pub.dev shortly — not yet installable. Get your key now and you'll be ready the moment it lands.

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 'package:workoutx_sdk/workoutx_sdk.dart'; final wx = WorkoutX( apiKey: 'wx_your_key_here', // sent in the X-WorkoutX-Key header scan: const ScanAuth.token('eyJ...'), // Body Scan (optional) ); final page = await wx.v2.list({'bodyPart': 'chest', 'limit': 10}); for (final ex in page.results) { print('${ex.name} (${ex.model})'); // Barbell Bench Press (male) print(ex.media?.mp4); // signed 720p MP4 link for video_player }
devices

One codebase, every platform

iOS, Android, web, and desktop from a single Dart package. Typed ExerciseV2, MediaLinks and V2RankedPage models; play media.mp4 with video_player.

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 429 and 5xx, honoring Retry-After. A used-up quota is not retried. Structured WorkoutXError with status, code and tips.

Play the animation with video_player

Every v2 exercise has media.mp4, a 600×720 loop at 30 fps (usually 100–300 KB), and media.poster, a JPEG still frame. Add the plugin with flutter pub add video_player, show the poster while the controller initializes, then play the MP4 muted and looping. Media links are signed and expire after about 24 hours, so store exercise ids, never media URLs, and call freshMedia(id) when the widget appears: it reuses the links the SDK already has and asks the API again only when they're close to expiry.

import 'package:flutter/material.dart'; import 'package:video_player/video_player.dart'; import 'package:workoutx_sdk/workoutx_sdk.dart'; class ExerciseVideo extends StatefulWidget { const ExerciseVideo({super.key, required this.exerciseId}); final String exerciseId; @override State<ExerciseVideo> createState() => _ExerciseVideoState(); } class _ExerciseVideoState extends State<ExerciseVideo> { MediaLinks? _media; VideoPlayerController? _controller; @override void initState() { super.initState(); _load(); } Future<void> _load() async { final media = await wx.v2.freshMedia(widget.exerciseId); if (media == null || !mounted) return; setState(() => _media = media); // poster first final controller = VideoPlayerController.networkUrl(Uri.parse(media.mp4)); await controller.initialize(); await controller.setLooping(true); await controller.setVolume(0); await controller.play(); if (!mounted) { await controller.dispose(); return; } setState(() => _controller = controller); } @override void dispose() { _controller?.dispose(); super.dispose(); } @override Widget build(BuildContext context) { final controller = _controller; final media = _media; return AspectRatio( aspectRatio: 600 / 720, child: controller != null ? VideoPlayer(controller) : media != null ? Image.network(media.poster, fit: BoxFit.cover) : const SizedBox.shrink(), ); } }

In lists, show Image.network(ex.media!.poster) instead of a video. On the Free plan both links are 300×360 watermarked previews (media.watermarked is true).

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.

final bench = await wx.v2.get('ex_SAWOOH'); // Barbell Bench Press, male model final otherId = bench.counterpartId; // 'ex_HYOSAB': the same movement, female model final female = otherId == null ? null : await wx.v2.get(otherId); final womenChest = await wx.v2.list({'bodyPart': 'chest', 'model': 'female'}); final germanChest = await wx.v2.list({'bodyPart': 'chest', 'lang': 'de'}); final 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 ExerciseV2 objects with media links plus a score, and never include the exercise itself or its other-model clip. All three take lang.

final similar = await wx.v2.similar('ex_SAWOOH', model: 'any', limit: 5); final swaps = await wx.v2.alternatives('ex_SAWOOH', equipment: 'dumbbell', limit: 3); final burn = await wx.v2.calories('ex_SAWOOH', weightKg: 80, minutes: 20); for (final r in swaps.results) { print('${r.name} ${r.score}'); // Dumbbell Bench Press 150 } print(burn['totalKcal']); // 139.4

Workouts and programs

Growth · Scale

v2.workout.generate() returns one session from the rule-based workout engine as a Map: 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 also carries estimatedDurationMinutes and the seed it used. 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).

final 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'], // sent comma-separated; or 'home', 'bodyweight', 'gym' 'avoidJoints': ['knee'], // general guidance, not medical advice 'format': 'auto', // straight, superset, circuit 'model': 'female', }); for (final item in workout['warmup']['items'] as List) { print("Warm-up: ${item['exercise']['name']} ${item['durationSeconds']}s"); } for (final item in workout['exercises'] as List) { // block 'C1' + 'C2' = a superset final amount = item['reps'] ?? "${item['durationSeconds']}s"; print("${item['block']} ${item['exercise']['name']}: ${item['sets']} x $amount ${item['exercise']['media']?['mp4']}"); } final program = await wx.v2.workout.program({'goal': 'strength', 'weeks': 8, 'daysPerWeek': 4}); // Scale final library = {for (final e in program['exerciseLibrary'] as List) e['id']: e}; for (final x in program['program'][0]['days'][0]['exercises'] as List) { // week 1, day 1 print("${x['name']} ${x['sets']} x ${x['reps']} ${library[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.

final strong = await wx.v2.supplements.list({'goal': 'muscle_gain', 'evidence': 'strong'}); final filters = await wx.v2.supplements.filters(); final creatine = await wx.v2.supplements.get('creatine-monohydrate'); final forBench = await wx.v2.supplements.forExercise('ex_SAWOOH', limit: 5); final 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.

final 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.

final lookup = await wx.v2.legacy('0025'); // match 'exact': ex_SAWOOH, Barbell Bench Press final match = lookup['match']; final exercise = lookup['exercise'] as Map<String, dynamic>?; if (match == 'exact' || match == 'equivalent') { saveExerciseId('0025', exercise!['id'] as String); // 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 final 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.

try { await wx.v2.workout.generate({'goal': 'strength'}); } on WorkoutXError catch (e) { if (e.status == 403) { // {"error": "Plan Upgrade Required", "feature": "workoutGenerator", "requiredPlan": "growth"} final plan = (e.body as Map?)?['requiredPlan']; print('Upgrade to $plan'); } else if (e.isRateLimited) { print('${e.code}: ${e.message}'); // Monthly Quota Exceeded: You have used 500/500 requests… } }

Frequently asked questions

How do I install the WorkoutX Flutter SDK?

Run flutter pub add workoutx_sdk (or dart pub add workoutx_sdk for a Dart-only project), then import package:workoutx_sdk/workoutx_sdk.dart. API v2 lives under wx.v2; add video_player to play the animations.

Which platforms does it support?

All Flutter targets: iOS, Android, web, macOS, Windows, and Linux. It also works in plain Dart projects. Video playback depends on the player plugin you use; video_player covers Android, iOS, macOS and web.

How do I display an exercise animation in Flutter?

Get links with wx.v2.freshMedia(id) and play media.mp4 with VideoPlayerController.networkUrl, setLooping(true) and setVolume(0). Use Image.network(media.poster) in lists and while the video loads.

Can I store the media URLs?

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

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 does authentication work for the Body Scan API?

Body Scan uses a logged-in user's JWT. Pass scan: ScanAuth.token('...'), or ScanAuth.credentials(email:, password:) to let the SDK log in automatically.

Security & API keys

vpn_key

Keys are visible in apps

Keys compiled into a mobile or web build can be extracted. For production, proxy requests through your own backend, or ship a separate key for the app that you can rotate from the dashboard rather than your main key. 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 video_player and Image.network can load them directly. Store exercise ids, not links. (v1's gifUrl() still puts the key in the URL.)

lock

Use tokens, not credentials

For Body Scan, fetch a short-lived JWT from your backend and pass ScanAuth.token(...). Avoid shipping the email/password login flow inside the app.

Start building with Flutter

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

Get your free API key