Published on July 18, 2026

----|-----------|-----------|-----------|
| Track title | track.name | items[].track.title | data.attributes.name |
| Artist name | track.artists[0].name | items[].track.artist | data.relationships.artists |
| Album art | track.album.images[0].url | items[].track.thumbnail | data.attributes.artwork.url |
| Duration | track.duration_ms (ms) | items[].track.length (s) | data.attributes.durationInMillis (ms) |
| Unique ID | track.id | items[].track.videoId | data.id |
You end up writing and maintaining a separate adapter for each platform. When any service updates its API, your adapter breaks.
Each platform uses OAuth 2.0, but the implementations vary. Scopes differ. Token lifetimes differ. Refresh flows differ. Some require PKCE. Others need server-to-server exchanges. Managing auth for even three services means building three separate OAuth integrations with distinct callback URLs, token storage, and refresh logic.
At scale, you're running a small identity provider just to keep tokens fresh. That's before you write a single line of library-fetching code. MusicAPI's authentication system handles OAuth and token management for all supported services through a single integration point, so you never touch platform-specific auth flows.
A solid aggregator has three layers: an authentication layer that connects users to their streaming accounts, a data-fetching layer that pulls library content from each service, and a normalization layer that maps everything into a single schema.
Here's the unified data model your app should target:
| Unified Field | Type | Description |
|---|---|---|
id | string | Platform-specific track ID |
isrc | string | International Standard Recording Code (cross-platform identifier) |
title | string | Track title |
artist | string | Primary artist name |
album | string | Album name |
albumArt | string (URL) | Cover art URL |
durationMs | number | Duration in milliseconds |
service | string | Source platform (e.g., "spotify", "apple", "youtube") |
addedAt | string (ISO 8601) | When the user saved the track |
The ISRC field is critical. It's the universal identifier that lets you match the same song across different platforms. We'll cover deduplication strategies in a later section.
With MusicAPI's supported services, your data-fetching layer talks to one API instead of building separate integrations for each platform. The responses already follow a consistent format, so the normalization layer shrinks from hundreds of lines per platform to a thin mapping function.
Let's build the core of the aggregator. After a user authenticates their streaming accounts through MusicAPI, you can fetch their libraries with straightforward API calls.
const MUSICAPI_BASE = 'https://api.musicapi.com';
const API_KEY = process.env.MUSICAPI_KEY;
async function getUserPlaylists(userToken, service) {
const response = await fetch(
`${MUSICAPI_BASE}/user/playlists?service=${service}`,
{
headers: {
'Authorization': `Bearer ${API_KEY}`,
'X-User-Token': userToken
}
}
);
return response.json();
}
// Fetch playlists from all connected services
async function aggregatePlaylists(userToken, services) {
const results = await Promise.all(
services.map(service => getUserPlaylists(userToken, service))
);
return results.flatMap((result, index) =>
result.playlists.map(playlist => ({
...playlist,
service: services[index]
}))
);
}
// Usage
const connectedServices = ['spotify', 'apple', 'youtube'];
const allPlaylists = await aggregatePlaylists(userToken, connectedServices);
console.log(`Found ${allPlaylists.length} playlists across ${connectedServices.length} services`);
Each call to the get-user-playlists endpoint returns a normalized response regardless of the source platform. The same code works for Apple Music and every other supported service with zero changes.
async function getFavoriteTracks(userToken, service) {
const response = await fetch(
`${MUSICAPI_BASE}/user/favorites?service=${service}`,
{
headers: {
'Authorization': `Bearer ${API_KEY}`,
'X-User-Token': userToken
}
}
);
return response.json();
}
async function aggregateFavorites(userToken, services) {
const results = await Promise.all(
services.map(service => getFavoriteTracks(userToken, service))
);
return results.flatMap((result, index) =>
result.tracks.map(track => ({
id: track.id,
isrc: track.isrc,
title: track.title,
artist: track.artist,
album: track.album,
albumArt: track.albumArt,
durationMs: track.durationMs,
service: services[index],
addedAt: track.addedAt
}))
);
}
const allFavorites = await aggregateFavorites(userToken, connectedServices);
The get-favorite-tracks endpoint returns liked songs from any connected platform. Notice that MusicAPI already normalizes the response shape: track.title, track.artist, and track.isrc are consistent fields across YouTube, Apple Music, and every other service.
MusicAPI handles the OAuth flows, token refresh, and response normalization that would otherwise take months to build per platform. One API key, one auth flow, one response format. Your aggregator code stays clean and maintainable. Start a free trial to see it in action.
async function getPlaylistTracks(userToken, service, playlistId) {
const response = await fetch(
`${MUSICAPI_BASE}/playlist/tracks?service=${service}&playlistId=${playlistId}`,
{
headers: {
'Authorization': `Bearer ${API_KEY}`,
'X-User-Token': userToken
}
}
);
return response.json();
}
Use the get-playlist-tracks endpoint to pull individual playlist contents when a user wants to drill into a specific playlist.
Users who listen on multiple platforms inevitably have the same songs saved in different places. Your aggregator needs to detect and merge these duplicates.
The International Standard Recording Code (ISRC) is a 12-character identifier assigned to individual recordings. Most major streaming services expose ISRC data in their track metadata, and MusicAPI includes it in normalized responses.
function deduplicateTracks(tracks) {
const isrcMap = new Map();
for (const track of tracks) {
if (track.isrc) {
if (isrcMap.has(track.isrc)) {
// Track exists on multiple services; merge service info
const existing = isrcMap.get(track.isrc);
existing.services.push(track.service);
existing.serviceIds.push({ service: track.service, id: track.id });
} else {
isrcMap.set(track.isrc, {
...track,
services: [track.service],
serviceIds: [{ service: track.service, id: track.id }]
});
}
} else {
// No ISRC available; fall back to title + artist matching
const key = `${track.title.toLowerCase()}::${track.artist.toLowerCase()}`;
if (isrcMap.has(key)) {
const existing = isrcMap.get(key);
existing.services.push(track.service);
existing.serviceIds.push({ service: track.service, id: track.id });
} else {
isrcMap.set(key, {
...track,
services: [track.service],
serviceIds: [{ service: track.service, id: track.id }]
});
}
}
}
return Array.from(isrcMap.values());
}
ISRC matching catches exact recordings. A track saved on three different platforms will share the same ISRC, making deduplication deterministic.
ISRC handles track-level deduplication, but albums and artists need their own normalization. Artist names appear differently across platforms: "The Beatles" vs "Beatles, The" vs "Beatles". Album titles include region-specific suffixes like "(Deluxe Edition)" or "[Remastered]".
function normalizeArtistName(name) {
return name
.replace(/^the\s+/i, '')
.replace(/\s+/g, ' ')
.trim()
.toLowerCase();
}
function normalizeAlbumTitle(title) {
return title
.replace(/\s*[\(\[](deluxe|remaster|bonus|expanded|anniversary|special).*?[\)\]]/gi, '')
.replace(/\s+/g, ' ')
.trim()
.toLowerCase();
}
These functions strip common variations so you can group tracks by normalized album and artist. Use them as secondary matching criteria when ISRC data is unavailable.
Power users accumulate thousands of saved tracks across multiple platforms. Your aggregator needs to handle large datasets without choking on API limits or memory.
Pagination. MusicAPI endpoints return paginated responses. Always iterate through all pages rather than assuming the first page contains everything.
async function fetchAllPages(userToken, service, endpoint) {
let allItems = [];
let cursor = null;
do {
const url = new URL(`${MUSICAPI_BASE}${endpoint}`);
url.searchParams.set('service', service);
if (cursor) url.searchParams.set('cursor', cursor);
const response = await fetch(url, {
headers: {
'Authorization': `Bearer ${API_KEY}`,
'X-User-Token': userToken
}
});
const data = await response.json();
allItems = allItems.concat(data.items);
cursor = data.nextCursor;
} while (cursor);
return allItems;
}
Caching. Cache library data with a reasonable TTL (15 to 30 minutes for active sessions). Users don't add songs every second, and caching reduces API calls significantly. Store the cache key as a combination of user token, service, and endpoint.
Rate limit handling. MusicAPI provides rate limiting documentation that covers per-endpoint limits. Use exponential backoff when you hit limits, and stagger requests across services rather than firing all calls simultaneously.
async function fetchWithBackoff(url, options, maxRetries = 3) {
for (let attempt = 0; attempt <= maxRetries; attempt++) {
const response = await fetch(url, options);
if (response.status === 429) {
const retryAfter = parseInt(response.headers.get('Retry-After') || '1', 10);
await new Promise(resolve => setTimeout(resolve, retryAfter * 1000 * (attempt + 1)));
continue;
}
return response;
}
throw new Error('Max retries exceeded');
}
Parallel fetching with concurrency limits. Fire requests to different services in parallel, but cap concurrency to avoid overwhelming your server or the API.
async function fetchWithConcurrency(tasks, limit = 3) {
const results = [];
const executing = new Set();
for (const task of tasks) {
const promise = task().then(result => {
executing.delete(promise);
return result;
});
executing.add(promise);
results.push(promise);
if (executing.size >= limit) {
await Promise.race(executing);
}
}
return Promise.all(results);
}
A music library API lets you programmatically access a user's saved music (playlists, liked tracks, albums) from streaming platforms. MusicAPI provides a unified interface that works across 10+ services with one integration.
Yes. With a unified API like MusicAPI, you authenticate users once per service, then fetch their library data through standardized endpoints. The API handles the differences between platforms so your code stays consistent.
The most reliable method is ISRC (International Standard Recording Code) matching. Each recording has a unique ISRC that stays the same across platforms. For tracks without ISRC data, fall back to normalized title and artist string matching.
MusicAPI supports 10+ streaming services including major platforms. Each service uses the same API endpoints and response format, so adding a new service to your aggregator requires zero code changes.
Instead of building separate OAuth implementations for each platform, use MusicAPI's unified authentication flow. Users connect their accounts through a single auth process, and MusicAPI manages token storage, refresh, and scope handling for all services.
Yes. All APIs have rate limits. MusicAPI documents its rate limiting policies per endpoint. Use pagination, caching, and exponential backoff to handle large libraries efficiently without hitting limits.
Absolutely. Once you've aggregated and deduplicated a user's library, you can use MusicAPI's playlist creation endpoints to write playlists back to any connected service. This enables cross-platform playlist syncing.
Ready to skip months of OAuth and SDK work? Start your free MusicAPI trial and connect 10+ streaming services with one unified API.