18-LAB / Documentation
HTTP API
Overlay/data base URL: http://127.0.0.1:24050 (SERVER_IP:SERVER_PORT). Dashboard/management base URL: http://127.0.0.1:24051 (DASHBOARD_IP:DASHBOARD_PORT), separate in vosu 1.1.0. Methods and paths are exact and case-sensitive. Unsupported combinations return 404, not 405; there are no implicit HEAD or OPTIONS handlers. WebSocket upgrades remain on the overlay origin; the dashboard refuses all upgrades. See the WebSocket reference.
All requests pass the configured trusted-peer policy; rejection is 403 Not Found. Dashboard mutations additionally require exact dashboard-origin provenance and a per-start X-Vosu-CSRF token injected only into dashboard pages. Origin is authoritative, or Referer only when Origin is absent; missing, duplicate, malformed, or opaque provenance otherwise returns 403. Overlay-origin browser management writes fail with 403; explicitly scoped API keys and the loopback native broker credential still authorize value/profile writes there. POST bodies are capped at 1 MiB; larger bodies return 413 {"error":"payload too large"}. CORS response headers do not bypass these checks. See security.
GET /api/dashboard on the overlay origin discovers the effective dashboard origin. Old overlay-origin GET links to /, /settings, and /available return 302, preserving path and query. Redirect authorities come from the effective listener, not arbitrary Host values; wildcard dashboard binds map to loopback for local discovery. Overlay URLs and their browser storage stay unchanged; no overlay files are served by the dashboard.
API keys
vosu only Added in vosu 1.1.0. Create a named key in Settings → API keys and copy its secret when shown; it cannot be recovered later. Native HTTP clients send Authorization: Bearer <key>.
| Scope | Authorized writes |
|---|---|
settings:write |
Command-socket writes and POST /api/counters/settings/<name> value saves, not ?update=yes schema saves |
profiles:manage |
All POST /api/counters/profiles actions |
Neither scope grants updates, counter installation/deletion, reports, daemon settings, or key management; those remain dashboard-origin provenance plus CSRF only. Scoped key writes remain on the overlay origin. Invalid/revoked keys and insufficient scopes return 401, with JSON for HTTP POSTs. POSTs re-check the granted scope after receiving the body and before dispatch, so revocation during an upload prevents the pending mutation.
For a native value save, set VOSU_API_KEY to your saved settings:write secret:
curl -H "Authorization: Bearer $VOSU_API_KEY" -H 'Content-Type: application/json' \
--data '[{"uniqueID":"title","value":"My overlay"}]' \
http://127.0.0.1:24050/api/counters/settings/my-overlayKeys never authenticate through URL query parameters. Cross-origin browser HTTP is not supported: CORS does not allow Authorization. For browsers, use the command WebSocket subprotocols. Remote peers must still be allowed by ALLOWED_IPS; a key never bypasses that policy. Plain HTTP/WS on a LAN exposes the secret to network observers: keep loopback by default and use a trusted TLS reverse proxy for remote access.
Keys persist across restarts, unlike the per-start loopback broker.json token. The file token provides zero-config native broker integration and authorizes command-socket writes, HTTP settings value saves, and profile actions from loopback. It never authorizes schema saves or management.
Dashboard-only key management
Both GET /api/keys and POST /api/keys live on the dashboard origin and require exact dashboard-origin provenance plus X-Vosu-CSRF; an API key cannot manage itself or other keys. GET returns {keys:[...]} metadata without secrets or hashes. POST accepts:
| Action | JSON body | Result |
|---|---|---|
| Create | {action:"create",name,scopes} |
{key,entry}; secret returned once |
| Rename | {action:"rename",id,name} |
Changes the name, not scopes |
| Revoke | {action:"revoke",id} |
Immediately closes associated command sockets with 1008, even while idle |
Names are 1–64 printable characters excluding Unicode bidirectional controls. scopes contains one or both scope values above. To change permissions, revoke and recreate the key.
Sources: API key README and examples, ADR 0015.
State and calculation
| Method + path | Parameters | 200 response | Other statuses |
|---|---|---|---|
GET /json |
None | gosuMemory-compatible v1 object | 500 readiness/serialization error |
GET /json/sc |
None | streamCompanion-compatible token object | 500 readiness/serialization error |
GET /json/v2 |
None | v2 document | 500 readiness/serialization error |
GET /json/v2/precise |
None | precise document | 500 readiness/serialization error |
GET /api/calculate/pp |
Optional query parameters below | Legacy rosu-pp performance object | 400 input/calculation error; 500 readiness/serialization error |
Readiness errors are {"error":"osu is not ready/running"}. Calculation errors use {"error":"message"}. An explicit valid path can select a file without a current live beatmap.
Calculation query
| Parameter | Type / meaning |
|---|---|
path |
Canonical regular .osu file to calculate; otherwise current map. vosu honors the supplied file; the upstream tosu implementation checks path but reads the current map. |
lazer vosu only |
Boolean text true or false; score-system selection |
mode |
Integer: 0 osu!, 1 taiko, 2 catch, 3 mania |
ar, cs, hp, od |
Numeric difficulty overrides |
clockRate |
Finite number in 0.01–100 |
passedObjects, combo, nMisses, n100, n300, n50, nGeki, nKatu |
Unsigned 32-bit integer counts |
acc |
Numeric accuracy |
sliderEndHits, smallTickHits, largeTickHits |
Unsigned 32-bit integer counts |
hitresultPriority |
0 best case (default), 1 worst case |
mods |
Legacy unsigned bitmask; vosu also accepts JSON mod objects/arrays and two-letter acronyms vosu only |
All parameters are optional. Unknown query keys are ignored. Counts accept integral numeric text (including exponent and hexadecimal/binary/octal notation), reject negatives/fractions/nonfinite values, and must fit u32. Acronym strings may contain whitespace or commas, for example HD,DT; structured mods contain acronym and optional settings.
Inputs are capped at 64 MiB, including opened-file checks and bounded reads. The live calculation facade additionally limits parsed objects to 250,000 with finite start times within ±10,000,000 ms; live graph output is capped at 1,000,000 aggregate bins. These do not constitute a global concurrent-memory ceiling.
The performance response contains difficulty, state, and numeric pp; optional numeric fields include ppAim, ppFlashlight, ppSpeed, ppAccuracy, ppDifficulty, effectiveMissCount, estimatedUnstableRate, speedDeviation, comboBasedEstimatedMissCount, scoreBasedEstimatedMissCount, aimEstimatedSliderBreaks, and speedEstimatedSliderBreaks. state contains numeric maxCombo, osuLargeTickHits, osuSmallTickHits, sliderEndHits, nGeki, nKatu, n300, n100, n50, and misses.
difficulty always contains numeric mode, stars, maxCombo, and boolean isConvert. Ruleset-dependent numeric fields are omitted when absent: aim, aimDifficultSliderCount, speed, flashlight, sliderFactor, aimTopWeightedSliderFactor, speedTopWeightedSliderFactor, speedNoteCount, aimDifficultStrainCount, speedDifficultStrainCount, nestedScorePerObject, legacyScoreBaseMultiplier, maximumLegacyComboScore, hp, nCircles, nSliders, nLargeTicks, nSpinners, stamina, rhythm, color, reading, nFruits, nDroplets, nTinyDroplets, nObjects, nHoldNotes, ar, preempt, greatHitWindow, okHitWindow, mehHitWindow, monoStaminaFactor, mechanicalDifficulty, and consistencyFactor.
Current game files
| Method + path | Resource / path base |
|---|---|
GET /backgroundImage |
Selected map background (legacy alias) |
GET /files/beatmap/background |
Selected map background |
GET /files/beatmap/audio |
Selected map audio |
GET /files/beatmap/file |
Selected .osu file |
GET /Songs/{path} |
Relative to the current songs root |
GET /files/beatmap/{path} |
Relative to the current songs root, not just the selected map folder |
GET /files/skin/{path} |
Relative to the current stable skin root; lazer returns 500 {"error":"This endpoint is unavailable for the lazer client."} |
Paths are percent-decoded once (+ stays literal). Absolute paths, parent traversal, NULs, malformed encoding, and escapes from the selected root return 404. Missing/unavailable game state returns 500 readiness; missing ready resources return 404. Successful files return bytes with a MIME Content-Type and Content-Length. Directory requests can return 200 HTML listings or 301 trailing-slash redirects.
File serving accepts a validated single Range: bytes=... request: 206 with Content-Range for a valid range; 416 with Content-Range: bytes */<length> for invalid/unsatisfiable ranges. Accepted forms are bytes=start-end, bytes=start-, and bytes=-suffixLength. Multiple ranges, embedded whitespace, zero suffixes, and explicit end positions beyond EOF are rejected (not clamped). Full responses are 200; media extensions mp3, wav, ogg, gif, webm, mp4, avi, and webp additionally advertise Accept-Ranges: bytes. Stream/seek failures can return 500.
Pages and static content
/, /settings, /available, and embedded /assets/{path} belong to the dashboard origin. Counter-static fallback belongs only to the overlay origin. Dashboard read APIs serve its pages, while read-only state, calculation, game files, settings/profiles/catalog APIs remain on the overlay origin as documented by ADR 0016.
| Method + path | Parameters / response |
|---|---|
GET / |
Dashboard HTML |
GET /settings |
Daemon settings HTML; a nonempty ?overlay=<name> selects the hosted counter-settings page |
GET /available |
Third-party catalog HTML |
GET /favicon.ico |
Icon bytes; adds X-Vosu-Identity: vosu vosu only |
GET /assets/{path} |
Embedded dashboard assets, 200 bytes or 404 |
GET /{counter}/{path} (fallback) |
Counter/static-root files; /{counter}/ selects index.html |
Dynamic page rendering can return 500 JSON errors. Counter fallback uses 200 bytes/HTML, 301 directory redirects, 404 missing/unsafe paths, and the file range behavior above. Successful full counter HTML responses append a script setting window.COUNTER_PATH to the logical parent directory. Use counter packages for its timing and settings contract. API and WebSocket reserved paths are not counter-static fallbacks.
Removed in-game page deprecated
GET /api/ingame is not served: the router returns 404. Dedicated __ingame__ storage and catalog commands were removed in ADR 0008. Do not depend on this tosu endpoint, including for your own settings/configuration. In-game rendering belongs to vosulay; use per-counter settings instead.
Counters and settings
The management write routes below live on the dashboard origin and require dashboard provenance plus X-Vosu-CSRF management. The overlay origin retains read-only settings/profile/catalog APIs and explicitly scoped API-key or loopback native broker value/profile writes, not schema edits or general management. They are not an overlay's general-purpose API. Most domain errors deliberately use HTTP 200 with an error field: check the body, not just response.ok.
| Method + path | Input | Response / status |
|---|---|---|
GET /api/counters/search/{query} |
Percent-encoded search text; source=installed (default) or source=available vosu only |
200 card HTML; 400 Error: unknown search source; 502 escaped Error: HTML on lookup failure |
POST /api/counters/download/{url} vosu only management |
Encoded HTTPS ZIP URL; required name single folder component; update=true permits replacement; body unused |
200 {"status":"Finished"} or {"error":"..."}; 429 install capacity full |
POST /api/counters/open/{name} vosu only management |
Encoded counter folder name; body unused | 200 {"status":"opened"} or error |
POST /api/counters/open-root vosu only management |
No parameters; body unused | 200 {"status":"opened"} or error; opens configured static root |
POST /api/counters/delete/{name} vosu only management |
Encoded counter folder name; body unused | 200 {"status":"deleted"} or error |
GET /api/counters/settings/{name} |
Encoded counter name | 200 {"settings":[...],"values":{...}} or error |
POST /api/counters/settings/{name} management |
JSON array of { "uniqueID": "id", "value": ... } |
200 {"result":"success"} or error; debounced value persistence and settings refresh |
POST /api/counters/settings/{name}?update=yes management |
JSON array of complete CounterSetting descriptors | 200 {"result":"success"} or error; updates package schema |
POST /api/settingsSave management |
JSON daemon configuration binding map | 200 {"status":"updated"}; 500 JSON parse/save/validation error |
Search retains letters, digits, and whitespace; any word can match name, author, or compatible clients. See package/settings descriptors for descriptor types and storage. Install admission is four shared concurrent jobs; HTTPS/private-destination/ZIP limits are documented there. Unlike tosu, download/open/delete are POST-only; their former GET forms return 404 with no redirect.
Daemon saves reject the whole request if interval/port values are nonfinite or outside: POLL_RATE 100–60000 ms, PRECISE_DATA_POLL_RATE 1–60000 ms, V2_SUBSCRIPTION_DEBOUNCE 1–60000 ms vosu only, and both SERVER_PORT and DASHBOARD_PORT integral 1–65535. Network fields require _network_action: "apply", followed by _network_action: "confirm" from the new dashboard origin within 30 seconds, or server-side rollback restores the previous network fields/listeners even after the browser closes. Only one edit can be pending; other settings autosave. See configuration.
Settings profiles
vosu only Added in vosu 1.1.0. Profiles group per-overlay values while package schemas stay shared.
| Method + path | Input | Response |
|---|---|---|
GET /api/counters/profiles |
None | Initially {"active":"default","profiles":["default"]} |
POST /api/counters/profiles management |
One JSON action below | Updated registry on success; HTTP 200 {"error":"..."} on domain errors, including unknown actions |
| Action | JSON body |
|---|---|
| Create empty profile | {"action":"create","name":"stream"} |
| Create by copying | {"action":"create","name":"stream","copyFrom":"default"} |
| Rename | {"action":"rename","name":"stream","target":"live"} |
| Delete | {"action":"delete","name":"live"} |
| Set as default | {"action":"activate","name":"stream"} |
| Copy one overlay's values | {"action":"copyOverlay","name":"counter-folder","source":"default","target":"stream"} |
POST requires dashboard-origin provenance plus CSRF, an API key with profiles:manage on the overlay origin, or the loopback native broker credential on that origin, and uses the same body limits as other management writes. Credential authorization is rechecked after reading the body. Names are case-sensitive references but case-insensitively unique: 1–48 ASCII letters, digits, _ or -. At most 32 profiles including reserved default are allowed. default cannot be renamed/deleted; deleting the default profile selects default.
GET /api/counters/settings/<name>?profile=stream keeps the existing {"settings":[...],"values":{...}} response. POST to that URL accepts the usual [{"uniqueID":"field","value":...}] and returns {"result":"success"}. Omit profile to use the global active profile. Explicit unknown profiles return a JSON error, not a write to a fallback profile. update=yes still edits the shared developer schema for every profile.
default keeps <config>/settings/<counter>.settings.values.toml; other profiles use <config>/settings/profiles/<profile>/<counter>.settings.values.toml. <config>/settings/profiles.json stores the names and active selection; files are atomically replaced within their own directory. Missing overlay values use schema defaults, never another profile's saved values.
/settings?overlay=<name>&profile=<p> on the dashboard origin opens the inline overlay editor for a profile; without profile, it uses the default profile (API field active). It and the dashboard card editor share a profile dropdown and ⋯ menu with Create, Copy, Rename, Delete, and Set as default (API action activate), plus title/description/ID search and header navigation (absent for schemas with no headers). The dropdown adds (default) to the profile used by unpinned overlays, except the reserved default profile. Matching sections retain their headers.
All dashboard pages forbid framing with frame-ancestors 'none'; no query parameter enables it. Native editors such as vosulay use the settings and profiles HTTP API through their Rust host. Dashboard pages send no parent-window notifications.
For an individual overlay page, use ?vosuProfile=<name>, not the editor's profile query. The injected qualified COUNTER_PATH selects the profile in commands sockets. Activating profiles refreshes unpinned clients; saving/copying values refreshes clients pinned to that profile and unpinned clients when it is active.
Reports and updates
Both routes below are dashboard-origin management APIs, requiring its provenance and X-Vosu-CSRF. The UI labels report download Diagnostics report and uses an authenticated fetch.
| Method + path | Input | Response |
|---|---|---|
POST /api/generateReport vosu only management |
None; body unused | 200 HTML attachment (diagnostic report); 500 JSON error |
POST /api/runUpdates vosu only management |
None; body unused | 200 JSON status: updated, latest, or noFiles; or error |
Report generation is serialized (one active report). It escapes inserted machine/configuration/counter/log data. Logs: ten newest files, at most 1 MiB/10,000 content lines each, and 8 MiB/100,000 lines overall, with truncation notices. A report can contain private diagnostics; never generate or export one silently from an overlay.
Externally managed installs return a 200 JSON error instead of replacing the binary. Updated responses are flushed before the deferred replacement. These POST methods intentionally differ from tosu's GET routes.