Tracklists as JSON.
Read every public set, artist and track on ryser.id with a free key. Plain HTTPS, JSON in and out, any origin, no SDK needed. Scanning new sets is a separate, paid door at the bottom.
Authentication
Create a key under Account → API. Any account can hold up to five keys; no plan is needed to read public data. A key starts with rysk_, is shown once at creation and stored as a hash. Send it as a bearer token on every request.
GET /api/public/search?q=cercle HTTP/1.1 Host: ryser.id Authorization: Bearer rysk_your_key_here
A missing or malformed header returns 401 with a message that says where to get a key. A revoked key returns the same status. The read key never spends minutes; only the scan endpoints do.
Conventions
- Base URL
https://ryser.id. All read endpoints live under/api/public/. - Responses are JSON with
snake_casefields. Times are ISO 8601 in UTC; durations and positions are seconds;startis also given asm:ssorh:mm:ssfor display. - Every object carries its public
urlon ryser.id. Show it when you show the data. - CORS is open (
Access-Control-Allow-Origin: *), so a browser can call the API directly. Keep the key server-side anyway; a key in client code is a key anyone can copy. - Responses are cached for five minutes at the edge (
Cache-Control: public, max-age=300). Polling faster than that returns the same body. - Rate limit: 60 requests per minute per key. Over it you get
429; wait for the next minute. - Only public, finished analyses are visible. A set someone kept private is a
404here, exactly as on the site.
/api/public/searchfree keyThree modes on one endpoint: free-text search, look-up by set link, or the newest sets.
qstring | DJ name, event, channel or words from the title. Case-insensitive substring match on title and uploader. |
urlstring | A YouTube, SoundCloud or Mixcloud link. The link is canonicalised (tracking parameters and short links resolved) before matching. Wins over q and latest. |
latestlibrary | community | The newest public sets, newest first. library = the sets ryser.id scans daily from the big channels; community = sets people analysed and shared. |
limitinteger, 1–50, default 10 | Maximum number of sets returned. Ignored in url mode. |
curl "https://ryser.id/api/public/search?q=boiler%20room&limit=5" -H "Authorization: Bearer rysk_…"
{ "query": "boiler room", "sets": [
{ "id": "9c2e…", "url": "https://ryser.id/s/9c2e…",
"title": "Boiler Room: Ben UFO in London", "uploader": "Boiler Room",
"source_url": "https://www.youtube.com/watch?v=…", "duration_seconds": 5412,
"identified_tracks": 31, "origin": "library", "finished_at": "2026-09-20T03:41:12.000Z" },
…
] }
curl "https://ryser.id/api/public/search?url=https://soundcloud.com/…" -H "Authorization: Bearer rysk_…"
# 200 { "found": true, "set": { … } }
# 404 { "found": false, "analyse_url": "https://ryser.id/?url=…" } ← nobody analysed it yet; send people there
curl "https://ryser.id/api/public/search?latest=library&limit=20" -H "Authorization: Bearer rysk_…"
# { "sets": [ … ] }/api/public/sets/{id}free keyOne public set with its full tracklist. The id is the uuid from search or from a ryser.id URL (https://ryser.id/s/<id>). Rows come in playback order; unidentified segments are present with status unknown so the timeline stays complete.
curl https://ryser.id/api/public/sets/9c2e… -H "Authorization: Bearer rysk_…"
{ "id": "9c2e…", "url": "https://ryser.id/s/9c2e…", "title": "…", "uploader": "…",
"source_url": "…", "duration_seconds": 5412, "identified_tracks": 31, "origin": "library",
"finished_at": "2026-09-20T03:41:12.000Z",
"tracks": [
{ "index": 0, "start_seconds": 0, "end_seconds": 318, "start": "0:00",
"artist": "Da Capo", "title": "Kelaya", "status": "matched",
"bpm": 122, "key": "F min",
"artist_url": "https://ryser.id/artists/da-capo",
"track_url": "https://ryser.id/tracks/da-capo-kelaya",
"soundcloud_url": "https://soundcloud.com/search?q=Da%20Capo%20Kelaya",
"beatport_search_url": "https://www.beatport.com/search?q=Da%20Capo%20Kelaya" },
{ "index": 1, "start_seconds": 318, "end_seconds": 611, "start": "5:18",
"artist": "", "title": "", "status": "unknown", "bpm": 124, "key": null, … },
…
] }404 { "error": "not a public set" } when the id does not exist, is private, or is still running.
/api/public/artists/{slug}free keyA canonical artist. The slug is the lowercase hyphenated name (black-coffee, amelie-lens); the same slug is in every artist_url a tracklist row carries. Spelling variants and aliases resolve to the same artist, so the counts here include every way the name was heard.
curl https://ryser.id/api/public/artists/black-coffee -H "Authorization: Bearer rysk_…"
{ "slug": "black-coffee", "name": "Black Coffee", "url": "https://ryser.id/artists/black-coffee",
"links": { "instagram": "https://www.instagram.com/…", "spotify": "…", "soundcloud": "…", "youtube": null, "website": null },
"identified_tracks": 142, "played_in_sets": 37,
"top_tracks": [ { "artist": "Black Coffee", "title": "Wish You Were Here", "plays": 9 }, … ],
"sets_performed": [ { …set summary… }, … ], // sets this artist played (max 20)
"played_in": [ { …set summary… }, … ] } // sets where their records were heard (max 20)404 { "error": "unknown artist" } for a slug that is not a canonical artist yet.
/api/public/tracks/{slug}free keyA canonical record: the versions that were heard (original, remixes, edits, extended mixes) and every public set it appears in, with the position. Slug is artist-title without the version tag, as in every track_url a tracklist row carries.
curl https://ryser.id/api/public/tracks/da-capo-kelaya -H "Authorization: Bearer rysk_…"
{ "slug": "da-capo-kelaya", "title": "Kelaya", "artist": "Da Capo",
"url": "https://ryser.id/tracks/da-capo-kelaya",
"artist_url": "https://ryser.id/artists/da-capo",
"versions": [
{ "artist": "Da Capo", "title": "Kelaya", "version": "Original", "plays": 12 },
{ "artist": "Da Capo", "title": "Kelaya (Extended Mix)", "version": "Extended Mix", "plays": 4 } ],
"appears_in": [
{ …set summary…, "at_seconds": 2711, "at": "45:11" },
…
] }404 { "error": "unknown track" } for a slug that is not a canonical track yet.
/api/public/tracklist?url={set link}no keyThe one keyless endpoint, built for the browser extension: given the link of the page a person is on, is there a public tracklist? It returns a compact list without the per-row links and is meant for showing a tracklist under a player, not for crawling. For anything else use the keyed endpoints above.
curl "https://ryser.id/api/public/tracklist?url=https://www.youtube.com/watch?v=…"
200 { "found": true, "run_id": "9c2e…", "url": "https://ryser.id/s/9c2e…", "title": "…", "uploader": "…",
"duration": 5412, "identified": 31, "total": 34,
"tracks": [ { "start": 0, "artist": "Da Capo", "title": "Kelaya", "bpm": 122, "key": "F min" }, … ] }
404 { "found": false, "analyse_url": "https://ryser.id/?url=…" }Objects
The set summary is the same shape everywhere it appears: search results, an artist's sets, a track's appearances.
idstring (uuid) | Stable id of the analysis. Use it with /api/public/sets/{id}. |
urlstring | Public page on ryser.id. |
title / uploaderstring | null | Title and channel or profile as published on the source platform. |
source_urlstring | null | Canonical link of the set on YouTube, SoundCloud or Mixcloud. |
duration_secondsinteger | null | Length of the audio. |
identified_tracksinteger | Rows with status matched. |
originlibrary | community | library: scanned automatically by ryser.id from a followed channel, first pass with one engine. community: analysed by a person and shared. |
finished_atISO datetime | null | When the analysis completed. |
A tracklist row:
indexinteger | Position in the set, from 0. |
start_seconds / end_secondsnumber | null | Where the segment starts and ends in the audio. end_seconds can be null on the last row. |
startstring | start_seconds formatted for display. |
artist / titlestring | Empty strings when the row is unknown. |
statusmatched | unknown | uncertain | matched: identified and corroborated. uncertain: one source named it and nothing confirmed it; show it as a guess. unknown: heard, not identified. |
bpm / keynumber | null, string | null | Tempo and musical key measured from the audio, per segment; present on unknown rows too when measurable. |
artist_url / track_urlstring | Canonical artist and track pages on ryser.id. |
soundcloud_url / beatport_search_urlstring | null | Search links for the record on those platforms. |
Code examples
Read the key from the environment; never ship it to a browser.
// Node 18+, Deno, Bun, or a Next.js server component
const KEY = process.env.RYSER_API_KEY;
async function tracklist(setId) {
const res = await fetch(`https://ryser.id/api/public/sets/${setId}`, {
headers: { Authorization: `Bearer ${KEY}` },
});
if (res.status === 404) return null; // private, running or unknown
if (!res.ok) throw new Error(`ryser.id ${res.status}: ${(await res.json()).error}`);
return res.json();
}
const set = await tracklist("9c2e…");
for (const t of set.tracks) {
console.log(t.start, t.status === "matched" ? `${t.artist} – ${t.title}` : "(unknown)");
}import os, requests
KEY = os.environ["RYSER_API_KEY"]
H = {"Authorization": f"Bearer {KEY}"}
def find_by_link(url: str):
r = requests.get("https://ryser.id/api/public/search", params={"url": url}, headers=H, timeout=20)
if r.status_code == 404:
return None # r.json()["analyse_url"] sends someone to analyse it
r.raise_for_status()
return r.json()["set"]
def artist(slug: str):
r = requests.get(f"https://ryser.id/api/public/artists/{slug}", headers=H, timeout=20)
r.raise_for_status()
return r.json()
s = find_by_link("https://www.youtube.com/watch?v=…")
if s:
print(s["title"], s["identified_tracks"], "tracks", s["url"])# A one-liner for the terminal: every matched row of a set as "start artist – title" curl -s https://ryser.id/api/public/sets/9c2e… -H "Authorization: Bearer $RYSER_API_KEY" \ | jq -r '.tracks[] | select(.status=="matched") | "\(.start)\t\(.artist) – \(.title)"'
Scan API (Studio)
Starting a new analysis from code uses the same key and the same minutes as the app. It is part of the Studio plan; a free key gets 403 here. A 60-minute set takes about 17 minutes; poll, do not hammer.
/api/v1/runsStudio keyQueue a YouTube, SoundCloud or Mixcloud link. Costs one minute of balance per minute of audio, deducted when the analysis starts. If a public analysis of the same link already exists you get a free copy back at once with duplicate: true and status done.
curl -X POST https://ryser.id/api/v1/runs \
-H "Authorization: Bearer rysk_…" -H "Content-Type: application/json" \
-d '{"url": "https://www.youtube.com/watch?v=…"}'
201 { "run_id": "1f0c…", "status": "queued" }
201 { "run_id": "7a90…", "status": "done", "duplicate": true }/api/v1/runs/{id}Studio keyStatus and the setlist so far. Poll every 30 to 60 seconds until status is done or failed. Rows stream in while it runs, so a partial tracklist is normal before completion.
{ "id": "1f0c…", "status": "done", "title": "…", "duration_seconds": 3612,
"matched_tracks": 24, "total_tracks": 27,
"tracks": [ { "index": 0, "start_seconds": 0, "end_seconds": 312, "artist": "…", "title": "…",
"status": "matched", "bpm": 128, "key": "A min", "provider": "fingerprint" }, … ] }/api/v1/runsStudio keyYour 50 most recent runs, newest first, with status and counts.
Limits and errors
| Read endpoints | 60 requests per minute per key. Cached five minutes. Max 50 results per call. |
| Scan endpoints | 30 runs per 10 minutes per account, shared with the web app. Studio plan. |
| Keys | Up to five active keys per account. Revoke and recreate under Account at any time. |
| Extension endpoint | No key; meant for one request per page view from the extension. Use the keyed endpoints for anything else. |
Errors are JSON with one field:
{ "error": "Slow down: 60 requests per minute per key." }400bad request | Missing url, invalid JSON, or a link that is not YouTube, SoundCloud or Mixcloud. |
401unauthorised | No key, malformed header, or a revoked key. |
402payment required | Scan only: not enough minutes for this set. The message says how many it needs. |
403forbidden | Scan only: the key's account is not on Studio. |
404not found | Not a public set, unknown artist or track, or no tracklist for that link yet. |
429too many requests | Rate limit hit. Wait for the next minute (read) or ten minutes (scan). |
503worker offline | Scan only: the analysis worker is unreachable. Retry in a minute. |
Beta. Endpoints and field names are stable; new fields may appear without notice, so tolerate unknown keys. Questions, wishes, higher limits: [email protected]. Want an assistant to do this for you instead? See the MCP server.