Published on July 10, 2026

Every streaming service describes the same song differently. Field names, data formats, ID systems, and genre labels all vary from one platform to the next. There is no shared metadata standard across the music streaming industry. If you are building an app that pulls data from multiple services, you will spend more time wrangling inconsistent metadata than building your actual product.
This guide breaks down exactly where metadata diverges, what a normalized schema looks like, and how to stop writing mapping code for every new service you integrate.
No two streaming services agree on how to describe a track. Field names differ, duration formats conflict, and even basic attributes like artist names come back in incompatible structures. The root cause: each platform built its API independently, with no shared specification to follow.
Here is how four major services return metadata for the same track:
| Field | Service A | Service B | Service C | Service D |
|---|---|---|---|---|
| Track title field | name | attributes.name | title | title |
| Artist name | artists[].name | attributes.artistName (string) | snippet.channelTitle | artist.name |
| Album art | album.images[].url | attributes.artwork.url (template) | snippet.thumbnails.high.url | album.cover_xl |
| Duration | duration_ms (integer, ms) | attributes.durationInMillis (integer, ms) | contentDetails.duration (ISO 8601) | duration (integer, seconds) |
| Track ID format | Base62 string | i. prefix + numeric | 11-char alphanumeric | Numeric integer |
| Genre | Not on track object | attributes.genreNames[] | topicDetails.topicCategories[] (URLs) | album.genres.data[].name |
| ISRC | external_ids.isrc | attributes.isrc | Not available | isrc |
| Explicit flag | explicit (boolean) | attributes.contentRating (string) | Not available | explicit_lyrics (boolean) |
That is eight common fields, and not a single one matches across all four services. Your code has to account for every difference.
A normalized metadata schema gives you one predictable shape for every track, regardless of which streaming service it came from. You define canonical field names, consistent types, and standard units. Then every service's response maps into that single structure.
Here is a practical normalized track object:
{
"id": "musicapi_track_abc123",
"sourceService": "spotify",
"sourceId": "4iV5W9uYEdYUVa79Axb7Rh",
"title": "Yesterday",
"artists": [
{
"name": "The Beatles",
"id": "musicapi_artist_xyz789",
"sourceId": "3WrFJ7ztbogyGnTHbHJFl2"
}
],
"album": {
"name": "Help!",
"id": "musicapi_album_def456",
"sourceId": "2BtE7MPixgBsA8OGqveOqR",
"artworkUrl": "https://example.com/artwork/600x600.jpg",
"artworkWidth": 600,
"artworkHeight": 600
},
"durationMs": 125000,
"isrc": "GBAYE0601498",
"explicit": false,
"genres": ["Rock", "Classic Rock"],
"trackNumber": 13,
"discNumber": 1
}
Every field uses a consistent name and type. Duration is always in milliseconds. Artists are always an array of objects (never a flat string). Artwork is always a direct URL (never a template). This consistency means your application code never has to ask "which service did this come from?" before accessing a field.
Some fields are straightforward to normalize. Others require significant effort because services represent the same concept in fundamentally different ways.
Most services return clean track titles, but some append extra information. You will find titles like "Yesterday - Remastered 2009" or "Yesterday (Live at the BBC)" where the version info is part of the title string rather than a separate field. Other services split this into title and subtitle or contentVersion fields. Your normalization layer needs rules for whether to preserve or strip these suffixes.
This is where things get painful. Some services return artists as an array of objects, each with a name and ID. Others return a single concatenated string: "Artist A, Artist B & Artist C". A few use a primaryArtist field plus a separate featuredArtists array. Normalizing artist data means parsing concatenated strings, handling delimiter variations (commas, ampersands, "feat.", "ft.", "with"), and deduplicating artists who appear under slightly different names.
One service returns an array of image objects at fixed sizes (64px, 300px, 640px). Another returns a URL template with {w}x{h} placeholders you must replace at render time. A third returns a single URL at one fixed resolution. Your normalization layer needs to resolve templates, pick the best available resolution, and output a direct, usable URL.
ISRCs are the closest thing to a universal track identifier across services. But not every service exposes them on every endpoint. Some only include ISRCs in "detailed" track responses (requiring an extra API call). Others do not surface ISRCs at all. And when ISRCs are present, the same recording can have multiple ISRCs across regions or reissues. Matching tracks across services by ISRC works about 85% of the time; the remaining 15% requires fuzzy matching on title, artist, and duration.
Three different units across four services: milliseconds (integer), seconds (integer), and ISO 8601 duration strings ("PT2M5S"). Your normalization layer must detect the format and convert to a single unit. Milliseconds is the most common choice because it avoids floating-point rounding issues.
Genre data is the least standardized field. One service uses a flat array of human-readable genre names. Another uses Wikipedia category URLs. A third skips genres entirely on track objects and only associates them with albums. Even when two services both use genre name strings, their taxonomies differ: one calls it "Hip-Hop/Rap" while another splits it into "Hip Hop" and "Rap" as separate entries. Building a genre mapping table that stays accurate requires ongoing maintenance.
You can absolutely build your own normalization layer. Here is what a manual normalizer looks like for just two services:
function normalizeTrack(rawTrack, service) {
if (service === 'serviceA') {
return {
title: rawTrack.name,
artists: rawTrack.artists.map(a => ({
name: a.name,
id: a.id,
})),
durationMs: rawTrack.duration_ms,
albumArt: rawTrack.album.images[0]?.url || null,
isrc: rawTrack.external_ids?.isrc || null,
explicit: rawTrack.explicit,
};
}
if (service === 'serviceB') {
const attrs = rawTrack.attributes;
return {
title: attrs.name,
artists: [{
name: attrs.artistName, // Single string, not an array
id: null, // Not available on this endpoint
}],
durationMs: attrs.durationInMillis,
albumArt: attrs.artwork.url
.replace('{w}', '600')
.replace('{h}', '600'),
isrc: attrs.isrc || null,
explicit: attrs.contentRating === 'explicit',
};
}
// Service C, D, E, F... each needs its own block
throw new Error(`Unsupported service: ${service}`);
}
This function handles two services. Now multiply by 10+ services and 15+ metadata fields. You are looking at hundreds of lines of mapping code, each branch with its own edge cases.
The real cost is not the initial build. It is maintenance. Streaming services update their APIs regularly. Field names change, new fields appear, deprecated fields disappear, and response structures shift between API versions. Every change requires you to update your normalizer, test it against real data, and deploy. For a team of two or three developers, this maintenance work competes directly with feature development.
If you would rather spend your engineering time on your actual product, MusicAPI handles normalization automatically across all supported services. One integration, one response schema, zero mapping code.
MusicAPI sits between your application and 12+ streaming services. It handles authentication, pagination, rate limiting, and metadata normalization in a single layer. Every response follows the same schema, regardless of which service the data came from.
Here is a raw response from one service's track endpoint:
{
"track": {
"name": "Blinding Lights",
"artists": [
{
"id": "1Xyo4u8uXC1ZmMpatF05PJ",
"name": "The Weeknd",
"type": "artist"
}
],
"album": {
"name": "After Hours",
"images": [
{ "url": "https://i.scdn.co/image/abc123", "height": 640, "width": 640 },
{ "url": "https://i.scdn.co/image/def456", "height": 300, "width": 300 },
{ "url": "https://i.scdn.co/image/ghi789", "height": 64, "width": 64 }
]
},
"duration_ms": 200040,
"explicit": false,
"external_ids": { "isrc": "USUG11904201" },
"popularity": 87,
"track_number": 9,
"disc_number": 1,
"type": "track"
}
}
And here is the same track through MusicAPI's normalized response:
{
"type": "track",
"name": "Blinding Lights",
"artists": ["The Weeknd"],
"album": {
"name": "After Hours",
"images": [
{
"url": "https://i.scdn.co/image/abc123",
"height": 640,
"width": 640
}
]
},
"durationMs": 200040,
"isrc": "USUG11904201",
"explicit": false,
"sourceId": "0VjIjW4GlUZAMYd2vXMi3b",
"service": "spotify"
}
The same schema works for tracks from any other service. Your application code reads name, artists, durationMs, and isrc without caring whether the source was a service that uses duration_ms, durationInMillis, or PT3M20S. MusicAPI's normalization layer handles all the field mapping, type conversion, template resolution, and edge cases documented in this article.
Check how this works with real endpoints like Get Playlist Info or Get Favorite Tracks to see normalized responses in action.
Normalized metadata is not just a nice-to-have. Several common product features break without it.
Cross-platform playlist sync. When a user moves a playlist from one service to another, you need to match tracks across platforms. Without normalized ISRCs, titles, and artist names, your matching algorithm produces false positives (wrong track) or false negatives (track not found). Clean metadata improves match rates from roughly 70% to 95%+.
Recommendation engines. Recommendation systems rely on consistent genre labels, artist relationships, and track attributes. If the same track has different genre classifications depending on its source service, your recommendations degrade. Normalized genre taxonomies keep your models accurate across data sources.
Analytics dashboards. Streaming analytics for labels, distributors, and artist teams aggregate data from multiple platforms. If duration formats differ between sources, play-count calculations skew. If artist names are inconsistent ("The Weeknd" vs. "the weeknd" vs. "Weeknd, The"), your dashboards split what should be a single artist into multiple entries.
Music discovery features. Apps that surface new music based on listening history need clean, consistent track metadata to avoid showing duplicates. Without normalization, the same song from two services shows up as two different tracks in the user's feed.
Normalizing music metadata means converting track, artist, album, and playlist data from multiple streaming services into a single, consistent schema. Instead of handling different field names (name vs. title), different duration formats (milliseconds vs. seconds vs. ISO 8601), and different artist structures (arrays vs. strings), you map everything into one predictable format your application can rely on.
Each streaming platform built its API independently, at different times, with different engineering teams and different priorities. There is no industry-wide metadata standard that all services follow. Some services prioritized REST conventions, others chose GraphQL, and each made different decisions about field naming, nesting depth, and data types. The result: every API returns the same information in a different shape.
ISRCs (International Standard Recording Codes) match tracks correctly about 85% of the time. The remaining 15% fails because the same recording can have multiple ISRCs across regions, reissues, or label transfers. Some services do not expose ISRCs at all on certain endpoints. For production-grade cross-platform matching, combine ISRC lookup with fuzzy matching on title, artist name, and duration as a fallback.
MusicAPI provides a single API layer that connects to 12+ streaming services and returns every response in a unified schema. It handles field mapping, type conversion, artwork URL resolution, and edge cases automatically. You write one integration against MusicAPI's endpoints and get normalized metadata from every supported service without writing per-service mapping code.
Genre data is the most difficult to normalize. Services use incompatible genre taxonomies: one platform uses "Hip-Hop/Rap" as a single genre, another splits it into "Hip Hop" and "Rap," and a third uses Wikipedia category URLs instead of human-readable labels. Some services only associate genres with albums, not individual tracks. Building and maintaining a genre mapping table that covers all services requires ongoing effort as taxonomies evolve.
Not right now, but consider your roadmap. Most music apps expand to multiple services as they grow. If your data layer assumes a single service's schema from day one, adding a second service later means refactoring your database, API, and frontend to handle a different shape. Starting with a normalized internal schema (or using an API that returns one, like MusicAPI) avoids that refactor entirely.
Set missing fields to null rather than omitting them or using empty strings. This keeps your schema predictable: every track object has the same set of keys, and your application code can check for null without catching exceptions on missing properties. Document which fields are guaranteed (title, duration) versus optional (ISRC, genres) so your frontend knows what to expect.
Ready to skip months of OAuth and SDK work? Start your free MusicAPI trial and connect 10+ streaming services with one unified API.