Published on July 19, 2026

Users switch streaming services more often than most developers expect. A listener might move from Spotify to Apple Music for better audio quality, try YouTube Music because it comes with their phone plan, or add Tidal for exclusive content. Their playlists, sometimes curated over years, are the hardest thing to leave behind.
Playlist migration features drive real user acquisition for music apps. If your app can move a user's playlists from their old service to their new one, you solve a problem that keeps millions of people locked into platforms they would rather leave.
This post walks through how to build playlist migration using a REST API: the workflow, the code, and the edge cases you need to handle for production-quality results.
Playlist migration is not just a nice-to-have feature. It is a proven growth driver for music applications.
Users accumulate hundreds (sometimes thousands) of tracks in playlists across their streaming history. The cost of rebuilding those playlists manually is high enough that many users stay on a service they no longer prefer. An app that eliminates that switching cost captures users at the exact moment they are most motivated to try something new.
Here is why playlist migration matters from a product perspective:
The technical challenge is real, though. Each streaming service has its own API for reading playlists, its own API for creating them, and its own set of limitations. Building this from scratch for even two services takes weeks. Supporting 12 services takes months.
Every playlist migration follows the same three-step pattern, regardless of the source and destination services.
Fetch the playlist metadata (name, description, cover image) and its full track list from the source service. This requires authenticated access to the user's account on the source platform.
For each track in the source playlist, find the corresponding track on the destination service. This is the hardest step. Track IDs are platform-specific, so you need to match by ISRC, title/artist metadata, or a combination.
Create a new playlist on the destination account and add all matched tracks. Handle failures gracefully: some tracks will not have matches, and the user needs to know which ones were skipped.
| Step | Without MusicAPI | With MusicAPI |
|---|---|---|
| Read source playlist | Service-specific OAuth + endpoint | One auth flow, one endpoint |
| Map tracks | Custom matching logic per service pair | Normalized metadata with ISRCs included |
| Create destination playlist | Service-specific playlist creation API | One create endpoint per service |
| Handle auth tokens | Per-service OAuth refresh, token storage | MusicAPI handles token refresh |
| Services supported | Each new pair = new integration | 12+ services, same code |
Track matching is the step where most playlist migration tools fail or produce poor results. The same song has different IDs on every platform. Your matching strategy determines whether users get a 70% match rate or a 95% match rate.
Use this priority order for the best results:
ISRC match (highest confidence): The International Standard Recording Code is a 12-character identifier assigned to recordings. When both the source and destination track have the same ISRC, you have a confirmed match. Most major label tracks have ISRCs.
Exact title + artist match: Normalize both strings (lowercase, strip special characters) and compare. This catches most tracks where ISRCs are missing.
Fuzzy title + artist + duration match: For tracks where exact matching fails (due to character encoding differences, "feat." vs "ft.", or slightly different titles), use edit distance with a duration check (within 3 seconds) as a tiebreaker.
Search-based matching: As a last resort, search the destination service for the track title and artist, then pick the best result from the search response.
async function matchTrack(sourceTrack, destinationService, userToken) {
// Try ISRC match first (fastest, most reliable)
if (sourceTrack.isrc) {
const isrcResult = await searchByIsrc(sourceTrack.isrc, destinationService, userToken);
if (isrcResult) return { match: isrcResult, method: 'isrc', confidence: 0.95 };
}
// Fall back to metadata search
const query = `${sourceTrack.title} ${sourceTrack.artist}`;
const searchResults = await searchTracks(query, destinationService, userToken);
// Score each result
const scored = searchResults.map(candidate => ({
track: candidate,
score: calculateMatchScore(sourceTrack, candidate)
}));
const best = scored.sort((a, b) => b.score - a.score)[0];
if (best && best.score > 0.7) {
return { match: best.track, method: 'metadata', confidence: best.score };
}
return null; // No match found
}
MusicAPI handles OAuth token refresh, playlist creation, and track matching across 12 services so you do not have to build and maintain per-service integration code. See how the unified auth flow works.
Here is a complete migration flow using MusicAPI. The same code structure works for any source/destination pair.
// Prerequisites:
// 1. User authenticated on both services via MusicAPI OAuth
// See: https://musicapi.com/docs/user-authentication/getting-started
// 2. MusicAPI API token ready
const MUSICAPI_BASE = 'https://api.musicapi.com/v1';
async function migratePlaylist(userToken, playlistId, sourceService, destService) {
// Step 1: Fetch the source playlist and its tracks
const playlistInfo = await fetch(
`${MUSICAPI_BASE}/playlists/${playlistId}`,
{
headers: {
'Authorization': `Bearer ${userToken}`,
'X-Music-Service': sourceService
}
}
).then(r => r.json());
const sourceTracks = await fetch(
`${MUSICAPI_BASE}/playlists/${playlistId}/tracks`,
{
headers: {
'Authorization': `Bearer ${userToken}`,
'X-Music-Service': sourceService
}
}
).then(r => r.json());
console.log(`Source playlist: "${playlistInfo.name}" (${sourceTracks.tracks.length} tracks)`);
// Step 2: Match each track on the destination service
const matchResults = [];
for (const track of sourceTracks.tracks) {
const match = await matchTrack(track, destService, userToken);
matchResults.push({
source: track,
destination: match ? match.match : null,
method: match ? match.method : null,
confidence: match ? match.confidence : 0
});
}
const matched = matchResults.filter(r => r.destination);
const unmatched = matchResults.filter(r => !r.destination);
console.log(`Matched: ${matched.length}/${sourceTracks.tracks.length}`);
console.log(`Unmatched: ${unmatched.length} tracks`);
// Step 3: Create the playlist on the destination service
const newPlaylist = await fetch(
`${MUSICAPI_BASE}/playlists`,
{
method: 'POST',
headers: {
'Authorization': `Bearer ${userToken}`,
'X-Music-Service': destService,
'Content-Type': 'application/json'
},
body: JSON.stringify({
name: playlistInfo.name,
description: `Migrated from ${sourceService}`,
trackIds: matched.map(r => r.destination.serviceId)
})
}
).then(r => r.json());
return {
playlistId: newPlaylist.id,
totalTracks: sourceTracks.tracks.length,
matchedTracks: matched.length,
unmatchedTracks: unmatched.map(r => ({
title: r.source.title,
artist: r.source.artist
}))
};
}
// Usage: Migrate a Spotify playlist to Apple Music
const result = await migratePlaylist(
userToken,
'spotify-playlist-id',
'spotify',
'apple_music'
);
console.log(result);
// {
// playlistId: 'new-apple-music-playlist-id',
// totalTracks: 47,
// matchedTracks: 44,
// unmatchedTracks: [
// { title: 'Rare Indie Track', artist: 'Small Artist' },
// { title: 'Regional Release', artist: 'Local Band' },
// { title: 'Platform Exclusive', artist: 'Signed Artist' }
// ]
// }
The key advantage: this code works for any service pair. Change spotify to tidal and apple_music to youtube_music, and the migration runs the same way. MusicAPI's normalized endpoints handle the per-service differences.
Production-quality playlist migration needs to handle several edge cases that do not show up in happy-path testing.
Some tracks will not have matches on the destination service. Common reasons:
Always return the list of unmatched tracks to the user. Give them the option to search manually for alternatives.
The same track can appear multiple times in a playlist (intentionally). Your migration should preserve duplicate entries. Do not deduplicate playlist contents unless the user explicitly asks.
Also watch for false duplicates in matching: different versions of the same song (live, acoustic, remastered, radio edit) may match to the same destination track. Check duration differences to catch this.
Each platform has different limits on playlist size:
| Platform | Max Tracks per Playlist | Max Playlists |
|---|---|---|
| Spotify | 10,000 | 10,000 |
| Apple Music | 25,000 | No published limit |
| YouTube Music | 5,000 | No published limit |
| Tidal | 10,000 | No published limit |
| Deezer | 2,000 | 2,000 |
| Amazon Music | 500 | No published limit |
If the source playlist exceeds the destination's limit, split it into multiple playlists automatically. Name them with a suffix (e.g., "My Playlist (Part 1)", "My Playlist (Part 2)").
Migrating large playlists means many API calls in sequence. Each track match may require a search request. MusicAPI's centralized rate limiting handles per-service throttling for you. If you are building directly against platform APIs, you need to implement backoff and retry logic for each service independently.
Playlist names and track metadata can contain emoji, non-Latin characters, and special symbols. Ensure your migration preserves these correctly. Test with playlists that have Japanese, Korean, Arabic, and emoji-heavy names.
Here is a checklist for shipping playlist migration as a production feature.
Define the listener-facing screens alongside the transfer implementation with this specification for music library import UX.
Use a playlist migration workflow: read the source playlist and its tracks via API, match each track on the destination service using ISRCs or metadata, then create a new playlist with the matched tracks. MusicAPI provides normalized endpoints for all three steps across 12+ services.
Use ISRC matching as your primary method (95%+ accuracy for major label content). Fall back to exact title + artist matching, then fuzzy matching with duration validation. Search-based matching is a last resort. Combining these methods typically yields a 90-95% match rate across major platforms.
With MusicAPI, yes. The unified API supports 12+ services including Spotify, Apple Music, YouTube Music, Tidal, Deezer, Amazon Music, and more. The same code handles any source/destination pair. You can also migrate playlists from Spotify to any other supported service.
Match rates depend on the service pair and playlist content. Major label content matches at 90-95% between major platforms. Playlists heavy on independent or regional music will have lower match rates (70-85%). Platform exclusives and delisted tracks account for most unmatched content.
For a 50-track playlist, migration typically completes in under 30 seconds. Larger playlists (500+ tracks) may take a few minutes due to per-track matching. Async processing with progress updates is recommended for playlists over 100 tracks. MusicAPI's rate limiting is handled server-side, so you do not need to implement throttling.
Yes. Reading the source playlist requires authenticated access to the user's source account. Creating the playlist on the destination requires authenticated access there too. MusicAPI's unified OAuth flow lets users authenticate both accounts through a single integration.
Return them to the user in a clear list with title and artist information. Best practice is to show unmatched tracks before creating the playlist, letting users decide whether to proceed. Some apps offer manual search for unmatched tracks, letting the user pick an alternative version.
Ready to skip months of OAuth and SDK work? Start your free MusicAPI trial and connect 10+ streaming services with one unified API.