Spotify Soloist WebSocket API
Enable the local WebSocket API with --ws ADDR:PORT:
_10soloist --device-name "Kitchen speaker" --api-key "$SOLOIST_API_KEY" --ws 127.0.0.1:9090
Connect to:
_10ws://ADDR:PORT
All messages are JSON text frames. Every client-to-server message must have
"type": "command". Every server-to-client event has a type field.
When the server accepts a connection, Spotify Soloist sends auth_state. If a
user is already logged in, it also sends playback_state.
The WebSocket server is opt-in. Without --ws, Spotify Soloist does not open a
local API port and soloist ctl observe/control commands cannot connect unless
you restart Spotify Soloist with --ws.
Endpoint discovery
When Spotify Soloist starts with --ws, it writes the active WebSocket address
and port to runtime files in the data directory:
| File | Contents |
|---|---|
ws.addr | Bind address, such as 127.0.0.1. |
ws.port | Actual listening port. |
Use port 0 to let the operating system choose a free port, then read these
files before connecting. Spotify Soloist removes the files on shutdown.
soloist ctl uses the same discovery mechanism.
Security
The Spotify Soloist WebSocket API is a local integration surface. It has no built-in client authentication, authorization, TLS, Origin validation, CSRF protection, browser-facing security controls, or network exposure policy.
Entity shape
Playback items, contexts, queue entries, parents, and creators use a common entity envelope:
_45{_45 "uri": "spotify:track:2JRo0gjbX4GrCqBYdRohoo",_45 "entity_type": "track",_45 "decorations": {_45 "identity": {_45 "name": "My Song"_45 },_45 "visual_identity": {_45 "cover": [_45 {_45 "url": "https://i.scdn.co/image/ab67616d00001e02...",_45 "size": "large"_45 }_45 ]_45 },_45 "parent": {_45 "entity": {_45 "uri": "spotify:album:4aawyAB9vmqN3uQ7FjRGTy",_45 "entity_type": "album",_45 "decorations": {_45 "identity": {_45 "name": "Album Name"_45 }_45 }_45 }_45 },_45 "creators": [_45 {_45 "entity": {_45 "uri": "spotify:artist:...",_45 "entity_type": "artist",_45 "decorations": {_45 "identity": {_45 "name": "Artist Name"_45 }_45 }_45 }_45 }_45 ],_45 "playback": {_45 "duration_ms": 210000,_45 "content_ratings": []_45 }_45 }_45}
| Field | Type | Description |
|---|---|---|
uri | string | Spotify URI, or an empty string when no top-level entity is available. Nested related entities may omit uri when unknown. |
entity_type | string | Known values include track, episode, artist, album, playlist, show, ad, and unknown. Empty when no entity is available. |
decorations.identity.name | string | Display name when known. |
decorations.visual_identity.cover[] | array | Optional cover images with url and size. Current sizes are small, default, large, and xlarge when source images are available. |
decorations.parent.entity | object | Optional parent entity, such as an album for a track or a show for an episode. |
decorations.creators[] | array | Optional creator entities. |
decorations.playback.duration_ms | number | Playable duration in milliseconds when known. |
decorations.playback.content_ratings[] | array | Content rating labels when known, such as explicit or south_korea_19. |
Event: auth_state
Sent on connect, authentication changes, and in response to get_auth_state.
_10{_10 "type": "auth_state",_10 "logged_in": true,_10 "is_active": true,_10 "device_name": "Kitchen speaker"_10}
| Field | Type | Description |
|---|---|---|
logged_in | boolean | Whether Spotify Soloist has an authenticated Spotify Connect session. |
is_active | boolean | Whether Spotify Soloist is the active Spotify Connect device. |
device_name | string | Spotify Connect device name. |
Event: playback_state
Full playback snapshot. Sent on connect when logged in, in response to
get_state, and when broad playback changes require a refreshed snapshot. Other
state changes arrive as granular events such as track_changed,
playback_changed, context_changed, options_changed, queue_changed, and
position_sync.
_49{_49 "type": "playback_state",_49 "status": "playing",_49 "item": {_49 "uri": "spotify:track:2JRo0gjbX4GrCqBYdRohoo",_49 "entity_type": "track",_49 "decorations": {_49 "identity": {_49 "name": "My Song"_49 },_49 "playback": {_49 "duration_ms": 210000,_49 "content_ratings": []_49 }_49 }_49 },_49 "context": {_49 "uri": "spotify:playlist:37i9dQZF1DXcBWIGoYBM5M",_49 "entity_type": "playlist",_49 "decorations": {_49 "identity": {_49 "name": "Today's Top Hits"_49 }_49 }_49 },_49 "position": {_49 "position_ms": 45000,_49 "timestamp_ms": 1747654321000,_49 "speed": 1.0_49 },_49 "volume": 65,_49 "is_active": true,_49 "options": {_49 "shuffle": false,_49 "repeat": "off",_49 "playback_speed": 1.0,_49 "modes": {}_49 },_49 "available_actions": {_49 "pause": {},_49 "seek": {},_49 "seek_forward": {_49 "step_ms": 15000_49 },_49 "seek_backward": {_49 "step_ms": 15000_49 }_49 }_49}
| Field | Type | Description |
|---|---|---|
status | string | idle, playing, paused, or buffering. |
item | object | Current playable entity. |
context | object | Current playback context entity. |
position.position_ms | number | Playback position at position.timestamp_ms. |
position.timestamp_ms | number | Server wall-clock time as Unix epoch milliseconds. |
position.speed | number | Position speed multiplier. 0.0 means position is not advancing. |
volume | number | Volume percentage from 0 to 100. |
is_active | boolean | Whether Spotify Soloist is the active Spotify Connect device. |
options.shuffle | boolean | Whether shuffle is enabled. |
options.repeat | string | off, context, or track. |
options.playback_speed | number | Requested playback speed. |
options.modes | object | Public playback mode values. Current values may include context_enhancement with NONE or RECOMMENDATION. |
available_actions | object | Actions currently available from this playback state. |
Current available_actions keys include pause, play, seek,
seek_forward, seek_backward, skip_next, skip_prev, add_to_queue,
shuffle, and set_repeat. Action values are empty objects unless parameters
are advertised, such as step_ms for relative seek actions.
available_actions uses player action names. The command table below maps those
actions to WebSocket command names; for example, shuffle maps to set_shuffle
and set_repeat maps to the repeat commands.
Playback update events
| Event | Payload |
|---|---|
track_changed | { "type": "track_changed", "item": Entity } |
playback_changed | { "type": "playback_changed", "status": "paused" } |
volume_changed | { "type": "volume_changed", "volume": 42 } |
device_changed | { "type": "device_changed", "is_active": true, "device_name": "Kitchen speaker" } |
context_changed | { "type": "context_changed", "context": Entity } |
options_changed | { "type": "options_changed", "options": PlaybackOptions } |
Event: position_sync
Sent whenever the playback position anchor changes. Use it to interpolate position locally between updates.
_10{_10 "type": "position_sync",_10 "position": {_10 "position_ms": 45000,_10 "timestamp_ms": 1747654321000,_10 "speed": 1.0_10 }_10}
| Field | Type | Description |
|---|---|---|
position.position_ms | number | Position at position.timestamp_ms. |
position.timestamp_ms | number | Server wall-clock time as Unix epoch milliseconds. |
position.speed | number | Position speed multiplier. 0.0 means paused or stopped. |
Event: queue_changed
Sent when the play queue changes and in response to get_queue.
_29{_29 "type": "queue_changed",_29 "previous": [_29 {_29 "uid": "spotify:track:previous",_29 "source": "context",_29 "item": {_29 "uri": "spotify:track:previous",_29 "entity_type": "track",_29 "decorations": {}_29 }_29 }_29 ],_29 "upcoming": [_29 {_29 "uid": "spotify:track:upcoming",_29 "source": "queue",_29 "item": {_29 "uri": "spotify:track:upcoming",_29 "entity_type": "track",_29 "decorations": {_29 "identity": {_29 "name": "Next Song"_29 }_29 }_29 }_29 }_29 ]_29}
| Field | Type | Description |
|---|---|---|
previous | array | Previous queue entries, ordered from most recently played to oldest. |
upcoming | array | Upcoming queue entries, ordered from next to play onward. |
previous[].uid, upcoming[].uid | string | Stable queue entry identifier when available, otherwise the item URI. |
previous[].source, upcoming[].source | string | context, queue, or autoplay. |
previous[].item, upcoming[].item | object | Queue item entity. |
Broadcast queue_changed events are capped at 10 entries. Use get_queue with
limit to request a different number. Omit limit or set it to 0 to request
all available entries.
Result and error events
Accepted control commands return:
_10{_10 "type": "command_result",_10 "command": "pause"_10}
Malformed messages, unknown commands, invalid command fields, and commands that require login return an error to the originating client:
_10{_10 "type": "error",_10 "message": "command requires authentication"_10}
The connection stays open after an error.
command_result means the command was accepted and dispatched. It does not
guarantee that playback state has already changed. Playback changes are
asynchronous, so clients should still listen for events such as
playback_changed, volume_changed, options_changed, queue_changed, and
position_sync.
WebSocket commands
All commands use:
_10{ "type": "command", "command": "get_state" }
Query commands
| Command | Authentication | Description |
|---|---|---|
get_auth_state | No | Request auth_state. |
get_state | Yes | Request playback_state. |
get_queue | Yes | Request queue_changed. Optional non-negative limit field. |
Example:
_10{ "type": "command", "command": "get_queue", "limit": 10 }
Query commands return their corresponding event payload and do not send
command_result.
Control commands
| Command | Fields | Description |
|---|---|---|
play | Optional uri | Resume playback or start playing a playable Spotify URI. |
pause | None | Pause playback. |
skip_next | None | Skip to next track. |
skip_prev | None | Skip to previous track or restart current track. |
seek | position_ms | Seek to position in milliseconds. |
set_volume | volume | Set volume from 0 to 100. |
set_shuffle | enabled | Enable or disable shuffle. |
set_repeat_context | enabled | Enable or disable context repeat. |
set_repeat_track | enabled | Enable or disable track repeat. |
add_to_queue | uri | Add a Spotify track URI to the queue. |
activate | None | Make Spotify Soloist the active Spotify Connect device. |
deactivate | None | Give up active-device status. |
Examples:
_10{ "type": "command", "command": "play" }_10{ "type": "command", "command": "play", "uri": "spotify:playlist:37i9dQZF1DXcBWIGoYBM5M" }_10{ "type": "command", "command": "seek", "position_ms": 30000 }_10{ "type": "command", "command": "set_volume", "volume": 50 }_10{ "type": "command", "command": "set_shuffle", "enabled": true }_10{ "type": "command", "command": "add_to_queue", "uri": "spotify:track:6rqhFgbbKwnb9MLmUQDhG6" }
All control commands require a logged-in Spotify Connect session.
play.uri accepts a playable Spotify URI such as a track, album, playlist, or
episode URI. add_to_queue.uri accepts only a Spotify track URI.
To set repeat mode from a raw WebSocket client, coordinate the two repeat commands:
| Repeat mode | Commands |
|---|---|
off | Send set_repeat_track with enabled: false, then set_repeat_context with enabled: false. |
context | Send set_repeat_track with enabled: false, then set_repeat_context with enabled: true. |
track | Send set_repeat_context with enabled: false, then set_repeat_track with enabled: true. |
Error handling
The server responds with error for malformed JSON, unknown message types,
unknown commands, missing or invalid command fields, unauthenticated access to
authenticated commands, and command dispatch errors reported synchronously by
the API.
Errors are sent only to the client that sent the invalid message. The connection stays open, so clients can recover by sending a valid command.
Lifecycle
- Start Spotify Soloist with
--ws ADDR:PORT. - Spotify Soloist starts the WebSocket server and writes
ws.addrandws.portin the data directory. - Clients connect and receive
auth_state. - After Spotify Connect login, connected clients receive
auth_statewithlogged_in: true; authenticated commands become available. - On logout or session replacement, connected clients receive a new
auth_state. - On shutdown, Spotify Soloist removes
ws.addrandws.port.