API Reference

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/ping
  • GET /api/info
  • POST /api/auth/pair
  • POST /api/auth/refresh
  • GET /api/share/{playlist|artist|album|track}/:id/bootstrap
  • POST /api/share/session/heartbeat

Local-only admin endpoints

These return 403 when called remotely:

  • GET /api/auth/status
  • POST /api/auth/arm
  • GET /api/connect/qr
  • GET /api/tunnel/qr
  • POST /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_only still 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
  • configPath is 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/:noteId

Create 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/:noteId

Use 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 id
  • hubId: identity of a participating hub
  • role: member or admin
  • capabilities: permissions such as playlist.read, playlist.write_ops, media.stream, members.invite, and members.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/import

Member management endpoints require admin rights on that collaborative playlist.

Main read endpoints:

  • GET /api/library/stats
  • GET /api/library/filters
  • GET /api/tracks?limit=50&offset=0
  • GET /api/track/:id
  • GET /api/albums?limit=50
  • GET /api/album/:id
  • GET /api/album/:id/tracks
  • GET /api/artists?limit=50
  • GET /api/artist/:id
  • GET /api/artist/:id/tracks
  • GET /api/search (see below)
  • GET /api/playlists
  • GET /api/playlists/:id
  • GET /api/playlists/:id/tracks
  • GET /api/saved
  • GET /api/search/history
  • GET /api/sources/status
  • GET /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

ParameterTypeDefaultDescription
qstring(required)Search string (empty returns no results)
typestringtrackstracks | albums | artists
limitnumber50Page size (max 100)
offsetnumber0SQL 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/:trackId
  • GET /hls/:trackId/index.m3u8
  • GET /artwork/:id
  • GET /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/bootstrap
  • GET /api/share/artist/:id/bootstrap
  • GET /api/share/album/:id/bootstrap
  • GET /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
  • hubName
  • shareSessionId
  • mediaToken
  • mediaExpiresAt

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/heartbeat

Request:

{
  "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
  • pairingCode when remote pairing is armed
  • shareLink (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 hostname
  • expiresAt so 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/progress
  • GET /api/library/events

Quick curl examples

Local health

curl http://localhost:38472/api/ping
curl http://localhost:38472/api/info

Pair 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"