One button per platform, not one platform per app
Every track-display surface — your catalog page, your release radar email, your playlist export — gets a small but real conversion lift from showing the user a button for their platform. The Spotify user clicks Spotify; the Apple Music user clicks Apple Music; the obscure-collector clicks Discogs. Sending everyone to the same platform leaks half your audience.
The mechanics are simple: one call to /v1/tracks/links returns every platform's ID for the track, plus a deterministic deep-link URL for each. You render a button per returned source — no URL construction on your side, no “does this platform have it?” check, no broken links.
Fetch links for a track
The endpoint accepts an internal track ID, an ISRC, or any platform-specific ID. The response carries every other platform's ID with a built deep-link URL:
{
"track_id": 123,
"title": "One More Time",
"isrc": "GBDUW0000053",
"links": [
{ "source": "spotify", "external_id": "5W3cjX…", "url": "https://open.spotify.com/track/5W3cjX…" },
{ "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": "beatport", "external_id": "1234567", "url": "https://www.beatport.com/track/-/1234567" },
{ "source": "discogs", "external_id": "487193-2", "url": "https://www.discogs.com/release/487193" },
{ "source": "musicbrainz","external_id": "8e030f4c-…","url": "https://musicbrainz.org/recording/8e030f4c-…" }
]
}Each links entry has three fields: source (the platform name), external_id (the platform's ID), and url (the ready-to-use deep link). Use url directly — never build your own.
Proxy through your server, not the browser
The API key shouldn't end up in the browser. Add a tiny server-side route that proxies /v1/tracks/links with the key attached and returns the response as-is, with a short cache header:
// Your /api/links route — never expose SONOVAULT_API_KEY to the browser. import { NextRequest, NextResponse } from "next/server"; export async function GET(req: NextRequest) { const id = req.nextUrl.searchParams.get("id"); if (!id) return NextResponse.json({ error: "id required" }, { status: 400 }); const res = await fetch( `https://api.sonovault.now/v1/tracks/links?id=${id}`, { headers: { "x-api-key": process.env.SONOVAULT_API_KEY! } }, ); if (!res.ok) { return NextResponse.json({ error: "upstream" }, { status: res.status }); } const data = await res.json(); // 5-minute browser cache; widget calls this on every render otherwise. return NextResponse.json(data, { headers: { "cache-control": "public, max-age=300" }, }); }
The React widget
One client component, one fetch, one map over the platforms in your preferred order. Platforms that aren't in the response are simply not rendered — no “not available” states, no graying out:
import { useEffect, useState } from "react"; type SourceMeta = { label: string; verb: "Listen" | "Buy" | "View"; color: string; }; const META: Record<string, SourceMeta> = { spotify: { label: "Spotify", verb: "Listen", color: "#1DB954" }, applemusic: { label: "Apple Music", verb: "Listen", color: "#FA243C" }, tidal: { label: "Tidal", verb: "Listen", color: "#000000" }, beatport: { label: "Beatport", verb: "Buy", color: "#A8E000" }, discogs: { label: "Discogs", verb: "View", color: "#333333" }, musicbrainz: { label: "MusicBrainz", verb: "View", color: "#BA478F" }, }; // Render order: highest-conversion DSPs first, archives last. const ORDER = ["spotify", "applemusic", "tidal", "beatport", "discogs", "musicbrainz"]; interface Link { source: string; external_id: string; url: string; } export function TrackLinks({ trackId }: { trackId: number }) { const [links, setLinks] = useState<Link[]>([]); useEffect(() => { fetch(`/api/links?id=${trackId}`) .then(r => r.json()) .then(({ links }) => setLinks(links)); }, [trackId]); const ordered = ORDER .map(s => links.find(l => l.source === s)) .filter((l): l is Link => !!l); return ( <div className="track-links"> {ordered.map(l => { const m = META[l.source]; return ( <a key={l.source} href={l.url} target="_blank" rel="noopener" style={{ background: m.color }} data-source={l.source} > {m.verb} on {m.label} </a> ); })} </div> ); }
Drop <TrackLinks trackId=123 /> next to any track in your UI. For server-rendered pages, do the fetch server-side and pre-render the buttons — avoids the flash and gives you SSR-friendly markup.
Going further
- Per-user platform reorder.If you know the user's preferred platform (from their settings or a Spotify-OAuth signal), put it first in the order. Conversion lifts further when the relevant button is the leftmost.
- Affiliate / referral params. Beatport, Apple Music, and Tidal support tracking parameters that pay out commissions. Add them as a query string append on top of the
urlfield — don't replace it. - Bulk pre-fetch. Rendering 50 tracks? Use
/v1/tracks/resolvewithinput_type: "sonovault_id"to grab all 50 tracks' links in one POST. See cross-platform ID backfill for the batching pattern.
Frequently asked questions
Why not just link to Spotify if I already have the Spotify ID?
You can — if all your users are on Spotify. The moment you have any user on Apple Music, Tidal, or Beatport, sending them to Spotify creates friction. A platform-aware row lets each user click the service they actually use, which converts much better than a single hard-coded button.
What if the user's preferred platform isn't in the response?
The response only contains platforms that actually carry the recording — if Apple Music doesn't have a release, it isn't in links. Render only the platforms returned. For an obscure track that's only on Discogs and MusicBrainz, those are the only buttons.
Do the URLs ever break?
No — the URLs are built deterministically from the external_idby SonoVault and follow the platform's canonical deep-link pattern. The only way they break is if the platform changes the URL scheme (rare, and we update centrally). Don't construct them yourself; always use the url field from the response.
How do I track click-throughs?
Either wrap the anchor with your analytics SDK's track call, or proxy through your own redirect endpoint and 302 to link.url. The latter gives you a stable click-event log even when consumers ad-block the analytics SDK.