Home Pricing Docs Compare SDK Blog Changelog Status Get API Key →
Music API · Migration · Field guide

Migrating from Spotify Audio Features: a Field-by-Field Threshold Guide

May 2026 · Rewritten 26 September 2026 · 5 min read · FreqBlog

Rewritten 26 September 2026. Earlier versions of this guide compared our values with Spotify’s and offered a ?calibrate=spotify mode. To comply with Spotify’s developer terms we no longer use Spotify’s data to calibrate or compare values, so those comparisons and the calibrated mode are gone (?calibrate= is still accepted but ignored). What remains is the field mapping and one piece of advice: re-derive your thresholds on our values rather than copying numbers tuned for another service. See the changelog for the detail.

You swapped the host from api.spotify.com to api.freqblog.com. Your GET /v1/audio-features/{id} handler still parses the response unchanged — same path, same field names, same JSON shape. What is not the same is how the numbers are made: we compute ours from signal analysis (Essentia for BPM and key, our own descriptors for the rest) and, where a recording has them, from AcousticBrainz’s open classifiers. Same names, different measurements — so any hard-coded threshold in your code needs checking against our values before you rely on it.

If you haven't done the migration yet: start with Spotify Audio Features Is Dead. Here's What to Use Instead in 2026 for the landscape and the host-swap code. This post is the next step — making sure your numbers behave once the calls are flowing.

Field mapping

Every field Spotify’s audio_features object carried is present in our /v1/audio-features/{id} response under the same name. Some are null where we have no trustworthy source — your code should already handle a null numeric field.

Spotify fieldOur fieldWhat ours measuresNullable?
tempotempo (bpm on /lookup)Essentia RhythmExtractor2013. bpm_alt on /lookup flags likely half-time readings.No
keykey (0–11, −1 if undetected)Essentia KeyExtractor. /lookup adds the key name, camelot and open_key.No
modemode (0 minor / 1 major)Same extractor as key.No
time_signaturetime_signatureEssentia meter estimate; falls back to 4, so treat it as informational.No
loudnessloudness (dB)Loudness of the 30-second analysis window. Compare tracks relative to each other.No
energyenergyPerceptual intensity from RMS loudness, on our own scale (loud masters sit near the top).No
danceabilitydanceabilityTempo, beat strength and rhythmic regularity from our own analysis. Our own scale, and it runs high — re-derive any cut-off.No
valencevalenceMusical positiveness estimated from mode, tempo, brightness and energy. A coarse sorting signal.No
acousticnessacousticnessAcousticBrainz’s trained classifier where the recording has it; otherwise our spectral-flatness estimate, rescaled (order-preserving) onto AcousticBrainz’s distribution.Rarely
instrumentalnessinstrumentalnessAcousticBrainz’s trained classifier where the recording has it.Yes — null otherwise
speechinessspeechinessNot served for our own analyses.Yes — null for our analyses
livenesslivenessNot served for our own analyses.Yes — null for our analyses

A more accurate analysis of the descriptor fields is in progress; when it ships, the null fields will start carrying values and the changelog will say so.

How to re-derive a threshold

  1. Pull a labelled sample from your own library. Take a few hundred tracks you already know the answer for — “danceable” vs not, acoustic vs electric — and fetch them in batches with GET /v1/audio-features?ids=id1,id2,… (up to 100 per call).
  2. Look at our distribution, not a remembered number. Sort the sample by the field and see where your two groups separate. That crossing point is your threshold. Percentiles work too: “top 20% by energy” transfers across any scale.
  3. Prefer ordering to absolute cut-offs for valence, danceability and acousticness: they rank tracks usefully, but no single value is a verdict on one track.
  4. Handle null. Drop speechiness and liveness from filters for now, and treat a null instrumentalness as “unknown”, not 0.
  5. Don’t filter on time_signature. It is almost always 4.

Identifiers

/v1/audio-features/{id} accepts an ISRC (with or without hyphens) or a Spotify track ID, URI or URL; /lookup?track=…&artist=… takes a name. A raw Spotify ID is resolved per request by matching the track and can miss when a title is shared by several artists, so for reliable coverage key your lookups by ISRC — your Spotify track object already carries it at external_ids.isrc — or by name. In a batch, IDs we can’t resolve come back null and are not charged.

Try the drop-in — free tier, no card

Get an API key in seconds; the first 1,000 lookups per month are free.

Get API Key →

Further reading