BOAR Server API

The app stores only a host name, then builds requests as https://{host}/{path}. Custom servers must be reachable over HTTPS at the root of the host. A path prefix such as https://example.com/boar/ is not supported by the app.

General Rules

GET/BOAR

Health check used before the app saves a custom server host.

Response

I am BOAR!

The body must equal I am BOAR! after trimming whitespace.

BOAR does not require a specific backend implementation. A compatible server may proxy YouTube Music, redirect to YouTube Music, or produce compatible JSON itself. What matters is the request and response shape seen by the app.

POST/v1/search

Searches artists or songs. The app sends a YouTube Music InnerTube-style JSON request.

Request

{
  "context": {
    "client": {
      "clientName": "WEB_REMIX",
      "clientVersion": "1.20251111.09.00"
    }
  },
  "query": "search text",
  "params": "..."
}

Response

Return a successful YouTube Music search-compatible JSON response. A 307 redirect that preserves the original POST body is also compatible if the final response is the YouTube Music JSON.

POST/v1/browse

Fetches artist pages, album pages, release shelves, and lyrics pages.

Request

{
  "context": {
    "client": {
      "clientName": "WEB_REMIX",
      "clientVersion": "1.20251111.09.00"
    }
  },
  "browseId": "MPREb_...",
  "params": "optional"
}

Response

Return a successful YouTube Music browse-compatible JSON response for the requested browseId. The app extracts artist metadata, album lists, album tracks, video IDs, thumbnails, and lyrics from this response.

GET/playlist/{playlist_id}

Returns lightweight metadata for a YouTube playlist without returning audio bytes.

Response

{
  "playlist_id": "PL...",
  "entries": [
    {
      "id": "youtubeVideoId",
      "title": "Song title",
      "channel": "Artist or channel",
      "thumbnail": "https://..."
    }
  ],
  "count": 1,
  "queued": false,
  "cached": false
}

playlist_id and entries are required. Each entry must include id. The app can tolerate missing title, channel, and thumbnail, but providing them gives better imported metadata.

POST/queue/{playlist_id}

Optional preload hint. The app calls this after opening an album when a playlist ID is known. Return 200 with any JSON body.

Example Response

{
  "playlist_id": "PL...",
  "queued": true
}

GET/{playlist_id}/{index}

Returns the playable audio file for a playlist entry. index is zero-based and matches the app's album track order.

Response

Audio file bytes with a successful HTTP status.

GET/video/{video_id}

Returns the playable audio file for a single YouTube video ID.

Response

Audio file bytes with a successful HTTP status.

GET/cache/has?video_id={video_id}

Tells the app whether server-side audio for a video ID can be reused in a listening room. If your server does not support reusable cached audio, return {"cached": false}.

Response

{"cached": true}

GET/cover/has?video_id={video_id}&cover_hash={sha1}

Tells the app whether a cover image is already available on the server. At least one query parameter should be accepted. If your server does not support reusable cached covers, return {"cached": false}.

Response

{"cached": true}

Listening Rooms

Listening rooms use server-authoritative playback state. The app joins a room, receives a member_id, uploads or references tracks, repeatedly syncs room state, and downloads room audio from URLs supplied by the server.

Required Behavior

GET/rooms

Lists public active rooms.

Response

{"rooms": [RoomSummary]}

POST/rooms/version/check

Checks whether the current app build may use listening rooms. To allow the app, return ok: true.

Request

{"build": 42}

Response

{
  "ok": true,
  "build": 42,
  "min_build": null
}

GET/rooms/version

Legacy fallback used only if /rooms/version/check is unavailable or unusable. Implementing /rooms/version/check is enough for current app builds.

Response

{"min_version": "1.0"}

POST/rooms

Creates a room.

Request

{
  "name": "Friday Night",
  "public": false
}

Response

{
  "code": "123456",
  "name": "Friday Night",
  "public": false
}

POST/rooms/{code}/join

Joins a room and returns the current snapshot plus the caller's member_id.

Request

{"member_name": "Listener"}

Response

RoomSnapshot with "member_id"

POST/rooms/{code}/leave

Leaves a room.

Request

{"member_id": "member-id"}

Response

{
  "left": true,
  "room_deleted": false
}

POST/rooms/{code}/heartbeat

Refreshes member presence without returning a full snapshot.

Request

{"member_id": "member-id"}

Response

{"ok": true}

GET/rooms/{code}/sync?member_id={member_id}&since={version}

Returns the authoritative room snapshot. member_id and since may be absent, but if member_id is present it should refresh member presence.

Response

RoomSnapshot

GET/rooms/{code}/tracks

Returns room track metadata and queue order.

Response

{
  "tracks": [RoomTrack],
  "queue": ["track-id"]
}

POST/rooms/{code}/upload

Uploads one playable audio file to a room. This endpoint uses multipart/form-data.

Multipart Fields

FieldRequiredValue
member_idYesRoom member ID.
metadataNoJSON text matching UploadTrackMetadata.
fileYesPlayable audio file. The app sends MIME type audio/m4a.
coverNoJPEG cover image.

Response

RoomSnapshot with "added_track_id" and "added"

POST/rooms/{code}/add_cached

Adds server-cached audio to a room without uploading audio bytes. If you return {"cached": false} from /cache/has, the app will normally use /upload instead.

Request

{
  "member_id": "member-id",
  "video_id": "youtubeVideoId",
  "metadata": UploadTrackMetadata
}

Response

RoomSnapshot with "added_track_id" and "added"

If the audio exists but the requested cover does not, return 412 so the app can fall back to /upload.

POST/rooms/{code}/state

Updates synchronized playback state.

Request Fields

FieldTypeRequired
member_idstringYes
client_versionintegerNo
commandnext or previousNo
track_idstringNo
queue_indexintegerNo
statusplaying or pausedNo
positionnumberNo
playback_ratenumberNo
lyrics_openbooleanNo
client_timenumberNo
server_time_offsetnumberNo

Response

RoomSnapshot

POST/rooms/{code}/force_track

Starts playback of an existing room track. If the track is not already in the queue, append it or otherwise make it the current queue item.

Request

{
  "member_id": "member-id",
  "track_id": "track-id",
  "position": 0,
  "playback_rate": 1.0,
  "status": "playing",
  "client_time": 1767311999.95,
  "server_time_offset": 0.12,
  "client_version": 5
}

Response

RoomSnapshot

POST/rooms/{code}/replace_current

Replaces the current queue item with an existing room track. BOAR uses this for private rooms. Public rooms may reject it with 403.

Request

{
  "member_id": "member-id",
  "track_id": "track-id",
  "position": 0,
  "playback_rate": 1.0,
  "status": "playing",
  "client_time": 1767311999.95,
  "server_time_offset": 0.12,
  "client_version": 5
}

Response

RoomSnapshot

POST/rooms/{code}/chat

Adds a chat message and returns recent chat messages.

Request

{
  "member_id": "member-id",
  "text": "hello"
}

Response

{"chat": [RoomMessage]}

GET/rooms/{code}/track/{track_id}?member_id={member_id}

Returns the playable room audio file for a track. The member must still belong to the room.

Response

Audio file bytes with a successful HTTP status.

GET/rooms/{code}/cover/{track_id}?member_id={member_id}

Returns a JPEG cover image for a track. If cover_url is present in RoomTrack, this URL should return bytes beginning with JPEG magic bytes.

Response

JPEG file bytes with a successful HTTP status, or 404 if no cover exists.

Shared Schemas

RoomSummary

{
  "code": "123456",
  "name": "Friday Night",
  "public": true,
  "members": 3,
  "tracks": 12,
  "current_track_title": "Track title",
  "current_track_artist": "Artist name"
}

code, name, public, members, and tracks are required. Current track fields may be null.

RoomSnapshot

{
  "room": RoomInfo,
  "state": RoomState,
  "queue": ["track-id"],
  "tracks": [RoomTrack],
  "members": [RoomMember],
  "chat": [RoomMessage],
  "server_time": 1767312000.456,
  "member_id": "member-id",
  "added_track_id": "track-id",
  "added": true
}

room, state, queue, tracks, and members are required. member_id is needed in the join response. added_track_id and added are needed in upload/add-cached responses. chat and server_time are recommended in every snapshot.

RoomInfo

{
  "code": "123456",
  "name": "Friday Night",
  "public": false
}

RoomState

{
  "version": 5,
  "status": "playing",
  "playback_rate": 1.0,
  "lyrics_open": false,
  "track_id": "track-id",
  "queue_index": 0,
  "position": 42.35,
  "updated_at": 1767312000.123,
  "rewind_floor_index": 0
}

All fields except rewind_floor_index are required. Use status: "playing" or "paused". position is seconds.

RoomTrack

{
  "id": "track-id",
  "title": "Song title",
  "artist": "Artist",
  "duration": 214.2,
  "owner_id": "member-id",
  "size": 3456789,
  "hash": "content-hash",
  "lyrics": LyricsInfo,
  "download_url": "/rooms/123456/track/track-id",
  "cover_url": "/rooms/123456/cover/track-id"
}

id, title, and download_url are required. artist, duration, owner_id, size, hash, lyrics, and cover_url may be null or omitted when unavailable.

RoomMember

{
  "id": "member-id",
  "name": "Listener"
}

RoomMessage

{
  "id": "message-id",
  "member_id": "member-id",
  "member_name": "Listener",
  "text": "hello",
  "timestamp": 1767312000.123
}

UploadTrackMetadata

{
  "title": "Song title",
  "artist": "Artist",
  "duration": 214.2,
  "video_id": "youtubeVideoId",
  "cover_hash": "sha1-of-cover-bytes",
  "lyrics": LyricsInfo
}

The app always sends title. Other fields may be null or absent.

LyricsInfo

{
  "status": "available",
  "text": "Lyrics text",
  "source": "embedded"
}

status must be available or notAvailable. For unavailable lyrics, use {"status":"notAvailable","text":null,"source":null} or omit the whole lyrics field.

Errors

StatusMeaning
200Successful JSON or file response.
307Allowed for search/browse redirects when the original POST body is preserved.
400Missing or invalid required field.
403Member is missing from a room or the action is not allowed.
404Room, track, playlist, video, cover, or cached file not found.
412Cached audio exists but the requested cached cover is missing.
429Rate limit exceeded.
500Unexpected server failure.

Most app flows only require a non-2xx status to treat a request as failed. For room sync errors, a JSON body such as {"description":"Room expired."} lets the app show a clearer message.