Endpoint and authentication
Connect towss://api.rocksky.app/ws. Include a Rocksky access token in every client JSON message. You can obtain one through rocksky login or Rocksky’s access-token settings. Devices and commands are scoped to the authenticated account’s DID.
Both roles register a connection. Only connections that publish track state appear as players in device snapshots. Device IDs belong to a connection; capture a new ID after reconnecting.
The primary device supplies the profile’s now-playing status. Select it with set_primary. When no primary is available, the server can adopt a lone player. Other players remain controllable.
Publishing playback state does not replace submitting scrobbles. Playerd
submits its own scrobbles through the Rocksky API. If you build a player,
implement scrobbling separately when you want to record listens.
Connection lifecycle
- Open the WebSocket to
wss://api.rocksky.app/ws. - Register. The server replies with your
deviceId, then adevicessnapshot. - Heartbeat. Send the literal string
"ping"every ~10s; the server replies"pong". This keeps the socket alive — the server times out an idle socket after ~60s. - Reconnect. If the socket closes (network blip, background throttling,
idle timeout), reconnect and re-
register. The server re-sends thedevicessnapshot so you resync immediately.
Heartbeat note: some existing clients also send{"type":"heartbeat","token":"…"}; the server ignores it (only the raw"ping"yields"pong"). Prefer"ping".
Message envelope
All non-heartbeat frames are JSON objects with atype field. Client→server
frames carry token. There are four client→server types (register, command,
set_primary, message) and several server→client types (below).
Client → server messages
register
Announce this connection as a device.
clientName— the human label shown in the miniplayer’s device picker (e.g. “Rocksky CLI”, “Living Room”).
{ "status": "registered", "deviceId": "<uuid>" }, immediately
followed by a devices snapshot below.
Capture yourdeviceIdonly from this reply (the frame withstatus: "registered"). Do not readdeviceIdfrom other frames — thedevice_registeredbroadcast below carries another device’s id, and capturing it will make your pushes look like they came from that device.
message — push now-playing / status / queue (player devices)
Wrap a state payload. data.type is one of track, status, queue.
data (album art, URIs, like status…), then broadcasts it to
all of the user’s devices as a message below.
The server routes/tags broadcasts by the connection’s registereddeviceId, not thedevice_idin your payload — so a wrong value can’t misroute commands. Still, send your owndeviceIdfor clarity.
track — the current track. Push on track change, and re-push every few
seconds so controllers can reconcile elapsed time.
codec and sample_rate are passed through verbatim on the broadcast;
controller UIs (the web/mobile miniplayers) render them as audio-format badges.
shuffle, repeat and volume report the player’s own transport state so a
controller can render the toggles in the right position. Omit a field you have
no control for — that is how a controller tells “this player has no shuffle”
from “shuffle is off”, and it hides the control rather than showing it wrong.
Fields the server fills in on the broadcast (you don’t send them): album_art
(canonical, from the library), song_uri, album_uri, artist_uri, liked,
sha256, duration_ms.
status — transport state.
status: 0 = stopped, 1 = playing, 2 = paused (3 is also treated as
paused). Send 1/2 on play/pause, 0 on stop.
queue — the playback queue + current index. Push on queue change / track
advance.
album_art from the library where possible.
command — control a device (controllers)
target(optional) — send only to that device. Omit to broadcast to all the user’s devices.args(optional) — action-specific (see Commands).
{ "type": "command", "action": …, "args": … } to the
target(s). args is omitted when absent.
set_primary — choose the profile now-playing source (controllers)
primary_changed, and re-points the
profile now-playing at that device’s current track.
Server → client messages
devices — snapshot (sent to you right after you register)
primary_device is a device ID or JSON null when none is selected.
device_registered — a new device joined (to the user’s other devices)
Informational. Never capture this deviceId as your own (see Register).
device_unregistered — a device left
message — a device’s enriched state (broadcast to all the user’s devices)
primary_changed
set_primary, or auto-adopt). Controllers
should converge their “active device” on it.
command — a relayed control command (delivered to player devices)
Commands (what a player device must handle)
An
enqueue descriptor is a track the controller resolved for you:
uploadId (Rocksky uploads) or trackId (Navidrome/Subsonic id),
whichever is present.
After executing any command, push fresh track / status / queue state so all
clients update promptly.
audio_settings — DSP
One command carries the whole DSP surface as a partial document: every
section, and every field inside it, is optional.
equalizer.
Enumerated values: channels is stereo | mono | custom | monoLeft |
monoRight | karaoke | swap; crossfade.mode is off | enabled |
shuffle | albumChange | trackChange | auto; fadeOutMixMode is
crossfade | mix; replayGain.mode is off | track | album |
trackIfShuffling; crossfeed.mode is off | meier | custom. Treat an
unknown value as the off/default member rather than rejecting the document.
crossfade.mode: "auto" — Auto DJ
auto asks the player to derive each transition from the audio itself rather
than from fixed times: analyse the outgoing and incoming tracks, and place the
fade so it ends where the music ends instead of running through the silence
after it. fadeOutDuration (or fadeInDuration) carries the desired
music-over-music overlap in ms; the delay fields are ignored, since the player
computes them per transition.
This is deliberately a mode and not a new action: a player that has never
heard of Auto DJ reads an unknown enum value, falls back to off per the rule
above, and keeps playing. playerd implements it; other players may not.
Units are the same as the app.rocksky.rockbox.audio.settings record, so a
settings UI can put its saved document straight on the wire:
EQ
bands is positional — index 0 is the lowest band. Band centre frequencies
are the fixed rockbox band table keyed by index, so a player should trust the
index over a frequency that may have been persisted from an older table.
Implementation notes
After a disconnect, reconnect with backoff, register again, and replace your device list with the new snapshot. Send the raw textping every ten seconds and handle pong before attempting to parse a frame as JSON. Clear your heartbeat timer when the connection closes.
For players, publish fresh state after commands and periodically while playing. Ignore commands and audio-setting sections your engine does not support. Only advertise optional controls that work in your player.
For controllers, start from devices and update your view with message, device_unregistered, and primary_changed. A device_registered notification alone does not establish that the new connection is a player. Wait for track state. Always use an explicit target when you intend to control one device: omitting it broadcasts to all of your devices.
Queue indexes and startIndex are zero-based. An enqueue descriptor needs a resolvable uploadId or Navidrome/Subsonic trackId; a title or a catalog URI alone is not an audio source. Fetch the stream through the appropriate upload or library API.
Commands are relayed to players; use their subsequent state updates to reflect what actually happened in your UI.
Use the SDK
The Rocksky SDKs provideRemotePlayer and RemoteController wrappers. Use them when you do not need to implement the wire protocol yourself. The Rust SDK’s remote support uses the remote-player feature.
See the TypeScript SDK or the remote service source for reference implementations. The official headless player is playerd.