Summary

The BluOS Custom Integration API (v1.7) is an HTTP-based control protocol for BluOS players, including products from Bluesound, NAD Electronics, and DALI Loudspeakers. All commands are HTTP GET requests (except player reboot, which is POST) sent to http://<player_ip>:<port>/<request>; responses are UTF-8 encoded XML. This spec covers volume control, playback control, play queue management, presets, content browsing, player grouping, and input selection.

Transport

protocols:
  - http
addressing:
  port: 11000  # stated in source; CI580 node 1 uses 11000, nodes 2-4 use 11010/11020/11030
  base_url: "http://<player_ip>:11000"
auth:
  type: none  # inferred: no auth procedure in source

Traits

- levelable       # volume set/get commands present
- queryable       # /Status, /SyncStatus, /Volume, /Playlist, /Presets, /Browse all return state
- powerable       # /reboot (soft reboot) command present; no power on/off toggle found in source
- routable        # input selection commands present (/Play?url=, /Play?inputIndex=, /Play?inputTypeIndex=)

Actions

# --- Volume Control ---

- id: set_volume_level
  label: Set Volume Level
  kind: action
  params:
    - name: level
      type: integer
      range: "0..100"
      description: Absolute volume level (0–100). -1 indicates fixed volume (read-only).
    - name: tell_slaves
      type: integer
      range: "0|1"
      description: "Optional. If 1, applies volume change to all players in the group. If 0 (default), applies only to the addressed player."
  request: "GET /Volume?level={level}&tell_slaves={tell_slaves}"
  response_xml: "<volume db=\"...\" mute=\"0|1\" offsetDb=\"...\" etag=\"...\">{level}</volume>"

- id: set_volume_absolute_db
  label: Set Volume (Absolute dB)
  kind: action
  params:
    - name: abs_db
      type: number
      description: Absolute volume level in dB (typically -80..0 range, configured via BluOS Controller app).
    - name: tell_slaves
      type: integer
      range: "0|1"
      description: "Optional. If 1, applies to all grouped players."
  request: "GET /Volume?abs_db={abs_db}&tell_slaves={tell_slaves}"
  response_xml: "<volume db=\"...\" mute=\"0|1\" offsetDb=\"...\" etag=\"...\">{level}</volume>"

- id: set_volume_relative_db
  label: Set Volume (Relative dB)
  kind: action
  params:
    - name: db
      type: number
      description: Relative volume change in dB. Positive increases volume, negative decreases.
    - name: tell_slaves
      type: integer
      range: "0|1"
      description: "Optional. If 1, applies to all grouped players."
  request: "GET /Volume?db={db}&tell_slaves={tell_slaves}"
  response_xml: "<volume db=\"...\" mute=\"0|1\" offsetDb=\"...\" etag=\"...\">{level}</volume>"

- id: volume_up
  label: Volume Up
  kind: action
  params:
    - name: db
      type: number
      description: Volume increase in dB. Typical value is 2.
  request: "GET /Volume?db={db}"
  notes: "Typical call: /Volume?db=2"

- id: volume_down
  label: Volume Down
  kind: action
  params:
    - name: db
      type: number
      description: Volume decrease in dB expressed as a negative number. Typical value is -2.
  request: "GET /Volume?db={db}"
  notes: "Typical call: /Volume?db=-2"

- id: mute_on
  label: Mute
  kind: action
  params: []
  request: "GET /Volume?mute=1"
  response_xml: "<volume muteDb=\"...\" db=\"-100\" muteVolume=\"...\" mute=\"1\" offsetDb=\"...\" etag=\"...\">0</volume>"

- id: mute_off
  label: Unmute
  kind: action
  params: []
  request: "GET /Volume?mute=0"
  response_xml: "<volume db=\"...\" mute=\"0\" offsetDb=\"...\" etag=\"...\">{level}</volume>"

# --- Playback Control ---

- id: play
  label: Play
  kind: action
  params:
    - name: seek
      type: integer
      description: "Optional. Jump to position in seconds. Only valid when /Status includes <totlen>."
    - name: id
      type: integer
      description: "Optional. Track id in the queue. Used with seek."
    - name: url
      type: string
      description: "Optional. URL-encoded stream URL to play directly."
  request: "GET /Play[?seek={seconds}][&id={trackid}][?url={encodedStreamURL}]"
  response_xml: "<state>play</state>  <!-- or <state>stream</state> -->"

- id: pause
  label: Pause
  kind: action
  params:
    - name: toggle
      type: integer
      range: "0|1"
      description: "Optional. If 1, toggles current pause state."
  request: "GET /Pause[?toggle=1]"
  response_xml: "<state>pause</state>"

- id: stop
  label: Stop
  kind: action
  params: []
  request: "GET /Stop"
  response_xml: "<state>stop</state>"

- id: skip
  label: Skip to Next Track
  kind: action
  params: []
  request: "GET /Skip"
  response_xml: "<id>{track_id}</id>"
  notes: "Only works when playing from the play queue (no <streamUrl> in /Status response). For streaming radio, use /Action."

- id: back
  label: Back / Previous Track
  kind: action
  params: []
  request: "GET /Back"
  response_xml: "<id>{track_id}</id>"
  notes: "If track has played >4 seconds, returns to start of current track. Otherwise goes to previous track. Only works when playing from the play queue."

- id: shuffle
  label: Set Shuffle
  kind: action
  params:
    - name: state
      type: integer
      range: "0|1"
      description: "0 to disable shuffle, 1 to enable shuffle."
  request: "GET /Shuffle?state={state}"
  response_xml: "<playlist name=\"...\" modified=\"0|1\" length=\"...\" shuffle=\"0|1\" id=\"...\"/>"

- id: repeat
  label: Set Repeat Mode
  kind: action
  params:
    - name: state
      type: integer
      range: "0|1|2"
      description: "0 = repeat entire queue, 1 = repeat current track, 2 = repeat off."
  request: "GET /Repeat?state={state}"
  response_xml: "<playlist length=\"...\" id=\"...\" repeat=\"0|1|2\"/>"

- id: streaming_radio_action
  label: Streaming Radio Action (Skip/Back/Love/Ban)
  kind: action
  params:
    - name: action_url
      type: string
      description: "The full action URL taken from the <action> element in /Status response (e.g., /Action?service=Slacker&skip=...)."
  request: "GET {action_url}"
  notes: "Use when <streamUrl> is present in /Status. Action URLs are provided by the player in the <actions> block of /Status response."

# --- Play Queue Management ---

- id: list_playlist
  label: List Play Queue
  kind: action
  params:
    - name: length
      type: integer
      description: "Optional. Set to 1 to return only queue status without track details."
    - name: start
      type: integer
      description: "Optional. First track index (0-based) for pagination."
    - name: end
      type: integer
      description: "Optional. Last track index for pagination."
  request: "GET /Playlist[?length=1][?start={first}&end={last}]"

- id: delete_track
  label: Delete Track from Queue
  kind: action
  params:
    - name: id
      type: integer
      description: Track id (position) in the play queue to delete.
  request: "GET /Delete?id={id}"
  response_xml: "<deleted>{position}</deleted>"

- id: move_track
  label: Move Track in Queue
  kind: action
  params:
    - name: old
      type: integer
      description: Current position of the track.
    - name: new
      type: integer
      description: New position for the track.
  request: "GET /Move?new={new}&old={old}"
  response_xml: "<moved>moved</moved>"

- id: clear_queue
  label: Clear Play Queue
  kind: action
  params: []
  request: "GET /Clear"
  response_xml: "<playlist modified=\"0\" length=\"0\" id=\"...\"/>"

- id: save_queue
  label: Save Play Queue as Playlist
  kind: action
  params:
    - name: name
      type: string
      description: Name for the saved playlist.
  request: "GET /Save?name={playlist_name}"
  response_xml: "<saved><entries>{count}</entries></saved>"

# --- Presets ---

- id: list_presets
  label: List Presets
  kind: action
  params: []
  request: "GET /Presets"
  response_xml: "<presets prid=\"...\"><preset id=\"...\" name=\"...\" url=\"...\" image=\"...\"/>...</presets>"

- id: load_preset
  label: Load Preset
  kind: action
  params:
    - name: id
      type: string
      description: "Preset id number, '+1' for next preset, or '-1' for previous preset."
  request: "GET /Preset?id={id}"
  response_xml: "<loaded service=\"...\"><entries>{count}</entries></loaded>  <!-- or <state>stream</state> for radio -->"

# --- Content Browsing ---

- id: browse
  label: Browse Music Content
  kind: action
  params:
    - name: key
      type: string
      description: "Optional. URL-encoded browse key from a previous response. Absent for top-level browse."
    - name: withContextMenuItems
      type: integer
      description: "Optional. Set to 1 to include inline context menus in the response."
    - name: q
      type: string
      description: "Optional. Search string. Use with a searchKey to search within a service."
  request: "GET /Browse[?key={key}][&withContextMenuItems=1][&q={searchText}]"
  response_xml: "<browse ...><item .../> ...</browse>"

# --- Player Grouping ---

- id: add_slave
  label: Add Secondary Player(s) to Group
  kind: action
  params:
    - name: slave
      type: string
      description: "IP address of a single secondary player."
    - name: slaves
      type: string
      description: "Comma-separated IP addresses for adding multiple secondary players."
    - name: port
      type: integer
      description: "Port of the secondary player (default 11000)."
    - name: ports
      type: string
      description: "Comma-separated ports for multiple players."
    - name: group
      type: string
      description: "Optional. Group name. BluOS assigns default name if omitted."
  request: "GET /AddSlave?slave={ip}&port={port}[&group={name}]  <!-- or slaves={ips}&ports={ports} -->"
  response_xml: "<addSlave><slave port=\"...\" id=\"...\"/>...</addSlave>"

- id: remove_slave
  label: Remove Secondary Player(s) from Group
  kind: action
  params:
    - name: slave
      type: string
      description: "IP address of the secondary player to remove."
    - name: slaves
      type: string
      description: "Comma-separated IP addresses of multiple secondary players to remove."
    - name: port
      type: integer
      description: "Port of the secondary player."
    - name: ports
      type: string
      description: "Comma-separated ports."
  request: "GET /RemoveSlave?slave={ip}&port={port}  <!-- or slaves={ips}&ports={ports} -->"
  response_xml: "<SyncStatus ...>...</SyncStatus>"

# --- Input Selection ---

- id: select_active_input
  label: Select Active Input
  kind: action
  params:
    - name: url
      type: string
      description: "URL-encoded input URL from /RadioBrowse?service=Capture response."
  request: "GET /Play?url={encodedInputURL}"
  response_xml: "<state>stream</state>"
  notes: "First call /RadioBrowse?service=Capture to get available input URLs."

- id: select_external_input_by_index
  label: Select External Input by Index (firmware <4.2.0)
  kind: action
  params:
    - name: inputIndex
      type: integer
      description: "1-based index of inputs from /Settings?id=capture&schemaVersion=32 (excluding Bluetooth)."
  request: "GET /Play?inputIndex={index}"
  response_xml: "<state>stream</state>"
  notes: "Only for firmware newer than v3.8.0 and older than v4.2.0."

- id: select_external_input_by_type_index
  label: Select External Input by Type-Index (firmware v4.2.0+)
  kind: action
  params:
    - name: inputTypeIndex
      type: string
      description: "Type-index string in the format '{type}-{index}', e.g. 'spdif-1', 'analog-1', 'bluetooth-1'. Types: spdif, analog, coax, bluetooth, arc, earc, phono, computer, aesebu, balanced, microphone."
  request: "GET /Play?inputTypeIndex={type}-{index}"
  response_xml: "<state>stream</state>"
  notes: "Only for firmware v4.2.0 or newer."

# --- Reboot ---

- id: reboot_player
  label: Reboot Player (Soft Reboot)
  kind: action
  params:
    - name: yes
      type: string
      description: "Any value (e.g. '1'). Required."
  request: "POST /reboot  (body: yes=1)"
  notes: "Uses HTTP POST, not GET. Example: curl -d yes=1 192.168.1.100/reboot"

# --- Doorbell ---

- id: play_doorbell
  label: Play Doorbell Chime
  kind: action
  params: []
  request: "GET /Doorbell?play=1"
  response_xml: "<status enable=\"1\" volume=\"...\" chime=\"...\"/>"

# --- Bluetooth Mode ---

- id: set_bluetooth_mode
  label: Set Bluetooth Mode
  kind: action
  params:
    - name: bluetoothAutoplay
      type: integer
      range: "0|1|2|3"
      description: "0 = Manual, 1 = Automatic, 2 = Guest, 3 = Disabled."
  request: "GET /audiomodes?bluetoothAutoplay={value}"
  notes: "No response body returned."

Feedbacks

- id: volume_state
  label: Volume State
  type: object
  query_request: "GET /Volume"
  response_xml: "<volume db=\"{db}\" mute=\"{0|1}\" muteDb=\"{db}\" muteVolume=\"{0..100}\" offsetDb=\"{offset}\" etag=\"{etag}\">{level 0..100 or -1}</volume>"
  fields:
    - name: volume
      type: integer
      description: "Current volume level 0..100, or -1 for fixed volume."
    - name: db
      type: number
      description: "Volume level in dB."
    - name: mute
      type: integer
      values: [0, 1]
      description: "1 if muted, 0 if not muted."
    - name: muteDb
      type: number
      description: "Unmuted volume in dB, present only when muted."
    - name: muteVolume
      type: integer
      description: "Unmuted volume 0..100, present only when muted."
    - name: etag
      type: string
      description: "Opaque value for long-polling."

- id: playback_status
  label: Playback Status
  type: object
  query_request: "GET /Status[?timeout={seconds}&etag={etag}]"
  polling_notes: "Supports long polling via timeout and etag parameters. Regular polling must not exceed once per 30 seconds. Long poll interval should not exceed 60 seconds and never faster than 10 seconds."
  key_fields:
    - name: state
      type: enum
      values: [play, pause, stop, stream, connecting]
      description: "Current player state."
    - name: volume
      type: integer
      description: "Volume 0..100 or -1 for fixed volume."
    - name: mute
      type: integer
      values: [0, 1]
      description: "1 if muted."
    - name: shuffle
      type: integer
      values: [0, 1]
    - name: repeat
      type: integer
      values: [0, 1, 2]
      description: "0=repeat queue, 1=repeat track, 2=repeat off."
    - name: title1
      type: string
      description: "First line of now-playing metadata. MUST be used for UI display."
    - name: title2
      type: string
      description: "Second line of now-playing metadata."
    - name: title3
      type: string
      description: "Third line of now-playing metadata."
    - name: syncStat
      type: string
      description: "Changes when /SyncStatus changes. Use to know when to refresh /SyncStatus."
    - name: pid
      type: integer
      description: "Unique play queue id."
    - name: prid
      type: integer
      description: "Unique preset id."
    - name: secs
      type: integer
      description: "Seconds played of current track. NOT included in etag calculation — client must increment locally."

- id: sync_status
  label: Player and Group Sync Status
  type: object
  query_request: "GET /SyncStatus[?timeout={seconds}&etag={etag}]"
  polling_notes: "Supports long polling. Recommended interval 180 seconds. Required to track volume of secondary players in a group."
  key_fields:
    - name: volume
      type: integer
      description: "Volume 0..100 or -1 for fixed."
    - name: mute
      type: integer
      values: [0, 1]
    - name: name
      type: string
      description: "Player name."
    - name: model
      type: string
    - name: modelName
      type: string
    - name: brand
      type: string
    - name: group
      type: string
      description: "Group name if player is primary."
    - name: master
      type: string
      description: "Primary player IP. Only present if this player is a secondary."
    - name: slave
      type: string
      description: "Secondary player IP(s). Only present if this player is primary."
    - name: initialized
      type: boolean
      description: "True if player is set up; false means setup via BluOS Controller app is required."

Variables

# UNRESOLVED: No settable parameters outside of discrete actions documented in source.
# Volume range (typically -80..0 dB) is adjustable via the BluOS Controller app UI, not via API.

Events

# Unsolicited events are not described in the source as push notifications.
# The API uses long-polling (not server-push) for state change detection.
# Long-poll endpoints: /Status and /SyncStatus (with timeout + etag parameters).

Macros

# UNRESOLVED: No multi-step macros are described explicitly in the source.

Safety

confirmation_required_for:
  - reboot_player  # soft reboot; source implies irreversible action during the operation
interlocks: []
# UNRESOLVED: No explicit interlock or power-sequencing requirements stated in source.

Notes

Port discovery: Port 11000 is used for all standard BluOS players. The CI580 uses ports 11000/11010/11020/11030 for its four streaming nodes. The authoritative port should be discovered via mDNS services _musc._tcp / _musp._tcp, or via the Lenbrook Service Discovery Protocol (LSDP) on UDP port 11430.

Long polling: Both /Status and /SyncStatus support long polling via timeout (seconds) and etag (from previous response) parameters. Long polling significantly reduces polling frequency. The client must not make two consecutive requests for the same resource less than 1 second apart. Only one of /Status or /SyncStatus needs long-poll active at a time; /Status includes a syncStat element to signal when /SyncStatus has changed.

Grouped players: When players are grouped, /Status on a secondary player mirrors the primary player. Use /SyncStatus long-polling to track the volume of individual players in a group. Volume changes with tell_slaves=1 propagate to all players in the group.

Input selection: Two separate firmware-gated methods exist for external input selection: inputIndex (firmware >3.8.0, <4.2.0) and inputTypeIndex (firmware ≥4.2.0). Active inputs (visible in /RadioBrowse?service=Capture) can use the /Play?url= method regardless of firmware version.

secs field: The secs field in /Status is not factored into the etag calculation. A long-poll will NOT return early just because playback seconds have advanced. Clients must increment playback position locally while state is play or stream.

Now-playing display: Always use title1, title2, title3 for three-line UI display. If twoline_title1 / twoline_title2 are present, use them for two-line display. Do not use album, artist, name fields directly in UI.

Streaming radio vs. play queue: If <streamUrl> is present in /Status, the player is streaming a radio station. /Skip, /Back, and shuffle/repeat controls are not relevant. Use the /Action command with URLs from the <actions> block in /Status for skip/back/love/ban on streaming radio.

LSDP discovery: Lenbrook Service Discovery Protocol (LSDP) uses UDP broadcast on port 11430. It is an alternative to mDNS for networks where multicast traffic is unreliable. Packets use binary format with "LSDP" magic word and big-endian encoding.

Provenance

source_domains:
  - bluos.io
source_urls:
  - https://bluos.io/wp-content/uploads/2025/06/BluOS-Custom-Integration-API_v1.7.pdf
retrieved_at: 2026-05-16T21:35:26.990Z
last_checked_at: 2026-06-02T21:56:23.037Z

Verification Summary

verdict: verified
checked_at: 2026-06-02T21:56:23.037Z
matched_actions: 31
action_count: 31
confidence: medium
summary: "All 31 spec actions traced to source. (7 unresolved item(s) noted in Known Gaps.)"

Known Gaps

- "Model-specific command support (which models have HDMI ARC, eARC, phono, etc.) is not stated in source. The CI580 multi-zone device uses non-standard port layout (11000/11010/11020/11030); this spec treats single-zone port 11000 as the default."
- "No settable parameters outside of discrete actions documented in source."
- "No multi-step macros are described explicitly in the source."
- "No explicit interlock or power-sequencing requirements stated in source."
- "Specific firmware version ranges for each model (M33 vs. Bluesound PULSE vs. NAD etc.) are not enumerated in the source. The spec assumes general BluOS API v1.7 compatibility."
- "Error response format beyond the <error><message/><detail/></error> structure mentioned for /Browse is not documented in source for other endpoints."
- "Authentication or access control mechanisms are not described; the API appears to be unauthenticated on the local network."

From the AI4AV catalog (https://ai4av.net) · ODbL-1.0