Why ISRCs are the right primary key
Every commercial recording released since the 90s has an ISRC— an International Standard Recording Code. It's a 12-character identifier that travels with the recording wherever it goes. Spotify, Apple Music, Tidal, Beatport, MusicBrainz, and Discogs all index by it. If two services disagree on the title, the artist order, or the release date, they will still agree on the ISRC.
That property is what makes ISRC the only sane primary key for any cross-platform workflow: catalog enrichment, royalty resolution, deduplication, ad-tech reporting, library management. Fuzzy artist + title matching breaks on remixes, edits, featured-artist order, regional re-releases, and typo'd metadata. ISRC matching doesn't.
GBDUW0000053 (Daft Punk — One More Time): GB = country (United Kingdom), DUW = registrant code (the label or rights holder), 00 = year of reference (2000), 00053 = designation. Twelve characters, globally unique per recording.The two endpoints
SonoVault exposes ISRC in two complementary ways:
/v1/tracks/isrc— exact lookup. Returns the full track payload: title, artists, release, duration, genre, subgenre./v1/tracks/links— the cross-platform resolver. Returns every external ID we know for that recording: Spotify, Beatport, Apple Music, Tidal, Discogs, and MusicBrainz.
Both accept an ISRC. The links endpoint is the one you want for the “one call, all platforms” case — it returns everything in a single round trip and a single billed request.
Look up a track by ISRC
Start with the basic lookup. Pass any valid ISRC and you get the full track record back, including genre and subgenre tags. No fuzzy matching — exact codes only.
{
"id": 123,
"title": "One More Time",
"isrc": "GBDUW0000053",
"releaseTitle": "Discovery",
"releases": [
{ "id": 1, "title": "Discovery",
"artist": { "id": 1, "name": "Daft Punk" } }
],
"artists": [
{ "id": 1, "name": "Daft Punk", "is_primary": true, "is_remixer": false }
],
"bpm": 123.0,
"key": "F min",
"duration": 320,
"popularity": 87,
"energy": 0.78,
"genre": ["Electronic"],
"subgenre": ["House", "French House"]
}This is useful when you have an ISRC and need the metadata. But note what's missing: there are no platform IDs in the response. For those, switch to the resolver.
Resolve to every platform in one call
Pass the same ISRC to /v1/tracks/links. Each entry in the links array has the source name, the source-specific ID, and a deep-link URL constructed deterministically from the ID.
{
"track_id": 123,
"title": "One More Time",
"isrc": "GBDUW0000053",
"links": [
{ "source": "spotify", "external_id": "spotify:track:5W3cjX2J3tjhG8zb6u0qHn",
"url": "https://open.spotify.com/track/5W3cjX2J3tjhG8zb6u0qHn" },
{ "source": "beatport", "external_id": "1234567",
"url": "https://www.beatport.com/track/-/1234567" },
{ "source": "applemusic", "external_id": "1440650",
"url": "https://music.apple.com/song/1440650" },
{ "source": "tidal", "external_id": "3789025",
"url": "https://tidal.com/browse/track/3789025" },
{ "source": "discogs", "external_id": "487193-2",
"url": "https://www.discogs.com/release/487193" },
{ "source": "musicbrainz", "external_id": "8e030f4c-7bca-4c54-9f5b-a3db7e2c9c41",
"url": "https://musicbrainz.org/recording/8e030f4c-7bca-4c54-9f5b-a3db7e2c9c41" }
]
}Wrap it in a typed helper
In real code you'll call this a lot, so wrap it once. The helper below is the minimum that lets you go from ISRC to a fully-typed list of platform links:
const API_KEY = process.env.SONOVAULT_API_KEY!; const BASE = "https://api.sonovault.now/v1"; interface Link { source: string; external_id: string; url: string | null; } interface LinksResponse { track_id: number; title: string; isrc: string; links: Link[]; } async function resolveIsrc(isrc: string): Promise<LinksResponse> { const res = await fetch(`${BASE}/tracks/links?isrc=${isrc}`, { headers: { "x-api-key": API_KEY }, }); if (!res.ok) throw new Error(`${res.status} ${res.statusText}`); return res.json(); } const data = await resolveIsrc("GBDUW0000053"); console.log(`${data.title} — ${data.links.length} platform link(s):\n`); for (const link of data.links) { console.log(` ${link.source.padEnd(12)} ${link.external_id}`); }
Output for our example:
One More Time — 6 platform link(s): spotify spotify:track:5W3cjX2J3tjhG8zb6u0qHn beatport 1234567 applemusic 1440650 tidal 3789025 discogs 487193-2 musicbrainz 8e030f4c-7bca-4c54-9f5b-a3db7e2c9c41
Enrich a catalog CSV
The most common real-world use case: you have a CSV from a label report, a DSP export, or your own DAM, and you want platform IDs attached to each row. One request per ISRC, write everything back out.
import fs from "node:fs"; import path from "node:path"; // CSV in: isrc,title,artist // CSV out: isrc,title,artist,spotify_id,beatport_id,applemusic_id,tidal_id const rows = fs.readFileSync("./catalog.csv", "utf-8") .trim() .split("\n") .slice(1) // header .map(line => line.split(",")); const out: string[] = [ "isrc,title,artist,spotify_id,beatport_id,applemusic_id,tidal_id", ]; for (const [isrc, title, artist] of rows) { try { const { links } = await resolveIsrc(isrc); const id = (s: string) => links.find(l => l.source === s)?.external_id ?? ""; out.push([ isrc, title, artist, id("spotify"), id("beatport"), id("applemusic"), id("tidal"), ].join(",")); } catch (e) { // Track not in DB — keep the row but leave platform IDs blank out.push([isrc, title, artist, "", "", "", ""].join(",")); } } fs.writeFileSync("./catalog.enriched.csv", out.join("\n")); console.log(`Wrote ${out.length - 1} rows to catalog.enriched.csv`);
p-limitat 8–16) and cache results by ISRC. The endpoint is cheap, but you'll burn through monthly quota faster than necessary if you don't.Going further
Once you can resolve ISRCs, a handful of workflows fall out cheaply:
- Deduplicate a library. Two files with different metadata but the same ISRC are the same recording. Group by ISRC and pick a canonical version.
- Cross-platform analytics. Match Spotify play counts against Beatport sales for the same recording — without writing a fuzzy matcher.
- Reverse-lookup by platform ID. The same
/v1/tracks/linksendpoint also acceptsspotify_id,beatport_id,applemusic_id, etc. Useful when the ISRC is missing but you have a platform ID from somewhere else. - Pair with search fallback.When ISRC isn't present, hit /v1/tracks/search, take the best result's ISRC, and feed that back to
/v1/tracks/links. Two requests, but you keep the accuracy benefits of ISRC matching everywhere downstream.
Frequently asked questions
What's an ISRC and how is it different from a UPC?
An ISRC (International Standard Recording Code) identifies a specific recording — Daft Punk's “One More Time” has one ISRC no matter where it appears. A UPC identifies the release it ships on (a single, an album, a compilation). One recording = one ISRC; the same recording can appear under many UPCs.
How is ISRC matching better than fuzzy artist + title search?
ISRCs are exact. Fuzzy artist + title matching breaks on remixes, radio edits, regional re-releases, featured-artist ordering, and typo'd metadata. When the data is available, ISRC lookup avoids every one of those failure modes.
What if my source data only has artist and title, not ISRC?
Use /v1/tracks/search to get a match first. The search response includes the ISRC, so you can pipe straight from search into /v1/tracks/links. For catalogs you already own (DSP reports, label exports), ISRC is almost always present.
Which platforms are returned by the resolver?
/v1/tracks/links returns links for Spotify, Beatport, Apple Music, Tidal, Discogs, and MusicBrainz — whichever services have a record for that recording. Each entry has source, external_id, and a deep-link url built deterministically from the ID.
What happens when a platform is missing for a new release?
When you query by ISRC, the endpoint actively asks Spotify, Beatport, Apple Music, and Tidal whether they have the recording. Any new IDs it discovers are returned in the same response and persisted, so the next call for that track comes straight from our DB. Discogs and MusicBrainz are populated from periodic dump imports rather than per-request lookups.