Music Hub API
Current desktop hub API for v0.2.2.
Base URL
http://localhost:38472
All examples use this URL.
Plugin API
The Plugin Host API is separate from the hub HTTP API. Plugins communicate
through a permissioned, versioned host bridge rather than calling privileged
/api/* routes directly. The public preview currently defines manifests,
bundle verification, permission-filtered declarative contributions and safe
metadata proposals; it does not publish arbitrary plugin HTTP access or an
executable third-party runtime. See Plugins for the contract.
Request classes
The backend treats requests differently depending on where they come from:
- Local loopback:
localhost,127.0.0.1,::1 - Remote socket: LAN, Personal Private network, direct IP, or other non-loopback clients
- Proxied remote: Cloudflare, Personal Private network tunnel, or other forwarded requests
Authentication model
Public endpoints
These are reachable without auth:
GET /api/pingGET /api/infoPOST /api/auth/pairPOST /api/auth/refreshGET /api/share/{playlist|artist|album|track}/:id/bootstrapPOST /api/share/session/heartbeat
Local-only admin endpoints
These return 403 when called remotely:
GET /api/auth/statusPOST /api/auth/armGET /api/connect/qrGET /api/tunnel/qrPOST /api/database/rebuild
Authenticated remote endpoints
Remote API reads and writes require either:
- a bearer access token from the pair/refresh flow, or
MUSIC_HUB_API_TOKEN
If auth is missing, the server returns 401.
Important scope rule:
publicRemoteScope = standalone_onlystill limits the anonymous public web/share surface- a valid paired-device bearer token still keeps access to the private mobile API/media surface, including search, browse, playback session, and media-session issuance
Licensing and remote access
Remote access is no longer only a networking question.
Current rule:
- LAN / Wi-Fi access can still work locally
- remote access depends on a valid desktop entitlement
When remote is blocked by licensing state, clients should expect an explicit reason such as:
{
"status": "unbound",
"remoteLicensed": false,
"reason": "license_required_for_remote"
}Database health gate
If the database is not healthy, most /api/* routes return:
{
"error": "Database unavailable",
"code": "database_unavailable",
"databaseHealth": {
"status": "faulted"
}
}Health/info, auth bootstrap, and recovery routes stay available so the client can diagnose and repair the hub.
Core endpoints
GET /api/ping
{
"status": "ok",
"timestamp": 1700000000000
}GET /api/info
{
"id": "local-hub",
"name": "Music Library",
"version": "0.1.41",
"stats": {
"trackCount": 1250,
"albumCount": 85,
"artistCount": 42,
"totalDuration": 324000,
"totalSize": 8500000000
},
"databaseHealth": {
"status": "healthy"
},
"uptime": 3600,
"features": ["streaming", "search", "artwork"]
}GET /api/settings
Returns folders, server/scanning config, stats, scan status, and databaseHealth.
Notes:
- Remote anonymous requests get
401 configPathis only exposed to privileged local reads
POST /api/database/rebuild
Local-only recovery endpoint.
{
"success": true,
"scanStarted": true,
"databaseHealth": {
"status": "healthy"
}
}Pairing and auth
GET /api/auth/status
Local-only arm status:
{
"armed": false,
"expiresAt": null
}POST /api/auth/arm
Request body:
{
"minutes": 30
}Response:
{
"armed": true,
"pairingCode": "abc123",
"expiresAt": 1700001800000
}POST /api/auth/pair
Request body:
{
"pairingCode": "abc123",
"deviceId": "mobile-123",
"deviceName": "music-hub-mobile"
}Response:
{
"accessToken": "...",
"accessExpiresAt": 1700000600000,
"refreshToken": "...",
"refreshExpiresAt": 1702592600000
}If pairing is not armed, the server returns 409 with code: "pairing_not_armed".
POST /api/auth/refresh
Request body:
{
"deviceId": "mobile-123",
"refreshToken": "..."
}Response shape matches /api/auth/pair.
Library reads
Notes
Desktop exposes notes for two common user workflows:
- notes attached to a track everywhere it appears
- notes attached to one track inside one playlist
Track notes
GET /api/tracks/:id/notes
POST /api/tracks/:id/notes
PUT /api/tracks/:id/notes/:noteId
DELETE /api/tracks/:id/notes/:noteIdCreate body:
{
"body": "Remember to use this in the opening set",
"positionSeconds": 64
}positionSeconds is optional.
Playlist track notes
GET /api/playlists/:id/tracks/:trackId/notes
POST /api/playlists/:id/tracks/:trackId/notes
PUT /api/playlists/:id/tracks/:trackId/notes/:noteId
DELETE /api/playlists/:id/tracks/:trackId/notes/:noteIdUse playlist track notes when the note only makes sense in that playlist.
Collaborative playlists
Collaborative playlists are hub-to-hub. A mobile client still talks to its paired personal hub; that hub talks to other hubs when needed.
Core concepts:
shareId: stable shared playlist idhubId: identity of a participating hubrole:memberoradmincapabilities: permissions such asplaylist.read,playlist.write_ops,media.stream,members.invite, andmembers.revoke
Important endpoints:
POST /api/collab/playlists
GET /api/collab/playlists
POST /api/collab/playlists/:playlistId/invites
POST /api/collab/invites/accept
GET /api/collab/playlists/:shareId/members
GET /api/collab/playlists/:shareId/snapshot
POST /api/collab/playlists/:shareId/ops
GET /api/collab/playlists/:shareId/items/:itemId/media
POST /api/collab/playlists/:shareId/items/:itemId/importMember management endpoints require admin rights on that collaborative playlist.
Main read endpoints:
GET /api/library/statsGET /api/library/filtersGET /api/tracks?limit=50&offset=0GET /api/track/:idGET /api/albums?limit=50GET /api/album/:idGET /api/album/:id/tracksGET /api/artists?limit=50GET /api/artist/:idGET /api/artist/:id/tracksGET /api/search(see below)GET /api/playlistsGET /api/playlists/:idGET /api/playlists/:id/tracksGET /api/savedGET /api/search/historyGET /api/sources/statusGET /api/browse
Remote callers must send Authorization: Bearer <token> unless they are using MUSIC_HUB_API_TOKEN.
GET /api/search
Unified library search with pagination. Returns a single entity type per request.
Query parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
q | string | (required) | Search string (empty returns no results) |
type | string | tracks | tracks | albums | artists |
limit | number | 50 | Page size (max 100) |
offset | number | 0 | SQL offset for the next page |
Tracks match on title, artist, album, effective genres, and tags. Albums match grouped rows on album title, album artist, or year. Artists match on artist name.
Response
{
"results": [],
"query": "example",
"type": "tracks",
"offset": 0,
"limit": 50,
"hasMore": false
}hasMore indicates whether another page exists; the server does not return a full total row count.
Playback and media
GET /api/playback/session
Returns the shared queue/session snapshot.
PUT /api/playback/session
Persists queue, queue index, status, current track, position, volume, and mute state.
POST /api/playback/history/back
Steps backward through persisted play history.
POST /api/media/session
Authenticated endpoint that returns a renewable media token for paired /stream, /hls, and /artwork access. The token is long-lived enough for extended mobile playback sessions; clients should still request a fresh token whenever they rebuild playback sources.
Media endpoints
GET /stream/:trackIdGET /hls/:trackId/index.m3u8GET /artwork/:idGET /artwork/track/:trackId
Remote/public playback relies on mediaToken.
Share bootstrap
Public share pages do not fetch protected library routes directly. They bootstrap through:
GET /api/share/playlist/:id/bootstrapGET /api/share/artist/:id/bootstrapGET /api/share/album/:id/bootstrapGET /api/share/track/:id/bootstrap
Optional query ?cap=<opaque>: when present, the server must find a matching share grant issued by an authenticated client (POST /api/share/grants). If cap is wrong or expired, bootstrap returns 403 with code: share_cap_invalid. When cap is omitted, bootstrap behaves as before (public share surface only; still subject to license and share session limits).
POST /api/share/grants
Requires normal hub API access (paired device bearer, local session, or MUSIC_HUB_API_TOKEN where applicable).
Body:
{
"entity": "album",
"id": "alb000…",
"ttlMinutes": 1440
}Use ttlMinutes: null for no expiry. Response includes url with ?cap= for sharing (absolute when a public remote base URL exists, otherwise a root-relative path clients may prefix with the hub origin). tunnelActive reports whether a stable public base was available.
Each bootstrap response includes:
- entity payload
- share-safe track payloads
hubNameshareSessionIdmediaTokenmediaExpiresAt
Bootstrap also accepts an optional resume header:
X-Music-Hub-Share-Session: <shareSessionId>When the same browser tab reloads the same shared page, the client can send that header to reuse the existing interactive session instead of consuming another slot. If the session is expired, missing, or belongs to a different shared resource, the server silently creates a fresh session instead.
To keep a public share session alive:
POST /api/share/session/heartbeatRequest:
{
"shareSessionId": "..."
}The server caps interactive public share concurrency per shared page; a fourth parallel interactive bootstrap returns 429. Preview bots/crawlers are tracked separately and do not consume that human-facing limit.
QR and connection helpers
GET /api/connect/qr
Local-only. Returns a QR/config that can include:
- local LAN endpoint
- Personal Private network / hub identity when available
- remote Cloudflare URL if enabled
pairingCodewhen remote pairing is armedshareLink(musichub://connect?...) for direct app handoff (same payload as QR JSON)shareLinkWeb(https://<landing>/mobile-pair?...) for chat-friendly sharing; landing mediates deep link; always uses the product landing origin, not the hub remote hostnameexpiresAtso desktop can show the armed pairing countdown
GET /api/tunnel/qr
Local-only. Requires Cloudflare tunnel up and pairing armed. Returns 409 pairing_not_armed if the public tunnel exists but remote pairing is not armed.
Real-time feeds
Server-sent events:
GET /api/scan/progressGET /api/library/events
Quick curl examples
Local health
curl http://localhost:38472/api/ping
curl http://localhost:38472/api/infoPair and use a remote bearer token
curl -X POST http://localhost:38472/api/auth/pair \
-H 'Content-Type: application/json' \
-d '{"pairingCode":"abc123","deviceId":"mobile-1","deviceName":"music-hub-mobile"}'curl http://localhost:38472/api/browse \
-H "Authorization: Bearer $ACCESS_TOKEN"curl -X POST http://localhost:38472/api/media/session \
-H "Authorization: Bearer $ACCESS_TOKEN"