curl
export UNCOMMON_EAR_API_KEY="ue_your_key_here"
curl "https://uncommonear.com/v1/search?q=calm+piano&limit=3" \
-H "Authorization: Bearer $UNCOMMON_EAR_API_KEY"Search free music and sound effects from your own code. Every result tells you its license and terms, whether credit is asked or required, and gives you a credit line to paste.
The API is a read-only JSON API. It needs a free Uncommon Ear account and an API key. To search from an AI assistant instead, see Use Uncommon Ear from your AI assistant. The routes are described in an OpenAPI 3.1 document, which you can load into tools such as Postman or a code generator.
Build script, and select Create key.
ue_. For your security, it's shown only once. If you lose it, revoke it and create another.You can have up to 5 active keys. Revoking a key stops it working right away, and deleting your account deletes all of your keys. Treat a key like a password: keep it out of source control and out of web pages, and store it in an environment variable or a secrets manager. Send it only in the Authorization header. The API doesn't use cookies, and it doesn't accept a key in the URL.
Every request is a GET to https://uncommonear.com with your key in the Authorization: Bearer header. This request finds three calm piano tracks.
export UNCOMMON_EAR_API_KEY="ue_your_key_here"
curl "https://uncommonear.com/v1/search?q=calm+piano&limit=3" \
-H "Authorization: Bearer $UNCOMMON_EAR_API_KEY"This example uses fetch, which is built into Node.js 18 and later and into every browser. Don't call the API from a web page, because that would expose your key to every visitor. Call it from a server or a script.
const key = process.env.UNCOMMON_EAR_API_KEY;
const url = "https://uncommonear.com/v1/search?" + new URLSearchParams({ q: "calm piano", limit: "3" });
const response = await fetch(url, {
headers: { Authorization: "Bearer " + key },
});
if (!response.ok) {
const { error, message } = await response.json();
throw new Error(error + ": " + message);
}
const { tracks, cursor } = await response.json();
for (const track of tracks) {
console.log(track.title, "-", track.credit_line);
}This example uses the requests library. Install it with pip install requests.
import os
import requests
response = requests.get(
"https://uncommonear.com/v1/search",
params={"q": "calm piano", "limit": 3},
headers={"Authorization": "Bearer " + os.environ["UNCOMMON_EAR_API_KEY"]},
timeout=10,
)
response.raise_for_status()
for track in response.json()["tracks"]:
print(track["title"], "-", track["credit_line"])A successful call returns status 200 and JSON like this:
{
"tracks": [
{
"id": "fp-0000000001",
"title": "Sunrise Walk",
"artist": "Kevin MacLeod",
"kind": "music",
"duration_s": 90,
"bpm": 100,
"tags": { "genre": ["cinematic"], "mood": [], "use": [], "instrument": [], "sfx": [] },
"license": { "id": "cc0", "name": "CC0 1.0", "url": "https://creativecommons.org/publicdomain/zero/1.0/", "status": "verified" },
"terms": { "commercial": true, "credit": "none", "share_alike": false },
"attribution": "requested",
"credit_line": "\"Sunrise Walk\" by Kevin MacLeod, via FreePD (CC0 1.0) https://freepd.com/...",
"source_page": "https://freepd.com/...",
"preview_url": "https://media.uncommonear.com/p/fp-0000000001.mp3",
"page_url": "https://uncommonear.com/browse?t=fp-0000000001"
}
],
"total": 1,
"cursor": null
}GET /v1/search returns tracks that match your parameters, with the best matches first. All parameters are optional. With none, you get a shuffled page of music.
| Parameter | Type | Description |
|---|---|---|
| q | string | Words for mood, genre, instrument, title, or intended use, such as "calm piano". |
| kind | string | music (default) or sfx for sound effects. |
| tempo | string | Beats per minute as a range, such as 90-120. |
| length | string | Length buckets, comma-separated: xs, s, m, l. For music: under 30 seconds, 30 seconds to 2 minutes, 2 to 5 minutes, over 5 minutes. Sound effects use shorter buckets. |
| energy | string | Comma-separated: calm, steady, driving. |
| tone | string | Comma-separated: dark, warm, bright. |
| loops | boolean | true returns only tracks marked as loops. |
| quality | string | good hides lower-quality recordings. Default: any. |
| genre | string | Genre tags, comma-separated, such as rock,jazz. A track matches if it has any of them. |
| mood | string | Mood tags, comma-separated. Any-of, like genre. |
| use | string | Intended-use tags, comma-separated, such as menu,trailer. Any-of. |
| instrument | string | Instrument tags for music, comma-separated, such as piano,guitar. Any-of. |
| sfx | string | Sound-effect type tags, comma-separated, such as impact,door. Any-of. |
| credit | string | optional keeps tracks that need no credit (the same as "No credit needed" on the site). requested keeps tracks whose license requires credit or whose creator or source asks for it. Default: any. |
| license | string | License ids, comma-separated, such as cc0,cc-by-4.0. A track matches if it has any of them. Ids: cc0, cc-by-3.0, cc-by-4.0, oga-by-3.0, oga-by-4.0, cc-by-sa-3.0, cc-by-sa-4.0, cc-by-nc-3.0, cc-by-nc-4.0, cc-by-nc-sa-3.0, cc-by-nc-sa-4.0. |
| commercial | boolean | true keeps tracks you can use in commercial work (the same as "Free for commercial use" on the site). false keeps only non-commercial tracks. Default: both. |
| share_alike | boolean | false keeps tracks with no share-alike condition (the same as "No share-alike" on the site). true keeps only share-alike tracks. Default: both. |
| creator | string | Artist name, matched without regard to case. |
| source | string | Source domain, such as kenney.nl. Comma-separate several. |
| sort | string | shuffle, relevance, quality, short, long, slow, or fast. Default: relevance when q is set, otherwise shuffle. |
| limit | integer | Tracks per page, from 1 to 50. Default: 20. |
| cursor | string | The cursor from the previous response, to get the next page. |
The response has tracks, total (how many tracks match in all), and cursor. To get the next page, send the same parameters with cursor set to the value you received. When cursor is null, you have reached the last page. A parameter that isn't in the table returns a 400 error, so a typo never silently changes your results.
GET /v1/tracks/{id} returns one track, in the same shape as an item in tracks. Use the id from a search result.
curl "https://uncommonear.com/v1/tracks/fp-0000000001" \
-H "Authorization: Bearer $UNCOMMON_EAR_API_KEY"GET /v1/tracks/{id}/similar returns { "tracks": [...] }: tracks of the same kind that sound like this one, closest first. It takes one parameter, limit (1 to 50; default 20). Both routes return a 404 error when no track has the id.
curl "https://uncommonear.com/v1/tracks/fp-0000000001/similar?limit=5" \
-H "Authorization: Bearer $UNCOMMON_EAR_API_KEY"Each track has these fields. We may add fields later, so ignore the ones you don't use.
| Field | Type | Description |
|---|---|---|
| id | string | The track id. Use it with the track and similar routes. |
| title | string | The track title. |
| artist | string | The artist. Can be empty when the source gives none. |
| kind | string | music or sfx. |
| duration_s | number | Length in seconds. |
| bpm | number or null | Beats per minute, or null when the track has no steady tempo. |
| tags | object | Lists of genre, mood, use, instrument, and sfx tags. |
| license | object | id (such as cc-by-4.0), name (such as CC BY 4.0), url (the license text), and status: verified (we checked the source page) or listed (the collection says so, and we have not checked). |
| terms | object | What the license lets you do: commercial (true or false), credit (none or required), and share_alike (true or false). |
| attribution | string | optional, requested, or required. required means the license requires credit. requested means the license does not, but the creator or source asks. See the next section. |
| credit_line | string | A ready-to-paste credit line. Use it when attribution is requested, or whenever you want to give credit. |
| source_page | string or null | The page where the source publishes the track. |
| preview_url | string or null | A URL you can play. Previews are lower quality than the original. |
| page_url | string | A link to the track in the Uncommon Ear catalog. |
Tracks come under different licenses, and each result carries its license and terms. CC0 tracks need no permission and no credit, including in commercial work. Other licenses can require credit, require you to share what you make under the same license (share-alike), or rule out commercial use. The attribution field tells you which credit case you're in:
optional: nobody asks for credit.requested: the license doesn't require credit, but the creator or source asks. Include the credit_line wherever you publish the track.required: the license requires credit. Include the credit_line, which names the license and links its text.To find only the tracks that need no credit, search with credit=optional. To find the ones that ask for it or require it, use credit=requested. To find tracks you can use in commercial work, use commercial=true, and to skip share-alike tracks, use share_alike=false.
Every error is JSON with two fields: error, a short code to check in your program, and message, an explanation in plain words.
{
"error": "invalid_parameter",
"message": "The kind parameter must be one of music, sfx."
}| Status | Code | Meaning |
|---|---|---|
| 400 | invalid_parameter | A parameter is not valid or not recognized. The message names it. |
| 401 | missing_api_key | The request has no Authorization header. |
| 401 | invalid_api_key | The key is not valid, or it was revoked. |
| 404 | not_found | No track has that id, or the route does not exist. |
| 405 | method_not_allowed | The API only answers GET requests. |
| 429 | rate_limited | Over the limit. The Retry-After header gives the seconds to wait. |
| 500 | internal_error | Something went wrong on our side. Try again soon. |
| 503 | unavailable | The catalog is not available right now. Try again soon. |
The two 401 errors have the same fix: send a valid key. For a revoked key, create a new one under API keys in the account menu.
The limits depend on your plan:
| Plan | Per minute | Per day | Per month |
|---|---|---|---|
| Free | 20 calls | 300 calls | No limit |
| Pro | 60 calls | No limit | 20,000 calls |
Free accounts can make 20 calls per minute and 300 calls per day. Pro accounts can make 60 calls per minute and 20,000 calls per month, with no daily limit. A month is a calendar month in UTC. The limit belongs to your account, not to a key, and it's shared with the MCP server: calls from your keys and from your connected assistants count together. Calls that fail with a 400 or 404 error count too.
Over a limit, the API returns status 429 with a Retry-After header that gives the number of seconds to wait, up to 86,400. The response body also has limit (minute, day, or month) and reset_at, the UTC time when that limit resets. Wait that long before you retry, and spread a long job over time instead of sending bursts. Calls with a missing or invalid key return 401 and don't count against any account.
For each call, we record your account, which key made it, the route, whether it worked, how long it took, and how many results it returned. For searches, we also record the search text. We never store the key itself or the Authorization header, and we don't record your IP address or the results. We remove the search text after 90 days and the rest after 400 days. Read the details in the privacy policy.
Make clips and save to collections.