SonoVault is in open beta — signups are live. Get your free API key →
ResourcesIntegration GuidesListen On / Buy On Buttons from One Track ID

Listen On / Buy On Buttons from One Track ID

Render Spotify, Apple Music, Tidal, Beatport, Discogs, and MusicBrainz buttons next to any track. One API call, deterministic URLs, graceful fallback when a platform doesn't carry the recording.

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.

Six surfaces. The resolver returns Spotify, Apple Music, Tidal, Beatport, Discogs, and MusicBrainz — listening on the first three, buying on Beatport, browsing release info on the last two. Whatever subset the recording is actually on.
Build
1

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:

GET/v1/tracks/links?id=123200 OK
{
  "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.

2

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:

TypeScriptapp/api/links/route.ts
// 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 5-minute cache is a free win — most users visiting the same track within a session shouldn't re-hit your upstream quota. Bump it longer if your catalog doesn't change frequently.
3

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:

TypeScriptTrackLinks.tsx
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/resolve with input_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.

Ready to build?

Free API key. No credit card. 1,000 requests to get started.

Get Free API Key