What identify does
POST /v1/tracks/identifytakes an audio fingerprint (or a raw audio file) and returns the catalogue tracks it matches, best match first. It's the request-response, one-shot counterpart to a live stream monitor: give it a few seconds of a recording and it tells you what the recording is. Under the hood it matches against a Beatport catalogue and AcoustID; it's a single fingerprint lookup with a tight error-tolerance ceiling, so a returned match is a confident one.
It's a paid-tier endpoint; the free tier gets a 403. The response is deliberately minimal (id, title, artists, confidence); you hydrate the full metadata in a cheap second call once you know which track matched.
Two ways to call it
There are two modes, chosen by Content-Type. They resolve to the same matcher and return the same shape:
- Fingerprint mode (JSON): you run Chromaprint locally, then POST the resulting integer array. Roughly 1 KB on the wire, a flat 10 credits. This is the mode to prefer.
- Raw-audio mode: POST the file bytes (up to 100 MB) with
Content-Type: application/octet-streamand the server fingerprints it for you. Zero client-side setup, but it ships the whole file and costs 10 + ceil(MB).
Fingerprint the audio
fpcalcis Chromaprint's command-line tool: a single static binary, packaged for every platform. The -raw flag emits the fingerprint as a comma-separated integer array (plus the decoded duration) instead of the compressed base64 form:
# Chromaprint's CLI - one static binary, no service to run $ fpcalc -raw track.mp3 DURATION=270 FINGERPRINT=1809491111,1809487015,1810011303, … ,1739372152
POST the fingerprint
Parse the FINGERPRINT= line into an array of numbers and send it as JSON. Pass fingerprint_duration when you have it, since it sharpens the match:
import { execSync } from "node:child_process"; // Decode + fingerprint locally, then post ~1 KB of integers. const raw = execSync("fpcalc -raw track.mp3", { encoding: "utf8" }); const line = raw.split("\n").find(l => l.startsWith("FINGERPRINT="))!; const fingerprint = line.slice("FINGERPRINT=".length).split(",").map(Number); const res = await fetch(`${BASE}/v1/tracks/identify`, { method: "POST", headers: { "content-type": "application/json", "x-api-key": API_KEY }, body: JSON.stringify({ fingerprint, fingerprint_duration: 270 }), }); const out = await res.json();
Read the ranked results
You get back matched, a best-first results array, and credits_charged. Each result carries a confidence from 0 to 1:
{
"matched": true,
"results": [
{
"id": "315587019",
"title": "Digital Love",
"artists": [
{ "id": 1925615, "name": "Daft Punk", "is_primary": true, "is_remixer": false }
],
"confidence": 0.97
}
],
"credits_charged": 10
}Take results[0] as the answer. If you need a certainty threshold, gate on confidenceand treat a lone low-confidence result as “no match”.
Hydrate the full record
The match tells you which track; a second call tells you everything about it. Look it up by id for the ISRC, genre, duration, and label, and hit /v1/tracks/links for the platform links:
// The identify response is slim - pull the full record by id. const id = out.results[0].id; const track = await fetch(`${BASE}/v1/tracks/${id}`, { headers: { "x-api-key": API_KEY }, }).then(r => r.json()); // track.isrc → the representative ISRC (may be null) // track.genre → ["House", …] // track.duration → seconds // track.releases[0] → { title, label: { name }, release_date }
That's the same track shape the rest of the API returns, so from here you can render a “now playing” card, attach Listen On / Buy On buttons, or write the ISRC into your own store.
The zero-setup alternative
If you can't run Chromaprint where the audio lives (a serverless function, a locked-down mobile runtime), skip step 1 and POST the bytes directly:
// No Chromaprint on your side? Post the bytes and let the server fingerprint. await fetch(`${BASE}/v1/tracks/identify`, { method: "POST", headers: { "content-type": "application/octet-stream", "x-api-key": API_KEY }, body: await readFile("clip.mp3"), // ≤ 100 MB · costs 10 + ceil(MB) });
Same response, same hydration path. On a miss the server quietly retries deeper 30-second windows of your clip before giving up, so a recording that only becomes recognisable a minute in still resolves.
Handling misses and errors
- No match.
matchedisfalseandresultsis empty. The recording isn't in the catalogue, or the clip was too short / too noisy to fingerprint. - 429.The request would cost more credits than you have left this month. It's rejected before any matcher work, so a denied identify never burns credits.
- 503 with Retry-After.The matcher is briefly unreachable (e.g. mid-deploy). Wait the header's seconds and retry the same request; it's idempotent.
- 413. The upload exceeded 100 MB. Send a shorter clip, or fingerprint it client-side and use the JSON mode instead.
Frequently asked questions
What audio formats can I send?
For the upload mode, anything ffmpeg can decode: mp3, flac, wav, m4a, mp4, ogg, opus. The format is sniffed from the bytes, not the extension or Content-Type. For the fingerprint mode it doesn't matter, because you've already decoded the audio locally with fpcalc; you're only sending the resulting integer array.
What's the difference in cost between fingerprint and upload?
A fingerprint is a flat 10 credits. An upload is 10 + ceil(MB) credits, billed on the full uploaded bytes, so a 4 MB clip costs 14. If you can run Chromaprint on your side, the fingerprint mode is both cheaper and lighter on the wire (roughly 1 KB versus the whole file).
What does the confidence score mean?
It's a 0–1 score derived from how closely the fingerprint matched, with 1.0 being an exact match. Results come back ranked best-first. In practice a real match sits high; treat a low-confidence single result with suspicion and, if you need certainty, cross-check the hydrated metadata (title, artist, duration) against what you expected.
Why doesn't the response include the ISRC directly?
The identify response is deliberately slim (id, title, artists, confidence) so it stays fast. Take the id of the top result and call GET /v1/tracks/:id to get the full record: ISRC, genre, duration, releases and label. GET /v1/tracks/links gets you the Spotify / Apple Music / Beatport / Tidal links.