api reference
Reading
GET /api/v1/zones → { "zones": [ … ] }
GET /api/v1/zones/{id} → { … } 404 zone-not-found
GET /api/v1/zones/{id}/equalizer → { "zoneId": 3, "bands": [ …10 ] }
GET /api/v1/zones/{id}/queue → { "items": [ … ], "start": 0, "total": 42, "currentIndex": 3 }
GET /api/v1/zones/{id}/favorites → { "items": [ … ], "start": 0, "total": 8 }
GET /api/v1/zones/{id}/recents → { "items": [ … ], "start": 0, "total": 20 }
GET /api/v1/destinations → { "destinations": [ … ] }
GET /api/v1/audio-servers → { "selfId": "…", "servers": [ … ] }
GET /api/v1/services → { "services": [ … ] }
GET /api/v1/browse → the root: one entry per service
GET /api/v1/browse/{id} → { "container": …, "items": [ … ], "start": 0, "total": 42 }
GET /api/v1/items/{id} → one item 404 not-found
GET /api/v1/search?q=… → { "items": { "track": [ … ] }, … }
GET /api/v1/playlists → { "items": [ … ], "total": 1 }
GET /api/v1/waveform?uri=… → { "uri": …, "buckets": [ … ] } 404 no-waveform
GET /api/v1/inputs → { "inputs": [ … ] }
GET /api/v1/health → { "status": "ok"|"degraded"|"unhealthy", … }
GET /api/v1/ready → { "ready": true, "phase": "ready" } 503 when not
GET /api/v1/zones/{id}/cover → the image itself 404 when the zone has none
Supervising the server
Two endpoints, because "is it working?" and "can I stop waiting?" are different questions.
GET /api/v1/ready is the cheap one — poll it every second if you like:
{ "ready": true, "phase": "ready" }
phase is starting, ready or failed, and the status code carries the same answer: 200 when ready, 503 when not. That distinction is the point — a server that is still booting and one that died during boot both fail to answer, but only the second needs you. A failed phase adds error with the reason. This is also what to poll after a restart, instead of sleeping and hoping.
GET /api/v1/health gives a verdict you can act on:
{
"status": "degraded",
"version": "4.0.0-beta.17",
"uptimeSec": 451,
"phase": "ready",
"checks": [
{ "name": "audio", "status": "degraded", "detail": "last playback attempt failed on Study (ffmpeg exited 1)" },
{ "name": "loxone", "status": "ok" }
]
}
Three statuses, and the middle one matters most:
| status | code | what to do |
|---|---|---|
ok | 200 | nothing |
degraded | 200 | look, but do not restart — it is still serving |
unhealthy | 503 | intervene |
degraded deliberately stays a 200. The usual reaction to a non-2xx is a restart, and restarting a server whose Loxone link is down or whose one zone has a failing encoder fixes nothing while interrupting every zone that was fine. If you want a single boolean for a docker healthcheck or a load balancer, the status code already is one — only unhealthy fails.
checks[] is keyed by a stable name, so branch on that rather than parsing detail (which is prose, aimed at whoever has to fix it, and may change). Healthy checks carry no detail. A check appears only where it is meaningful: a server no Miniserver has ever paired with reports no loxone check at all, because an absent integration is not a broken one.
uptimeSec counts from when the server last became ready, not from process start — otherwise a restart keeps counting through a window in which nothing was served.
Neither endpoint needs a session.
Cover art
track.coverUrl is the artwork's real location, and it changes every track — it can be a remote CDN, a data uri, or a url only reachable from the server. That is fine if you read state and update an <img> each time, but not if you want one address you can point a wall panel or a Loxone visualisation at and forget about.
animatedCoverUrl sits beside it on a track, a queue entry and a browse item, when the provider has motion artwork for that release. It is absent rather than empty when there is none, which is the common case — so treat it as an enhancement over coverUrl and never as a replacement: coverUrl is always the one to fall back to, and a client that ignores animatedCoverUrl entirely is correct, just stiller.
GET /api/v1/zones/{id}/cover is that address. It names only the zone, returns the image bytes, and follows whatever that zone is playing:
# Always shows the kitchen's current cover http://server:7090/api/v1/zones/3/cover http://server:7090/api/v1/zones/3/cover?size=300
size is a hint. Where the provider offers variants (Apple Music, TuneIn, the image proxy) the server asks upstream for one near that size, which is sharper and cheaper than scaling here; otherwise it is ignored. Out-of-range or unparseable values fall back to the default rather than erroring, so an <img src> that cannot handle a 400 stays safe.
A zone playing nothing, or an unknown zone, answers 404 — draw your own placeholder rather than expecting a blank image.
Keeping it fresh
Because the url does not change when the track does, caching needs a way to tell one cover from the next. Every response carries an ETag hashed from the artwork itself, alongside Cache-Control: public, max-age=10:
curl -s -o cover.jpg -D - http://server:7090/api/v1/zones/3/cover
# ETag: "xLp3…"
# Still the same cover → 304, no body transferred
curl -s -o /dev/null -w '%{http_code}\n' \
-H 'If-None-Match: "xLp3…"' http://server:7090/api/v1/zones/3/cover
So polling costs almost nothing while a track plays, and the moment it changes you get bytes again. The tag is derived from the source and the requested size, which makes it stable across restarts and identical on two servers holding the same art — not a counter you have to store.
Some clients ignore all of that. A Loxone visualisation will hold an <img src> far longer than max-age suggests, and there is no header that fixes it. For those, add any parameter of your own and vary it when the track changes — unknown parameters are ignored:
http://server:7090/api/v1/zones/3/cover?v=applemusic:track:b64_MTc4MDM4MjY5NQ==
track.coverUrl from /api/v1/events is a convenient value to put there: it changes exactly when the picture does.