Most "music analysis" APIs want a 30-second audio clip and a multipart upload. That's fine if you're building a DJ app where the user is dragging files in. It's a wall if you're building a fitness app trying to match treadmill cadence to a Spotify playlist, a music recommender that takes a track URL and returns "more like this," or honestly, any app where the user types Blinding Lights and expects the BPM back.
This is the architectural split nobody talks about: upload-based analysis APIs vs catalog-based lookup APIs. They look superficially similar (both return BPM, key, energy, etc.), but the failure modes are completely different and the right one for your app depends on what your user actually has in their hand.
The two architectures, in one paragraph each
Upload-based
You POST audio bytes; the API runs Essentia or librosa on them server-side; you get features back. Examples: AudD, Cyanite, AI Mastering. Hit rate is 100% (any audio works) but the user must have the file. No file means no answer. Pricing is usually per-call and on the higher end (compute-bound).
Catalog-based
You GET with ?track=Blinding+Lights&artist=The+Weeknd; the API looks the track up in a pre-analyzed library and returns the cached features. Hit rate is <100% (only works for tracks already in the catalog) but no audio file required, the response is fast (typically <100 ms), and pricing is usually per-lookup and lower (no per-call compute). When the catalog misses, the better APIs queue an on-demand analysis and email the user when it lands.
Spotify's audio_features endpoint was a catalog-based API: you passed a Spotify track ID, you got the features. It died in November 2024, which is why this post exists at all.
What "name-based lookup" actually does
The pattern, in code:
import requests
r = requests.get(
"https://api.freqblog.com/lookup",
params={"track": "Blinding Lights", "artist": "The Weeknd"},
headers={"X-Api-Key": api_key},
)
data = r.json()
# {
# "track_name": "Blinding Lights",
# "artist_name": "The Weeknd",
# "audio_features": {
# "bpm": 171.0,
# "bpm_alt": 85.5, # half-time variant
# "key": "C#-Minor",
# "camelot": "12A",
# "open_key": "5m",
# "energy": 0.91,
# "danceability": 0.85,
# ...
# }
# }
What's actually happening behind the request:
- Catalog hit — the lookup matches by name and returns within ~10 ms. Most popular tracks are pre-analyzed and live here.
- Catalog miss but reachable — the API can't find the track, but it can find the iTunes preview for it. It returns HTTP 202 ("queued"), kicks off a background analysis (download preview → run Essentia → cache result), and emails the user when the data is ready (typically 30 seconds to 2 minutes later). Subsequent calls hit the cache.
- Genuinely unfindable — demos, bootlegs, alternate-takes that have no commercial release. The API returns 404 or an empty object. Plan for this in your UX.
The 202 path is the differentiator. Most catalog APIs return 404 on miss and leave you to figure it out. The "queue and email" pattern means a track that's missing today is in the catalog tomorrow — the catalog grows toward the long tail of what your users actually search for, instead of being frozen to whatever was popular when the dataset was built.
What you can ask for by name
The full set of fields a good name-based BPM API should return:
| Field | Example | Why it matters |
|---|---|---|
bpm |
171.0 |
Tempo for sync/cadence/sorting |
bpm_alt |
85.5 |
Half-time variant when the detector reports the wrong octave |
key |
"C#-Minor" |
Musical key (12 pitches × major/minor) |
camelot |
"12A" |
DJ-friendly key notation for harmonic mixing |
open_key |
"5m" |
Alternative DJ notation |
energy |
0.91 |
0-1 normalised intensity |
danceability |
0.85 |
0-1 rhythmic groove score |
valence |
0.50 |
0-1 musical positivity |
loudness |
-5.2 |
dB FS reference |
time_signature |
4 |
Beats per bar |
genre |
"pop" |
Coarse Last.fm-style tag |
isrc + mbid
|
"USUM72003867" |
Cross-link IDs to other catalogs |
The hit rate is the metric that matters
The single most important question to ask any catalog-based API: what fraction of the tracks my users will search for are actually in your catalog? Vendors will quote you a top-line track count ("10M tracks!") which is meaningless unless you know the distribution.
Three honest signals to look for:
- Charts coverage — if the catalog has the current Apple Music / Spotify global top-200, you'll hit on most pop tracks people search for.
- Decade depth — if 80% of the catalog is 2020+, anything pre-2010 will miss. Music apps that surface throwback tracks need 60s/70s/80s coverage too.
- On-demand backfill — "we'll fetch and analyze if it's missing" turns a hard miss into a slow hit. Bigger long-term win than headline catalog size.
When the catalog misses, what do you do?
Three patterns work:
Pattern 1: Queue + notify
The 202-pattern. Show "Analyzing… we'll email you" in your UX. Comes back via webhook or the user re-querying. Best when users care about that specific track and will wait.
r = requests.get("https://api.freqblog.com/lookup",
params={"track": track, "artist": artist},
headers={"X-Api-Key": api_key})
if r.status_code == 202:
# Queued. Show a "checking..." UI. Re-poll in 30s, or wire a webhook.
return {"status": "queued"}
elif r.status_code == 200:
return r.json()
else:
return {"status": "unavailable"}
Pattern 2: Bulk pre-warm
If you know the user's library upfront (e.g. they uploaded a CSV of their playlist), POST the whole list to /bulk. The API returns immediately with a mix of hits + queued items, and the queued ones land via email/webhook over the next few minutes.
Pattern 3: Graceful degradation
If your UX doesn't need every field, the API returns what it has and nulls the rest. Render BPM and key when present; hide the energy/valence chips when null. Users don't know what they're missing if you don't show empty boxes.
BPM accuracy: a footnote that matters
BPM detectors get confused on roughly 20% of dance tracks. The Weeknd's Blinding Lights is the textbook example: the actual tempo is 171 BPM, but Spotify (and most DSP-based detectors) returns ~85.5 because the algorithm latches onto the half-time pulse. The fix: expose both the detected BPM and the half/double-time alternate, and let your app decide which to display based on the genre context.
Any BPM API you evaluate should either fix the half-time issue automatically or expose enough context (the alternate value, a confidence score, or both) for you to fix it client-side. Spotify's old audio_features didn't, which created a long tail of "this app says my song is 85 BPM" UX problems.
What no name-based API will give you
- Per-bar/per-beat timing. Catalog APIs return whole-track features. If you need beat-level timing for visuals or sync, you need an upload-based API or you'll have to run a beat tracker on the audio yourself.
- Tracks below a popularity floor. Bootlegs, demos, edit-only releases, and most non-commercial music isn't in any catalog. If your app routes around obscure music, you'll need either upload-based fallback or to live with the gap.
- Real-time analysis of this microphone input. That's a DSP problem, not an API one. Use librosa or Essentia client-side.
How to choose
- Users have audio files in hand? Upload-based API. Pay per call, accept the higher latency, get 100% coverage.
- Users type a track name or paste a URL? Catalog-based. Fast, cheap, plan for ~80-95% hit rate plus a graceful "queued" path for the misses.
- Both, sometimes? Most apps end up here. Use a catalog API as the fast path and an upload API (or your own Essentia worker) as the fallback for misses + obscure content.
Originally published at freqblog.com. The FreqBlog Music API has a free tier — no card required.
Got a specific catalog-vs-upload architecture question for your app? Drop it in the comments.
Top comments (0)