The Naim Mu-so 2 HTTP Bible

For my personal convenience, I am in process of writing the One App to Rule them All: a single macOS menu bar / iOS app that carries all primary control functions of my most used speakers in one place: KEF LS60, Meridian Ellipse, Naim Mu-so 2. So far, I have finished KEF and Meridian. The usual stuff is present, with a few surprises along the way (EQ profile selection and radio on the LS60, a hidden equalizer on Meridian). As all devices are on StreamUnlimited’s platform (with a few local dialects just for fun), wrapping them into one is actually the easy part. :slight_smile:

Small glimpse at the KEF app to see what I am yapping about:

FLTR: main screen (source & EQ selection), volume presets, shortcut/webhook presets, radio presets (read from the spreaker).

This weekend, I am eyeing the Mu-so – the party starts with a scan of the device. As threads covering the API have come up every now and then, I’ve asked my little brother Claude to summarize the findings as to benefit anyone who likes to tinker with their NAIM.

Anyway – enjoy. :slight_smile: I’ll update how the NAIM app turned out, if there’s any interest…

The Naim Mu-so 2 HTTP Bible

For the hobbyist who wants to drive their speaker from curl, Apple
Shortcuts, Home Assistant, a Stream Deck, or anything else that speaks
HTTP. Everything runs on the local network — no authentication, no
cloud.

Probed and verified on a Mu-so 2, firmware 4.5.0.6644, 2026-08-08 —
including a live session against internet radio and a Spotify
Connect stream.

:white_check_mark: = verified live on the device,


The device

IP 192.168.1.154 (example — see discovery)
Hostname muso-1503031748.local
Discovery Bonjour _sues800device._tcp (instance naims810x-…; TXT record carries ip=, name=, serial=)
API 1 Naim REST on port 15081 — power, inputs, favourites, now-playing, volume, all transport
API 2 StreamUnlimited (SUES) on port 80 — volume, mute, rich metadata, tone

The one rule that keeps you out of trouble :white_check_mark:

Use port 15081 for all transport. The SUES transport verbs are
booby-trapped on this device: against a live Spotify Connect stream,
SUES pause and seekTime work — but SUES play and next_ KILL
the Spotify session outright
(state drops to stopped, music gone).
The antidote, also verified: GET /inputs/spotify?cmd=select revives
the dropped session without touching the phone. The 15081 verbs
(playpause, next, prev, stop) all behave impeccably.

Two more behavioural facts :white_check_mark::

  • Radio is invisible to SUES. While internet radio plays via
    15081, SUES player:player/data reports stopped. Spotify Connect
    is mirrored by SUES (full metadata). Read radio state only from
    15081.
  • Standby: in network standby (“lona”) port 80 keeps answering;
    on 15081 /power always answers, but /nowplaying and /levels
    only respond while awake.

API 1 — Naim REST (port 15081)

Plain GETs and PUTs; responses are JSON. Objects carry a ussi
(path), a class, and sometimes children.

Power :white_check_mark:

# Status: state is "on" (awake) or "lona" (network standby)
curl "http://192.168.1.154:15081/power"

# Wake
curl -X PUT "http://192.168.1.154:15081/power?system=on"

# Back to standby
curl -X PUT "http://192.168.1.154:15081/power?system=lona"

Volume & mute :white_check_mark:

# Read: {"volume": "28", "mute": "0", ...}
curl "http://192.168.1.154:15081/levels/room"

# Set (absolute, 0-100)
curl -X PUT "http://192.168.1.154:15081/levels/room?volume=25"

# Mute on / off
curl -X PUT "http://192.168.1.154:15081/levels/room?mute=1"
curl -X PUT "http://192.168.1.154:15081/levels/room?mute=0"

Volume up/down: there is no native step verb :white_check_mark: — the obvious
candidates (?cmd=volup etc.) are accepted but do nothing, and a
relative value is dangerously misparsed: volume=+2 sets the
volume to literal 2 :white_check_mark: (imagine the reverse). Step by reading and
writing back:

# Volume up by 2
M=192.168.1.154
V=$(curl -s "http://$M:15081/levels/room" | python3 -c "import json,sys;print(min(100,int(json.load(sys.stdin)['volume'])+2))")
curl -s -X PUT "http://$M:15081/levels/room?volume=$V"

(Shortcuts version: Get contents of URL → Get dictionary value
volume → Calculate +2 → Get contents of …?volume=[result].)

Transport :white_check_mark: — the safe and only full set

# Toggle pause/resume (verified on Spotify: transportState 2⇄3)
curl "http://192.168.1.154:15081/nowplaying?cmd=playpause"

# Next / previous track (verified on Spotify)
curl "http://192.168.1.154:15081/nowplaying?cmd=next"
curl "http://192.168.1.154:15081/nowplaying?cmd=prev"

# Stop (verified on radio)
curl "http://192.168.1.154:15081/nowplaying?cmd=stop"

Which verbs the current source honours is advertised in the
restrict* flags of /nowplaying (0 = allowed, 128 =
restricted): Spotify allows everything; live radio allows only
stop/resume.

Now playing :white_check_mark:

curl "http://192.168.1.154:15081/nowplaying"

Awake only. Key fields, seen live:

Field Meaning
source e.g. inputs/spotify, inputs/radio
transportState 2 = playing, 3 = paused :white_check_mark:
transportPosition live play position in ms, ticking :white_check_mark:
artistName, albumName Spotify metadata :white_check_mark:
station, genre, country, bitRate radio metadata :white_check_mark:
artwork real URL (Spotify: i.scdn.co; radio: station logo) :white_check_mark:
restrict* transport permissions per source :white_check_mark:

Inputs

# All 16 inputs with their status
curl "http://192.168.1.154:15081/inputs"

# Select a source ✅
curl "http://192.168.1.154:15081/inputs/dig?cmd=select"

# Revive a dropped Spotify Connect session ✅
curl "http://192.168.1.154:15081/inputs/spotify?cmd=select"

Selectable (selectable: 1): inputs/ana (3.5 mm), inputs/dig
(optical), inputs/hdmi (ARC), inputs/radio, inputs/spotify,
inputs/playqueue. Passive (activate on arrival): airplay, gcast,
tidal, qobuz, bluetooth, upnp, usb, multiroom, files,
qq.

Internet radio & favourites :white_check_mark:

# Your favourites list (British spelling!)
curl "http://192.168.1.154:15081/favourites"

# Play one — starts the station immediately ✅
curl "http://192.168.1.154:15081/favourites/<32-char-id>?cmd=play"

# Or activate the radio input (resumes the last station)
curl "http://192.168.1.154:15081/inputs/radio?cmd=select"

/favourites returns children, each with a name and a ussi of
the form favourites/<32-char-id>. These double as the device’s real
preset system — the ten SUES presets: slots are all “Empty” :white_check_mark:.

Read-only info (always available)

Endpoint What
/system Model, firmware (build), serial, Chromecast details
/network Interface, SSID, IP configuration
/update Firmware update status
/alarms Alarm clocks (empty here)

API 2 — StreamUnlimited (port 80)

The KEF/Ellipse dialect: three verbs under /api, with typed JSON
values.

Verb Shape
Read GET /api/getData?path=<path>&roles=value
List GET /api/getRows?path=<path>&roles=@all&from=0&to=99
Write GET /api/setData?path=<path>&roles=value&value=<json>

The value is always a typed object, URL-encoded:
{"type":"i32_","i32_":28} (integer), {"type":"bool_","bool_":true},
{"type":"double_","double_":-2.5}, {"type":"string_","string_":"…"}.

Volume :white_check_mark: and mute :white_check_mark:

curl "http://192.168.1.154/api/getData?path=player:volume&roles=value"
curl "http://192.168.1.154/api/setData?path=player:volume&roles=value&value=%7B%22type%22%3A%22i32_%22%2C%22i32_%22%3A25%7D"

curl "http://192.168.1.154/api/getData?path=settings:/mediaPlayer/mute&roles=value"
curl "http://192.168.1.154/api/setData?path=settings:/mediaPlayer/mute&roles=value&value=%7B%22type%22%3A%22bool_%22%2C%22bool_%22%3Atrue%7D"

Player data :white_check_mark: (the rich read — Spotify/streams only)

curl "http://192.168.1.154/api/getData?path=player:player/data&roles=value"

Verified against a live Spotify Connect stream: state: playing,
trackRoles.title, mediaData.metaData (artist/album),
trackRoles.icon (artwork URL), status.duration, and — the gem —
resources[0].codec: "Free Lossless Audio Codec (FLAC 24bit)" with
bitRate ≈ 1.2 Mbps: your Spotify Lossless badge, for free :white_check_mark:.
The controls object advertises pause/previous/next_/seekTime — but
see the booby-trap warning: only use it for reading. Radio does not
appear here at all.

Transport on SUES — handle with gloves

Verb Effect on a Spotify stream
pause pauses :white_check_mark: (resume via 15081 playpause)
seekTime (ms) seeks, keeps playing :white_check_mark:
play kills the session :white_check_mark:
next_ / previous kills the session :white_check_mark:
# The one useful write: seek to 1:00 (ms)
curl "http://192.168.1.154/api/setData?path=player:player/control&roles=activate&value=%7B%22control%22%3A%22seekTime%22%2C%22time%22%3A60000%7D"

Recovery from a killed session: GET /inputs/spotify?cmd=select on
port 15081 :white_check_mark:.

Tone :white_check_mark:

Under settings:/mediaPlayer (double_, family range ±10 dB in 0.5
steps :warning: for the exact bounds):

Path What Status
settings:/mediaPlayer/bass Bass read+write :white_check_mark:
settings:/mediaPlayer/treble Treble read+write :white_check_mark:
settings:/mediaPlayer/balance Balance write :white_check_mark: — quirk: reads back as i32_, accepts double_ writes
settings:/mediaPlayer/loudness Loudness bool, read+write :white_check_mark:
# Bass to +1.5 dB
curl "http://192.168.1.154/api/setData?path=settings:/mediaPlayer/bass&roles=value&value=%7B%22type%22%3A%22double_%22%2C%22double_%22%3A1.5%7D"

Beware: the tree also holds variants like balance_Diva and
balance_NSS333 — other Naim models sharing the firmware; the bare
node is yours.

Other confirmed paths (reads)

Path What
settings:/deviceName “Naim Mu-so 2” — writable in the family :warning:
settings:/system/modelName “Mu-so”
presets: (getRows) 10 slots, all “Empty” :white_check_mark: — use /favourites instead
grouping: (getRows) SUES multiroom (4 nodes)
spotify: / airplay: / localfile: (getRows) Service trees
settings:/ (getRows) The full settings tree, 66 branches

Family quirk :white_check_mark:: getRows on a leaf errors — use getData for
leaves. Bare schemes like airable: and ui: do not exist on the
Naim (the Ellipse’s ui: duties are all on 15081 here).


Recipes

M=192.168.1.154

# Morning: wake + Radio Paradise
curl -s -X PUT "http://$M:15081/power?system=on" && sleep 2 && \
curl -s "http://$M:15081/favourites/972c3eaa8312470bb2a21e43dbbbd447?cmd=play"

# Evening: everything off
curl -s -X PUT "http://$M:15081/power?system=lona"

# Volume up a notch (read, +2, write)
V=$(curl -s "http://$M:15081/levels/room" | python3 -c "import json,sys;print(min(100,int(json.load(sys.stdin)['volume'])+2))") && \
curl -s -X PUT "http://$M:15081/levels/room?volume=$V"

# Pause/resume whatever plays
curl -s "http://$M:15081/nowplaying?cmd=playpause"

# One-line status (station, or artist — track, or idle)
curl -s "http://$M:15081/nowplaying" | python3 -c "
import json,sys
d = json.load(sys.stdin)
print(d.get('station') or ' — '.join(x for x in (d.get('artistName'), d.get('name')) if x) or 'idle')"

Apple Shortcuts: every line above is one “Get contents of URL”
action (set the method to GET or PUT). No headers or body needed —
even the SUES writes carry their value in the URL.

Home Assistant: rest_command: entries for the actions, plus a
rest sensor on /power (state) for on/off and one on
/nowplaying (transportState, transportPosition) while awake.


6 Likes

Mu-so app coming along quite nicely:

2 Likes

Super nice, awesome work. Once at the stage of beta testing, are you considering inviting beta testers via TestFlight ?