The map index behind the Browse page has a small public API. It is read-only, needs no account and returns JSON (the chart file excepted). Please be gentle: answers are cached, and requests are limited per address.
Chart identity: the note hash
A chart is identified by its note hash: 64 lowercase hexadecimal characters, a BLAKE3 digest of the key count and of
every note (start time, column, kind, end time of a long note) in whole milliseconds, relative to the first note.
The same notes give the same hash whether they come from an .osu, .sm, .ssc or .qua file, and whatever the
file is called; mines and bursts are ignored; the audio and the metadata do not count. The hash version is returned as
noteHashVersion. The index only holds osu!mania and Etterna charts of 4 to 7 keys.
GET /api/charts/{hash}
Where can this chart be downloaded? A malformed hash answers 400; a well-formed hash the index does not know answers
200 with status: "unknown" (so that a client can ask without error handling).
{
"chartHash": "9f2c…64 hex…",
"status": "known",
"sources": [
{ "kind": "osuSet", "ref": "4001", "confidence": "verified", "checkedAtMs": 1791600000000 }
],
"ttlMs": 300000,
"chart": {
"hash": "9f2c…", "noteHashVersion": 1, "keys": 7,
"bpmMin": 120.0, "bpmMax": 180.0, "bpmMain": 180.0,
"lengthMs": 144000, "notes": 1200, "lns": 40,
"densityAvg": 8.3, "densityPeak": 14.0, "roxBytes": 5120,
"title": "Song", "artist": "Artist", "creator": "Mapper", "difficulty": "7K Hard"
}
}
kind is an open set (osuSet, etternaPack, ...): ignore kinds you do not know. ref is an identifier, never a
URL: an osu! set id, an EtternaOnline pack id. The page of the source is
https://osu.ppy.sh/beatmapsets/<ref> or https://etternaonline.com/packs/<ref>. The answer carries an ETag and
Cache-Control: public, max-age=300.
GET /api/charts/{hash}/rox
The chart in the compact ROX format (the open exchange format of the game), served on request as an attachment
(application/octet-stream, ETag: "<hash>", immutable). 404 if the chart is unknown or was removed. Never audio,
never images.
GET /api/charts/{hash}/scores
The online board of a chart: each player once, with their best play. The game server verified every play and replayed it on its own copy of the chart before writing it into the site's database, and it reads every play under the Prism judgement (the judgement a player chose is only the record of what they played with: there is no judgement dimension on the boards). Accuracy, combo and misses are the Prism ones, never the player's claim. Best rating first (the MinaCalc 515 rating the server computed; a play without one last), then the Prism accuracy, then the highest combo, then the play recorded first. The plays of a banned account, or of an account scheduled for deletion, are not listed.
| Query parameter | Meaning |
|---|---|
rate |
absent or all: THE board of the chart, each player once with their best play whatever its speed. An exact playback rate, 0.5 to 2.0: the same ranking over the plays of that speed, each player once too (total counts players) |
limit, offset |
limit is 1 to 100 (default 50), offset at most 10000 |
There is no judgement parameter any more; an unknown parameter (an old client's judgement=) is ignored, not an error.
{
"chartHash": "9f2c…64 hex…", "rate": 1.0,
"total": 132, "offset": 0, "limit": 50,
"items": [
{ "id": 4821, "hasReplay": true, "rank": 1, "username": "Ada", "userId": "0190…uuid…", "accuracy": 99.12, "maxCombo": 842, "misses": 1,
"counts": [610, 120, 60, 30, 12, 6, 3, 1], "rating": 24.5, "playedAtMs": 1791600000000, "playbackRate": 1.0 }
]
}
A chart nobody played is total: 0 with no items, never an error; a malformed hash or parameter answers 400 and names
it. rank counts over the whole board, not the page. playedAtMs is the date the player's game claims. userId is the account's UUID (lowercase): the game compares it to
its own account to spot "my score". counts (the eight Prism tiers, the Miss last) and rating are absent from a play stored before the server kept them.
GET /api/charts/{hash}/boards
The rates a chart has, so that a page can offer only the choices that lead to a board with plays: one entry per playback rate with at least one visible play, by rate ascending (at most 200). There is no judgement in it.
{
"chartHash": "9f2c…64 hex…",
"boards": [
{ "rate": 1.0, "plays": 132 }
]
}
plays counts the players of that rate (the total of GET /api/charts/{hash}/scores?rate=). Plays of banned accounts and of accounts scheduled for
deletion are not counted. A chart nobody played is an empty list, never an error; a malformed hash answers 400.
GET /api/chartsets
The chartsets (one per song folder of an osu! set or an Etterna pack), newest first by default.
| Query parameter | Meaning |
|---|---|
q |
text found in the title, artist, creator or tags |
keys |
comma-separated key counts, e.g. 4,7 |
source |
osuSet or etternaPack |
creator, artist, title |
part of the name (with creatorMatch, artistMatch, titleMatch = exact for an exact, case-insensitive match) |
difficulty |
a difficulty name of one of the set's charts (difficultyMatch as above) |
tags |
text found in the tags |
bpmMin, bpmMax |
BPM range overlapping the set's charts |
lengthMin, lengthMax |
length in seconds |
densityMin, densityMax |
peak notes per second |
ratingCalc, ratingMin, ratingMax |
difficulty range by calculator: minacalc (default), osu, quaver or sunny, at 1.0× |
sort |
updated (default), title, artist, bpm, length, density, rating (by ratingCalc) |
page, perPage |
perPage is at most 60 |
The answer is { "items": [...], "total": n, "page": p, "perPage": n }; each item carries id, title, artist,
creator, sourceKind, keys, bpmMin, bpmMax, lengthMinMs, lengthMaxMs, densityMax, chartCount and
downloadUrl (/go/chartset/<id>, absent for a set with no public source).
GET /api/chartsets/{id}
One chartset: the fields above plus tags, sources and charts (each with its chart object, its difficulty name
in this set and, for osu!, its osuBeatmapId).
GET /go/chartset/{id}
A 302 redirect to the page of the original source, where the map is downloaded. 404 for a set without a public source.
Limits and manners
- Requests are limited per address (240 per minute by default for each of the four groups: chart lookups,
.roxfiles, boards, the chartset list); beyond it the answer is429withRetry-After. - Use
ETag/If-None-Match; do not poll. - Put a way to reach you in your
User-Agent. - Creators are credited wherever a chart is shown. To have an entry removed, use the Contact page.