Why a release radar beats a search box
Catching new releases in your scene reactively — searching every now and then for an artist you like — leaks signal. Things drop on Wednesday, you don't notice until Friday, the buzz has already happened. An A&R or playlist curator needs the opposite: a scheduled pull of every release that fits a scope, delivered before anyone else has heard it.
The scope is the interesting part. “Every release” is too much — thousands per day. “Just this one label” is too narrow — covered by the label release trackerarticle. The sweet spot is a small set of labels OR a genre filter — show me today's House releases, plus everything on Drumcode and Ninja Tune.
/v1/releases/new is the time axis — releases ordered by date./v1/tracks/browse is the filter axis — tracks filtered by label, artist, genre, or year. The radar joins them: take recent releases, intersect with genre-filtered tracks, deliver.Pull today's new releases
/v1/releases/new returns releases ordered by release date (newest first), with cursor pagination. Walk it until you hit your date cutoff:
{
"results": [
{
"id": 789,
"title": "Resonance EP",
"artist": { "id": 3, "name": "Bicep" },
"label": { "id": 42, "name": "Ninja Tune" },
"release_date": "2026-03-22",
"track_count": 4
},
/* … */
],
"next_cursor": "2026-03-22:78:789"
}Pull genre-filtered tracks for the same window
/v1/tracks/browse accepts genreId (the canonical id from /v1/genres) or genre (free-text name), plus year. Filter by your target genre to get 20 tracks ordered by release date:
// /v1/tracks/browse filtered by genreId and year. const params = new URLSearchParams({ genreId: "12", // House (from /v1/genres) year: "2026", }); const res = await fetch(`${BASE}/tracks/browse?${params}`, { headers: { "x-api-key": API_KEY }, }); const { results } = await res.json(); // 20 tracks ordered by release date (newest first), all House releases.
genreId over genre— it's stable and unambiguous. Fetch the id once from GET /v1/genres and cache it.Join: recent releases that match the scope
Combine the two: a release goes in the radar if it's on a watched label OR if any of its tracks appears in today's genre-filtered browse. One concrete implementation:
const API_KEY = process.env.SONOVAULT_API_KEY!; const BASE = "https://api.sonovault.now/v1"; interface Release { id: number; title: string; artist: { id: number; name: string }; label: { id: number; name: string } | null; release_date: string; track_count: number; } // 1. Pull every release from the past N days, paginating. async function recentReleases(days = 1): Promise<Release[]> { const cutoff = new Date(Date.now() - days * 86_400_000) .toISOString().slice(0, 10); const out: Release[] = []; let cursor: string | null = null; while (true) { const qs: Record<string, string> = { limit: "100" }; if (cursor) qs.cursor = cursor; const res = await fetch(`${BASE}/releases/new?${new URLSearchParams(qs)}`, { headers: { "x-api-key": API_KEY }, }); const page = await res.json(); for (const r of page.results as Release[]) { if (r.release_date < cutoff) return out; // past our window, done out.push(r); } cursor = page.next_cursor; if (!cursor) break; } return out; } // 2. Filter releases to those matching a configured taxonomy. const WATCHED_LABELS = new Set([1, 42, 128]); // Drumcode, Ninja Tune, etc. const WATCHED_GENRE = "House"; async function buildRadar() { const [recent, genreTracks] = await Promise.all([ recentReleases(1), fetch( `${BASE}/tracks/browse?${new URLSearchParams({ genre: WATCHED_GENRE, year: new Date().getFullYear().toString() })}`, { headers: { "x-api-key": API_KEY } }, ).then(r => r.json()), ]); // Release matches if it's on a watched label, OR if any of its tracks // show up in the recent House browse. const houseReleaseIds = new Set<number>(); for (const t of genreTracks.results) { for (const r of t.releases) houseReleaseIds.add(r.id); } return recent.filter(r => (r.label && WATCHED_LABELS.has(r.label.id)) || houseReleaseIds.has(r.id), ); } const radar = await buildRadar(); console.log(`${radar.length} releases in scope today`); for (const r of radar) { console.log(` ${r.release_date} ${r.artist.name} — ${r.title} [${r.label?.name ?? "no label"}]`); }
From here, render it however you want — a JSON file dropped to S3, a Slack post via webhook, an email digest, or an internal dashboard widget. The expensive part (API calls) is done; the rest is presentation.
Going further
- Score by popularity. Releases are returned with their artist popularity inputs to the sort but no score is exposed in the response. To prioritise, fetch
/v1/artists/:idand userelease_countas a proxy for established artists, or just keep the date order which already lifts notable releases through the artist-popularity tiebreaker server-side. - Diff against yesterday's run.Cache the radar output keyed by release ID. Only post to Slack what wasn't already in yesterday's feed — eliminates noise from multi-day-window pulls.
- Add the label tracker. See the label release tracker for the focused single-label variant — useful when one label warrants its own feed.
Frequently asked questions
What's the difference between /v1/releases/new and /v1/tracks/browse?
/v1/releases/new is a date-ordered feed of newly-released releases — no filtering, just chronological. /v1/tracks/browse filters tracks by label/artist/genre/year and supports a randomize mode. Use new for the time dimension, browse for everything else.
Can I filter the new-releases feed by genre directly?
No — /v1/releases/new returns releases in date order with no filter parameters. For genre filtering you either join with /v1/tracks/browse after the fact, or use /v1/tracks/browse with year=2026 andrandomize=false (which orders by release date too).
Both /v1/releases/new and /v1/tracks/browse are paid endpoints. Can I do this on the free tier?
Not at full fidelity. The free tier supports search, ISRC lookup, and platform links but not the discovery endpoints. Use Starter (€29/mo) or higher to run a release radar.