Published on June 24, 2026

Fitness apps live and die by engagement. Users who listen to music during workouts exercise 15% longer on average, and they come back more often. But most fitness apps still treat music as an afterthought: a "connect to Spotify" button that opens a separate app.
The apps winning the retention game do something different. They pull music directly into the workout experience, match track tempo to exercise intensity, and let users bring their own playlists from whichever streaming service they already pay for. That means building real integrations with streaming APIs, not just a deep link.
This post walks through the architecture of a fitness music app that syncs BPM to workout phases, pulls playlists from multiple streaming services, and handles authentication for users across Spotify, Apple Music, and YouTube Music.
The right BPM keeps users in rhythm. A 180 BPM track pushes a runner's cadence. A 70 BPM ambient track guides a cooldown stretch. When tempo matches movement, users report higher motivation and lower perceived effort. Your app can use this by filtering tracks based on beats per minute and serving them at the right moment during a workout.
The core idea: map each workout phase (warmup, high intensity, recovery, cooldown) to a target BPM range, then filter the user's library or playlists to find tracks that fit.
| Workout Phase | Target BPM Range | Example Genres |
|---|---|---|
| Warmup | 100-120 BPM | Pop, indie, light electronic |
| Moderate cardio | 120-140 BPM | Dance, house, pop-rock |
| High intensity / sprint | 140-180 BPM | Drum & bass, hardstyle, fast hip-hop |
| Strength training | 100-130 BPM | Hip-hop, rock, trap |
| Cooldown / stretching | 60-80 BPM | Ambient, lo-fi, acoustic |
Most streaming services expose audio features or track metadata that includes tempo (BPM), energy, danceability, and valence. Your app can use these values to sort and filter tracks programmatically.
Here is how you would filter a list of tracks by a target BPM range:
// Filter tracks by BPM range for a specific workout phase
function filterTracksByBPM(tracks, minBPM, maxBPM) {
return tracks.filter(track => {
const bpm = track.audioFeatures?.tempo || track.bpm;
return bpm >= minBPM && bpm <= maxBPM;
});
}
// Map workout phases to BPM ranges
const workoutPhases = {
warmup: { min: 100, max: 120 },
moderate: { min: 120, max: 140 },
highIntensity: { min: 140, max: 180 },
strength: { min: 100, max: 130 },
cooldown: { min: 60, max: 80 },
};
// Get tracks for the current workout phase
function getTracksForPhase(allTracks, phase) {
const range = workoutPhases[phase];
return filterTracksByBPM(allTracks, range.min, range.max);
}
The key challenge: getting this metadata consistently across streaming services. Each platform returns audio features in a different format, with different field names and different levels of detail. One service calls it tempo, another calls it bpm, and a third buries it inside an audio_features object. Normalizing this across providers is where most of the integration pain lives.
Your users do not all use the same streaming service. Roughly 30% use Spotify, 25% use Apple Music, and the rest split across YouTube Music, Tidal, Deezer, and Amazon Music. A fitness app that only supports one service loses up to 70% of potential users at the music connection step.
The solution: let users connect whichever service they already use, then pull their playlists through a single integration layer. This is where a unified music API saves months of development. Instead of building and maintaining separate integrations for each streaming service, you make one API call and get normalized playlist data back regardless of the source.
Here is a practical example of fetching a user's playlists from their connected streaming service via MusicAPI, then filtering tracks by BPM for a workout:
// Fetch user's playlists via MusicAPI (works for any connected service)
async function getUserWorkoutTracks(musicApiToken, targetBPM, bpmTolerance = 10) {
// Step 1: Get the user's playlists
const playlistsResponse = await fetch(
'https://api.musicapi.com/api/v1/playlists',
{
headers: { 'Authorization': `Bearer ${musicApiToken}` }
}
);
const playlists = await playlistsResponse.json();
// Step 2: Get tracks from each playlist
const allTracks = [];
for (const playlist of playlists.data) {
const tracksResponse = await fetch(
`https://api.musicapi.com/api/v1/playlists/${playlist.id}/tracks`,
{
headers: { 'Authorization': `Bearer ${musicApiToken}` }
}
);
const tracks = await tracksResponse.json();
allTracks.push(...tracks.data);
}
// Step 3: Filter by target BPM range
const minBPM = targetBPM - bpmTolerance;
const maxBPM = targetBPM + bpmTolerance;
return allTracks.filter(track => {
const bpm = track.tempo || track.bpm;
return bpm >= minBPM && bpm <= maxBPM;
});
}
// Usage: Get tracks around 140 BPM for a high-intensity interval
const sprintTracks = await getUserWorkoutTracks(userToken, 140, 15);
import requests
def get_user_workout_tracks(musicapi_token, target_bpm, bpm_tolerance=10):
headers = {'Authorization': f'Bearer {musicapi_token}'}
# Step 1: Get user's playlists
playlists = requests.get(
'https://api.musicapi.com/api/v1/playlists',
headers=headers
).json()
# Step 2: Collect tracks from all playlists
all_tracks = []
for playlist in playlists['data']:
tracks = requests.get(
f"https://api.musicapi.com/api/v1/playlists/{playlist['id']}/tracks",
headers=headers
).json()
all_tracks.extend(tracks['data'])
# Step 3: Filter by BPM range
min_bpm = target_bpm - bpm_tolerance
max_bpm = target_bpm + bpm_tolerance
return [
track for track in all_tracks
if min_bpm <= (track.get('tempo') or track.get('bpm', 0)) <= max_bpm
]
# Get tracks near 140 BPM for sprint intervals
sprint_tracks = get_user_workout_tracks(user_token, 140, 15)
This code works the same whether the user connected Spotify, Apple Music, YouTube Music, or any other supported service. MusicAPI normalizes the response format, so you write the filtering logic once.
Authentication is the hardest part of multi-service music integration. Each streaming platform uses OAuth 2.0, but every one implements it differently. Token lifetimes vary. Refresh flows differ. Scopes are named inconsistently. And your fitness app needs to handle all of this invisibly while a user is mid-workout on a gym treadmill.
Here is what building auth in-house looks like for three services:
Each service requires its own callback URL, its own token storage, its own refresh logic, and its own error handling. When a token expires mid-workout, your app needs to refresh it silently without interrupting playback. Multiply that by three (or more) services and you have a significant maintenance burden.
MusicAPI handles this with a single authentication flow. Your app redirects the user to one auth endpoint. MusicAPI manages the OAuth exchange with whatever streaming service the user picks. You get back a single token that works across all services. Token refresh happens automatically on MusicAPI's side.
// Initialize auth for any streaming service through MusicAPI
// One flow handles Spotify, Apple Music, YouTube Music, and more
// Step 1: Start the auth flow
const authResponse = await fetch(
'https://api.musicapi.com/api/v1/auth/initialize',
{
method: 'POST',
headers: {
'Authorization': `Bearer ${YOUR_MUSICAPI_KEY}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
redirectUri: 'https://yourfitnessapp.com/callback',
service: userSelectedService // 'spotify', 'apple', 'youtube', etc.
})
}
);
const { authUrl } = await authResponse.json();
// Step 2: Redirect user to authUrl (they pick their service and log in)
// Step 3: Handle the callback - MusicAPI gives you a unified token
// No per-service token management needed
This cuts weeks of OAuth implementation down to a single integration. For the full auth setup, see the authentication getting started guide and callback handling docs.
Building a fitness app that connects to multiple streaming services? MusicAPI handles OAuth, token refresh, and response normalization for 10+ services through one API. Skip the per-platform auth headaches and focus on your workout features. Start your free trial.
A static playlist does not match a dynamic workout. When a user transitions from warmup to sprint intervals, the music should shift with them. This requires an event-driven architecture that reacts to workout state changes in real time.
Here is the pattern:
[Workout Engine] --phase_change--> [Music Controller] --filter/queue--> [Playback Manager]
| | |
Tracks phases, Filters tracks by Handles crossfade,
heart rate, BPM/energy for gapless playback,
user input current phase queue management
The workout engine emits events when the exercise phase changes. The music controller listens for these events, queries the pre-fetched track pool for songs matching the new phase's BPM target, and queues them in the playback manager. The playback manager handles crossfading between tracks so transitions feel smooth.
// Event-driven workout music controller
class WorkoutMusicController {
constructor(trackPool) {
this.trackPool = trackPool; // Pre-fetched and indexed by BPM
this.currentPhase = null;
}
onPhaseChange(newPhase) {
this.currentPhase = newPhase;
const range = workoutPhases[newPhase];
// Filter tracks for the new phase
const phaseTracks = this.trackPool.filter(track => {
const bpm = track.tempo || track.bpm;
return bpm >= range.min && bpm <= range.max;
});
// Shuffle and queue the filtered tracks
const shuffled = this.shuffle(phaseTracks);
this.playbackManager.crossfadeTo(shuffled[0]);
this.playbackManager.setQueue(shuffled.slice(1));
}
shuffle(tracks) {
const arr = [...tracks];
for (let i = arr.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[arr[i], arr[j]] = [arr[j], arr[i]];
}
return arr;
}
}
// Usage with workout engine events
workoutEngine.on('phaseChange', (phase) => {
musicController.onPhaseChange(phase);
});
The critical performance detail: fetch and index all tracks at workout start, not during phase transitions. Hitting the API mid-sprint adds latency the user will feel. Pre-fetch the user's playlists via MusicAPI, extract audio features, and build a BPM-indexed map before the first rep.
Before you start coding, the build-vs-buy decision shapes everything. Here is a direct comparison of building multi-service music integration yourself versus using a unified API like MusicAPI.
| Capability | Build In-House | Unified API (MusicAPI) |
|---|---|---|
| Services supported | Each service added individually (weeks per service) | 10+ services available immediately |
| OAuth implementation | Separate flows per service, custom token storage and refresh | Single auth flow, automatic token management |
| Response normalization | You build and maintain mapping layers per service | Normalized responses across all services |
| Playlist operations | Different endpoints, schemas, and pagination per service | One endpoint for playlists, tracks, and user data |
| Rate limit handling | Monitor and respect per-service limits individually | Handled at the API layer |
| API changes and deprecations | You track and fix breaking changes per service | MusicAPI absorbs upstream changes |
| Time to first integration | 2-4 weeks per service | Hours for all services |
| Ongoing maintenance | Significant (auth changes, API versioning, new scopes) | Minimal (one SDK to update) |
| Supported features | Only what you build | Full feature matrix across services |
For a fitness app, the math is straightforward. Your competitive advantage is workout intelligence, BPM matching, and user engagement. It is not OAuth token rotation for six streaming services. Every week spent on streaming API plumbing is a week not spent on the features that differentiate your app.
If you want to create workout playlists programmatically, check out the playlist creation endpoints for each service. If you need to read a user's existing playlists to build a BPM-filtered library, the get user playlists and get playlist tracks pages show exactly what the API returns.
For a deeper look at building playlist generators with MusicAPI, read our post on how to build a playlist generator with MusicAPI.
Research and user data point to clear BPM sweet spots. Warmups work well at 100-120 BPM. Moderate cardio (jogging, cycling) fits 120-140 BPM. High-intensity intervals and sprints land between 140-180 BPM. Strength training varies more (100-130 BPM depending on exercise pace). Cooldowns and stretching work best with 60-80 BPM ambient or acoustic tracks.
Most streaming platforms provide audio analysis or track metadata endpoints that include tempo (BPM), energy, danceability, and other audio features. The challenge is that each service returns this data in different formats. A unified API like MusicAPI normalizes these responses so you can filter by audio features consistently regardless of which service the user connected.
Yes. The two main approaches are: build separate integrations for each service (handling different OAuth flows, API schemas, and token management independently) or use a unified music API. MusicAPI supports 10+ streaming services through a single integration. You write the playlist and track fetching code once, and it works for Spotify, Apple Music, YouTube Music, Tidal, Deezer, and more. The authentication flow is also unified, so users connect their preferred service through one consistent experience.
Token expiration mid-workout is a common failure point. Spotify tokens expire every hour. Apple Music user tokens last longer but still need renewal. The safest approach is proactive token refresh: check token validity before each API call and refresh silently in the background. If you use MusicAPI, token management is handled automatically. Your app receives a single session token, and MusicAPI refreshes the underlying service tokens on its side without interrupting the user's workout.
Pre-fetch. Loading tracks on demand during phase transitions introduces network latency that users will notice, especially in a gym environment with spotty WiFi. The recommended pattern is to pull the user's full playlist library at workout start, extract audio features (BPM, energy), build an in-memory index grouped by BPM range, and then filter locally when phases change. This keeps phase transitions instant and reduces API calls during the session.
If a user cancels their Spotify subscription and moves to YouTube Music, your app needs to re-authenticate them with the new service. With per-service integrations, this means building an entirely new auth flow and updating your API calls. With MusicAPI, the user simply re-authenticates through the same unified flow, picks their new service, and your existing code works without changes. Playlist structures, track metadata, and all API responses follow the same normalized format regardless of the underlying service.
Use the playlist creation endpoints to build playlists on the user's connected streaming service. Your app can analyze the user's existing library for BPM-appropriate tracks, group them by workout phase, and create named playlists (like "Sprint Intervals 140-160 BPM" or "Cooldown Mix"). The user then has these playlists available in their streaming app even outside your fitness app. Check out our guide on building a playlist generator for a full walkthrough.
Ready to skip months of OAuth and SDK work? Start your free MusicAPI trial and connect 10+ streaming services with one unified API.