Beta founding offer: lock in 50% off any plan for life, before the beta ends. Claim founding pricing →
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 and 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. That 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
More in Integration Guides
Integration GuidesMigrate a User Library Between Streaming Platforms6 min read