On July 12, 2026 I asked a free lyrics API for the most famous Coldplay song there is. It answered in a few milliseconds: HTTP 200, valid JSON, a full array of twenty results. The top one was a song called "Help Is Round the Corner."
# runnable, read-only: no key needed
curl -s "https://lrclib.net/api/search?q=yellow%20coldplay"
[{"id":13774,"trackName":"Help Is Round the Corner","artistName":"Coldplay",
"albumName":"Yellow - Single","duration":156.0}, ...19 more...]
Status 200. The array is not empty. It has exactly the shape you expect. And results[0] is the wrong song. "Help Is Round the Corner" is a Coldplay B-side that shipped on the "Yellow" single back in 2000; the search matched the album name, ranked the B-side first, and handed it back with no hesitation. Every guard I reach for passes this response: resp.ok, len(results) > 0, results[0]["trackName"] exists. The wrong entity walks straight past all of them and gets stapled to the wrong lyrics.
My earlier keyless-API posts kept circling one idea from different angles. HTTP 200 does not mean the read worked, because the body can be empty. HTTP 201 Created does not mean a write happened, because the read-back returns 404. Then a 200 can parse, match your schema, and still hand you null. This post moves the problem sideways. Here the status is 200, the body arrives, it parses, the list is full, every field is present and well-typed. The top row is just the wrong thing. A 200 with a full list is not a 200 with the right result.
The reason is structural, and it is specific to music. There is no single ID for "the song Yellow by Coldplay." There is a studio single, a remaster, a live radio session, a karaoke track, a handful of covers, and a B-side that sits on the "Yellow" single. Every music search is a fuzzy ranking over that mess, so every results[0] is a guess. A valid, non-empty, correctly shaped guess. The only thing that tells you whether it is the right guess is a number or a field in the body that most tutorials never read.
A free music API here means a public endpoint that returns music data (tracks, artists, lyrics, cover art, radio, audio features, listening history) with no API key, no signup, and no credit card. A URL you can paste into a terminal right now. Eleven clear that bar, and I re-verified every response below with a live curl on July 12, 2026: real HTTP code, real body, trimmed but never reworded.
One scope note first, so the numbers stay honest. I curl-verified all eleven APIs on July 12, 2026. I have not run a music resolver in production. My 2,190 production scraper runs (962 of them on a single Trustpilot scraper) are a different domain, and I cite them for one reason only: they are why I read a body's own confidence fields instead of its status line. That number is not a claim about these eleven endpoints.
| # | API | What it returns | Example call | The fuzzy trap to watch |
|---|---|---|---|---|
| 1 | MusicBrainz | Artist / recording metadata + MBIDs | GET musicbrainz.org/ws/2/recording?query=... |
200, score:100, top hit is a live bootleg |
| 2 | Cover Art Archive | Album art by MBID | GET coverartarchive.org/release-group/{mbid} |
Hit is a 307, miss is a 404, never a plain 200 |
| 3 | iTunes Search | Apple catalog search | GET itunes.apple.com/search?term=... |
200, fuzzy-matches a typo into real tracks |
| 4 | Deezer | Track / artist search + ISRC | GET api.deezer.com/search?q=... |
200 carrying an error object in the body |
| 5 | Lyrics.ovh | Plain lyrics by artist/title | GET api.lyrics.ovh/v1/{artist}/{title} |
Works, but the host flaps between days |
| 6 | lrclib | Time-synced lyrics | GET lrclib.net/api/search?q=... |
200, results[0] is a B-side, not the song |
| 7 | Radio Browser | Internet radio directory | GET de1.api.radio-browser.info/json/stations/search |
200 lists dead streams; read lastcheckok
|
| 8 | Open Opus | Classical composers / works | GET api.openopus.org/composer/list/search/... |
Curated set; a miss returns rows:0
|
| 9 | ListenBrainz | Open listening-history stats | GET api.listenbrainz.org/1/stats/sitewide/artists |
Read is keyless; submit needs a token |
| 10 | AcousticBrainz | Audio features (loudness, BPM, key) | GET acousticbrainz.org/api/v1/{mbid}/low-level |
200 forever, dataset frozen since 2022 |
| 11 | Odesli / song.link | Cross-platform track links | GET api.song.link/v1-alpha.1/links?url=... |
200, but links are geo-dependent |
One flagged bonus and a short list of key-required names get their own section after the eleven.
1. MusicBrainz: the score of 100 that means "string matched," not "right recording"
MusicBrainz is the open music encyclopedia, the free MBID system that half the other APIs on this list key off. It is keyless, but it has two rules: send a real User-Agent (a default client UA gets a 403) and stay under about one request per second. The happy path is clean.
# runnable, read-only: real User-Agent is mandatory
curl -s -A "my-app/1.0 (me@example.com)" \
"https://musicbrainz.org/ws/2/artist?query=Radiohead&fmt=json&limit=2"
{"count":29,"artists":[{"id":"a74b1b7f-71a5-4011-9441-d0b5e4122711",
"name":"Radiohead","score":100,"country":"GB"}, ...]}
Score 100, country GB, one MBID you can trust. Now search for a recording instead of an artist, and watch the ranking betray you:
# runnable, read-only
curl -s -A "my-app/1.0 (me@example.com)" \
"https://musicbrainz.org/ws/2/recording?query=Yellow%20AND%20artist:Coldplay&fmt=json&limit=2"
{"count":146,"recordings":[{"id":"f15eb81a-b021-4208-9f1c-f4db2a9f09ae",
"title":"Yellow","score":100,
"disambiguation":"live, 2001-06-13: KBCO Studio C, Boulder, CO, USA",
"releases":[{"title":"KBCO Studio C, Volume 13","date":"2001"}]}]}
Read that top row carefully. The title is exactly "Yellow." The score is a perfect 100. If your code trusts results[0].title, you are certain you found the song. You did not. The disambiguation field says live, 2001-06-13: KBCO Studio C, Boulder, CO, USA, and the release is a live radio-session compilation. Out of 146 matches, the highest-ranked "Yellow" by Coldplay is a bootleg of a radio appearance, not the studio single from Parachutes. A score of 100 means your query string matched this row. It says nothing about whether this row is the canonical recording. The only field that warns you is disambiguation, and the happy path throws it away.
When to use it: anything that needs stable IDs across services, past a gate that reads score and disambiguation before it trusts a match. Docs: musicbrainz.org/doc/MusicBrainz_API.
2. Cover Art Archive: the hit is a 307 and the miss is a 404, never a plain 200
Cover Art Archive serves album art keyed by a MusicBrainz release or release-group MBID. It is the sister project to MusicBrainz, keyless, and it has a contract that breaks naive status checks at both ends.
# runnable, read-only: -i shows the status line, no -L yet
curl -si -A "my-app/1.0" \
"https://coverartarchive.org/release-group/1b022e01-4da6-387b-8658-8678046e4cef" | head -1
HTTP/2 307
That is a 307 redirect, not a 200. Follow it and you land on an archive.org JSON index:
# runnable, read-only: -L follows the redirect
curl -sL -A "my-app/1.0" -o /dev/null -w "%{http_code} after %{num_redirects} redirects\n" \
"https://coverartarchive.org/release-group/1b022e01-4da6-387b-8658-8678046e4cef"
200 after 2 redirects
A hit is a 307 that resolves, through archive.org, to a 200. A miss is blunt:
# runnable, read-only
curl -s -o /dev/null -w "%{http_code}\n" \
"https://coverartarchive.org/release/00000000-0000-0000-0000-000000000000"
404
So the contract is redirect-or-404, never a plain 200 with image bytes. Code that does if resp.status_code == 200 before following redirects sees the hit as a "failure" (it is a 307) and needs a separate branch for the honest 404 on a miss. Let your HTTP client follow redirects, then check the final status, and treat 404 as "no art," not as an error to retry.
When to use it: fetching album covers by MBID, with redirect-following on and a 404 handled as an empty result. Docs: coverartarchive.org/doc/API.
3. iTunes Search: it never says "no match," it fuzzy-matches your typo
Apple's iTunes Search API is keyless, fast, and returns a text/javascript body that is really JSON. The clean call is what you expect.
# runnable, read-only
curl -s "https://itunes.apple.com/search?term=radiohead&entity=song&limit=2"
{"resultCount":2,"results":[{"trackId":1097861834,"artistName":"Radiohead",
"trackName":"Let Down","collectionName":"OK Computer"}, ...]}
Now feed it a keyboard mash that is not a band, an album, or a word:
# runnable, read-only
curl -s "https://itunes.apple.com/search?term=zzxqwlkjhgf&entity=song&limit=3"
{"resultCount":3,"results":[
{"trackName":"Zzzquil Commercial","artistName":"Brave Rome Terrified"},
{"trackName":"Waves (feat. Lil Zzzquil)","artistName":"TheNewErra"},
{"trackName":"H0URGLASS (feat. Lil Zzzquil)","artistName":"VibezyBaby"}]}
I asked for zzxqwlkjhgf. I got HTTP 200 and resultCount: 3, three real tracks that share a few letters with my garbage. The API never returns "no match found." It fuzzy-matches, always, and reports success. So resultCount > 0 is not a signal that your query was meaningful; it is a signal that Apple found something loosely alphabetically nearby. Garbage in gives you garbage that looks found, at a confident 200. If you build autocompletion or enrichment on this, compare the returned artistName against what you searched for before you trust the match.
When to use it: quick catalog lookups and preview URLs, with a sanity check that the top result actually resembles the query. Docs: Apple iTunes Search API.
4. Deezer: the 200 that carries an error object in its body
Deezer's public API does search and lookup with no key. The useful part of a search hit is the ISRC, a real cross-catalog identifier you can carry to other services.
# runnable, read-only
curl -s "https://api.deezer.com/search?q=radiohead&limit=2"
{"data":[{"id":138546811,"title":"All I Need","isrc":"GBSTK0700005","rank":794527}, ...]}
Now look up an artist ID that cannot exist:
# runnable, read-only
curl -si "https://api.deezer.com/artist/999999999999" | head -1
curl -s "https://api.deezer.com/artist/999999999999"
HTTP/2 200
{"error":{"type":"DataException","message":"no data","code":800}}
HTTP 200, and the payload is an error. resp.raise_for_status() is delighted. The failure lives in data["error"], code 800, and nowhere in the status line. Deezer reports transport success and puts the application failure in the body, which is the exact opposite of a schema you can trust by shape alone: the success body and the error body are different objects under the same 200. Branch on whether "error" is a key, not on the HTTP code.
When to use it: getting ISRCs and preview URLs, with an explicit check for an error key before you read data. Docs: developers.deezer.com.
5. Lyrics.ovh: keyless and simple, and the flakiest host on this list
Lyrics.ovh returns plain lyrics for an artist and title. No key, no envelope, just the text.
# runnable, read-only
curl -s "https://api.lyrics.ovh/v1/Coldplay/Yellow"
{"lyrics":"Look at the stars\nlook how they shine for you\nand everything you do..."}
It answered a clean 200 for me on July 12, 2026. I am going to be blunt about its one real weakness, because pretending otherwise would waste your afternoon: this host flaps. On other days I have watched the same endpoint time out or return a 500 with no pattern I could pin down. It is the least reliable host in this roundup. If you depend on it, wrap the call in a short timeout and a fallback to another lyrics source, and do not treat a single good curl as an uptime guarantee. I did not measure its uptime and I am not going to claim one.
When to use it: a fast, no-frills lyrics lookup for a hobby project, always behind a timeout and a fallback. Docs: lyrics.ovh.
6. lrclib: time-synced lyrics, and the search that ranks a B-side first
lrclib is built for music players that need time-synced (karaoke-style) lyrics, and it is the source of the wrong song at the top of this post. Its /api/search endpoint is a fuzzy search, and you already saw what it did with "yellow coldplay": HTTP 200, twenty results, results[0] the B-side "Help Is Round the Corner." The fix is that lrclib also ships an exact-lookup endpoint, and you should almost always use that one instead.
# runnable, read-only: exact lookup by the four fields it needs
curl -s -o /dev/null -w "%{http_code}\n" \
"https://lrclib.net/api/get?track_name=Yellow&artist_name=Coldplay&album_name=Parachutes&duration=267"
200
/api/get takes the track name, artist name, album name, and duration, and resolves to the specific recording instead of ranking a pile of loose matches. Same API, same 200, completely different reliability: /api/search is a guess machine you sort yourself, /api/get is a lookup. If you have the album and rough duration (from a file's tags, say), skip the search entirely.
When to use it: synced lyrics in a player, through /api/get with the track's own metadata, falling back to /api/search only when you must and then filtering the results yourself. Docs: lrclib.net/docs.
7. Radio Browser: a directory that lists dead streams at 200
Radio Browser is a community-run directory of internet radio stations. There is no single canonical host; you pick a mirror (de1., nl1., all.) and query it keyless.
# runnable, read-only: pick a mirror host
curl -s "https://de1.api.radio-browser.info/json/stations/search?name=bbc&limit=2"
[{"stationuuid":"f7fd408e-...","name":"BBC RADIO 1 - The biggest new pop...",
"url":"http://a.files.bbci.co.uk/ms6/live/...","lastcheckok":1}, ...]
Two things bite here. First, the mirror: there is no one hostname, so hardcode one and your app dies when that mirror does; the directory publishes a server list you are meant to rotate through. Second, and this is the important one, a station in the directory is not the same as a station that is streaming right now. The lastcheckok field is the freshness flag: 1 means the last health check reached the stream, 0 means it did not. A 200 with lastcheckok: 0 is a dead stream that still shows up in your results with a full, valid record. Filter on lastcheckok before you hand a URL to a player.
When to use it: building a radio picker, rotating across mirror hosts and filtering lastcheckok: 1. Docs: api.radio-browser.info.
8. Open Opus: classical metadata that the pop APIs get wrong
Classical music is where the mainstream catalog APIs fall apart, because they model a "song" and an "artist" and classical needs a composer, a performer, a work, and a movement. Open Opus models it properly, keyless.
# runnable, read-only
curl -s "https://api.openopus.org/composer/list/search/beethoven.json"
{"status":{"success":"true","rows":1},
"composers":[{"id":"145","complete_name":"Ludwig van Beethoven","epoch":"Early Romantic"}]}
rows: 1, one composer, a clean ID. The limitation to know up front: Open Opus is a curated set of composers, not an exhaustive index of everyone who ever wrote a string quartet. A search for an obscure name returns rows: 0 inside a 200, not an error, so check the rows count rather than assuming a hit. For the standard repertoire it is the cleanest free classical source I found.
When to use it: a classical browser or quiz where the composer coverage is the popular canon, checking status.rows before you read composers. Docs: openopus.org.
9. ListenBrainz: open listening history you can read without a token
ListenBrainz is MetaBrainz's open answer to Last.fm scrobbling: a public dataset of what people are listening to. Reading aggregate stats and public user listens needs no token.
# runnable, read-only
curl -s "https://api.listenbrainz.org/1/stats/sitewide/artists?range=week&count=2"
{"payload":{"artists":[{"artist_mbid":"0d79fe8e-...","artist_name":"BTS",
"listen_count":819275}, ...]}}
Public user listens work the same way, keyless: GET https://api.listenbrainz.org/1/user/rob/listens?count=1 returned a real listen for me at 200. The honest split: reading public data is free and open, but submitting listens or reading a private user's data needs a token from a free account. So "keyless" is true for the read half and false for the write half; do not design a submitter and then discover the auth wall.
When to use it: charts, "top artists this week," and reading public scrobbles, with a token only if you need to write. Docs: listenbrainz.readthedocs.io.
10. AcousticBrainz: real audio features, from a dataset frozen in 2022
AcousticBrainz serves computed audio features (loudness, BPM, key, mood estimates) per recording MBID, keyless. The data is real and detailed.
# runnable, read-only
curl -s "https://acousticbrainz.org/api/v1/96685213-a25c-4678-9a13-abd9ec81cf35/low-level"
{"lowlevel":{"average_loudness":0.570070445538, ...},
"metadata":{"audio_properties":{"length":240.509384155}, ...}}
Here is the limitation that matters more than any field in that body: the AcousticBrainz project stopped collecting data in 2022 and the dataset is frozen. The API still answers a confident 200 with genuine features, but they are a snapshot that has not moved in years, and any recording that entered MusicBrainz after the freeze returns nothing. This is a freshness trap the status code will never show you: 200 forever, data stuck in 2022. Treat it as a historical dataset, not a live service, and expect misses on anything recent.
When to use it: experiments and teaching on pre-2022 recordings, with eyes open that it is an archive. Docs and the shutdown notice: acousticbrainz.org.
11. Odesli / song.link: cross-platform links that change by country
Odesli (the API behind song.link) takes one platform URL and returns the same track's links on the others: Spotify, Apple Music, YouTube, and more, keyless.
# runnable, read-only
curl -s "https://api.song.link/v1-alpha.1/links?url=https%3A%2F%2Fopen.spotify.com%2Ftrack%2F4LRPiXqCikLlN15c3yImP7"
{"entityUniqueId":"SPOTIFY_SONG::4LRPiXqCikLlN15c3yImP7","userCountry":"KZ",
"entitiesByUniqueId":{"...":{"title":"As It Was","artistName":"Harry Styles"}}}
Look at userCountry: "KZ". I did not send a country. Odesli defaulted it to the country of my request IP, which resolved to Kazakhstan that day. This matters because the platform links and availability are geo-dependent: the same track can map to different catalog links, or be missing on a platform, depending on the country. A 200 in one region and a 200 in another can carry different bodies for the identical URL. If your users are not sitting where your server is, pass an explicit userCountry parameter instead of letting it guess.
When to use it: "listen on your platform" buttons and cross-service matching, with userCountry set explicitly for your audience. Docs: odesli.co (see the song.link API section).
Why not on the list: one shared key and five paywalls
TheAudioDB almost made the eleven. It returns rich artist data keyless-looking:
# runnable, read-only
curl -s "https://www.theaudiodb.com/api/v1/json/2/search.php?s=coldplay"
{"artists":[{"idArtist":"111239","strArtist":"Coldplay","intFollowers":"57510550"}]}
That 2 in the path is a shared demo key, not "no key." It works today, but it is a public credential the project can rotate or rate-limit at any time, and building on a shared test key is how a tutorial rots. I left it off the eleven and I am telling you why rather than quietly padding the count.
And the names people expect that are simply not keyless in 2026: Last.fm (API key), Spotify (OAuth client credentials), Genius (access token), Jamendo (client ID), GetSongBPM (key plus a backlink requirement). Good APIs, none of them "paste a URL into a terminal" free. If a 2021 tutorial told you Spotify was easy, it was easy before the app-registration step it skipped.
Why does results[0] keep handing you the wrong song?
Because every music search is a fuzzy ranking over a namespace with no primary key, and results[0] is the top of a ranking, not the answer to a lookup. There is no unique ID for "Yellow by Coldplay." There is a studio single, a live radio session, a remaster, covers, and a B-side on the "Yellow" release, and the engine scores all of them against your string and returns the winner. A score of 100 means the string matched. It does not mean the row is the thing you meant. That gap, valid and non-empty and right-shaped and still wrong, is the whole post.
I have paid for that exact failure class in a different domain. Across 2,190 production scraper runs, 962 of them on one Trustpilot scraper, the incidents that cost real money were almost never the crashes. A 500 pages you at 3am and you fix it. The response that came back valid, non-empty, and quietly attributed to the wrong company, because two businesses shared a name, does not page anyone. It corrupts a column, and you find out weeks later when a report looks off. Music APIs are that same disease with a friendlier face. (Honest note: I have not run a music resolver across those 2,190 runs. They are a scraping-and-enrichment domain, cited only as where the read-the-value habit comes from. Do not read 2,190 as a music-API number.)
The fix is not another validation library. It is a field-sanity gate: never take results[0] on faith, and reject the top hit when the body's own metadata says it is thin or ambiguous. Here is the shape of the gate I use, in music terms, against MusicBrainz:
# runnable local: reject the top hit unless the body backs it up
import requests
UA = "your-app/1.0 (you@example.com)" # MusicBrainz 403s a default UA
def resolve_recording(title, artist, min_score=90):
r = requests.get(
"https://musicbrainz.org/ws/2/recording",
params={"query": f"{title} AND artist:{artist}", "fmt": "json", "limit": 5},
headers={"User-Agent": UA}, timeout=15,
)
r.raise_for_status() # a 200 sails through, so keep reading
recs = r.json().get("recordings", [])
if not recs:
raise ValueError(f"no match for {title!r} by {artist!r}")
top = recs[0]
if top.get("score", 0) < min_score:
raise ValueError(f"low score {top.get('score')} for {title!r}")
if top.get("disambiguation"): # e.g. "live, 2001-06-13: KBCO Studio C..."
raise ValueError(f"ambiguous top hit: {top['disambiguation']}")
return top["id"]
>>> resolve_recording("Yellow", "Coldplay")
ValueError: ambiguous top hit: live, 2001-06-13: KBCO Studio C, Boulder, CO, USA
That is the real behavior against the live API on July 12, 2026. The function does not silently return the KBCO Studio C bootleg. It refuses, because the top hit carries a disambiguation string that says "live." That is the entire move: read score and disambiguation on MusicBrainz, resultCount and the returned artistName on iTunes, the error key on Deezer, lastcheckok on Radio Browser, userCountry on Odesli, the fields the tutorial's results[0].name skips right over.
There is no universal threshold. min_score = 90 is mine, for MusicBrainz, picked after watching which scores went with wrong entities; choose your own per API and write it down. And the name-collision problem is worse than one bootleg. A MusicBrainz search for artist:Nirvana returned count: 41 the day I checked: forty-one distinct artists carrying the name Nirvana. If your resolver grabs the first one, you are right about the grunge band by luck, not by logic, and luck is not a data pipeline.
FAQ
What is the best free music API with no key?
It depends on what you need, and several are keyless together. MusicBrainz is the backbone for metadata and stable IDs (send a real User-Agent). iTunes Search and Deezer are fast catalog search, and Deezer hands you ISRCs. lrclib gives time-synced lyrics. All returned HTTP 200 keyless on July 12, 2026. Pick by the job, and read each one's confidence fields before you trust results[0].
Does the MusicBrainz API require an API key?
No key, but two conditions. You must send a descriptive User-Agent header, because a default client UA gets a 403, and you must stay under about one request per second or you get rate-limited. Within those rules it is fully open and free, including the MBIDs that Cover Art Archive and AcousticBrainz key off.
Why does a music search return the wrong song at HTTP 200?
Because a search is a fuzzy ranking, not a lookup, and music has no single ID per song. For q=yellow coldplay, lrclib returned HTTP 200 with a full list whose top result was the B-side "Help Is Round the Corner," and MusicBrainz ranked a live radio bootleg of "Yellow" first with a score of 100. The status is 200 and the body is well-formed; the signal that it is the wrong entity lives in fields like score and disambiguation, not in the HTTP code.
Is there a free lyrics API without a key?
Two, keyless. lyrics.ovh returns plain lyrics but its host flaps, so wrap it in a timeout and a fallback. lrclib returns time-synced lyrics; prefer its /api/get exact lookup (track, artist, album, duration) over /api/search, because the search ranks fuzzily and can put a B-side first.
Can I get audio features like BPM and key for free?
AcousticBrainz serves loudness, BPM, key, and mood estimates keyless by MusicBrainz recording ID, and returned a clean 200 for me on July 12, 2026. The catch is freshness: the project stopped collecting data in 2022, so the dataset is frozen and any recording added after the freeze returns nothing. Treat it as a historical archive, not a live service.
How do I match one song across Spotify, Apple, and YouTube?
Odesli, the API behind song.link, takes one platform URL and returns the others keyless. One caveat: results are geo-dependent. Its userCountry defaults to your request IP's country (mine resolved to KZ), and platform availability changes by country, so pass an explicit userCountry for your audience instead of letting it guess.
Written by Aleksei Spinov. I build web-scraping and data-enrichment tool layers, currently 2,190 runs across 32 actors in production. Every endpoint above was re-verified with a live curl (real HTTP code, real body) on July 12, 2026 before publishing; responses are trimmed, never reworded. I have not run a music resolver in production; the 2,190 runs are a scraping-and-enrichment domain, cited only as the origin of the read-the-value habit. I wrote up the same read-the-body pattern for free government data APIs and free mock and fake-data APIs. Drafted with an AI assistant, then fact-checked and edited by me against the raw responses.
Follow for the next keyless layer I verify. And tell me: what is the worst wrong-entity a "successful" API has handed you, the right shape with the wrong thing inside, and how long before you caught it? I read every comment.
Top comments (0)