-
-
Notifications
You must be signed in to change notification settings - Fork 45
Headless Daemon API
This is the worked-example companion to Headless Daemon. The main page tells you what qbzd is and how to install it; this one is for people who want to build on top of it — a Plasma widget, a web dashboard, a bar applet, a home-automation rule, a shell script.
Everything here talks to the daemon's small HTTP control plane on :8182. Every JSON body below is an illustrative dummy — real field values will differ, but the shapes are accurate. Read verbs (anything that returns data) also work from the CLI with --json, which prints the exact same payloads.
Contract: the JSON shapes and the CLI exit codes are stable within an API version (
api_version, currently1). Parse--json/ the HTTP JSON, never the human-readable text.
-
Base URL:
http://<host>:8182(default bind0.0.0.0, so any LAN client works). Point the CLI at another box withQBZD_HOST=<ip>:8182. -
Origin shield (always on): any request carrying an
Originheader is rejected403. Native clients (curl, QML, scripts,reqwest) don't send one; a browserfetch()does — so a browser page can't drive the port (CSRF protection). An<img>tag sends noOrigin, so image URLs still work. -
Optional token: if
[server] tokenis set inqbzd.toml, every route except/api/pingneedsAuthorization: Bearer <token>.
# Liveness — always answers, even in token mode.
curl -s http://192.168.0.40:8182/api/ping{ "ok": true, "app": "qbzd", "api_version": 1 }GET /api/status is the whole daemon in one document — the first call any dashboard makes.
curl -s http://192.168.0.40:8182/api/status | jq{
"version": "2.0.2",
"api_version": 1,
"uptime_secs": 48213,
"data_root": "/home/pi/.local/share/qbzd",
"driver_tick_age_ms": 412,
"auth": { "state": "logged_in", "user_id": 10385965, "subscription": "hi-res" },
"audio": { "backend": "alsa", "configured_device": "hw:CARD=D30,DEV=0",
"device_present": true, "device_open": true,
"bit_perfect": "exclusive", "sample_rate": 192000, "bit_depth": 24 },
"playback":{ "state": "playing", "track_id": 52960027, "title": "Spain",
"artist": "Chick Corea", "position": 74, "duration": 592 },
"qconnect":{ "enabled": true, "connected": true, "device_name": "QBZ (kitchen-pi)" },
"network": { "bind": "0.0.0.0:8182", "token": false, "reachable": true },
"last_errors": { "audio": null, "network": null, "auth": null }
}GET /api/now-playing is the lighter poll for a "now playing" widget: the live playback block plus the full current track.
curl -s http://192.168.0.40:8182/api/now-playing | jq{
"playback": { "state": "playing", "position": 74, "duration": 592,
"volume": 0.8, "muted": false, "shuffle": false, "repeat": "off",
"sample_rate": 192000, "bit_depth": 24, "queue_len": 14 },
"track": { "id": 52960027, "title": "Spain", "artist": "Chick Corea",
"album": "Light as a Feather", "duration_secs": 592, "hires": true,
"bit_depth": 24, "sample_rate": 192000,
"artwork_url": "https://static.qobuz.com/images/covers/xy/…_600.jpg" }
}When nothing is loaded, "track": null.
All transport is POST with a small (often empty) JSON body; each returns the resulting state.
curl -s -XPOST http://192.168.0.40:8182/api/playback/toggle # play/pause
curl -s -XPOST http://192.168.0.40:8182/api/playback/next # advance
curl -s -XPOST http://192.168.0.40:8182/api/playback/seek -d '{"position": 90}'
curl -s -XPOST http://192.168.0.40:8182/api/playback/volume -d '{"volume": 0.5}'
curl -s -XPOST http://192.168.0.40:8182/api/playback/shuffle -d '{"mode": "toggle"}'{ "state": "paused" } // toggle
{ "position": 90, "duration": 592 } // seek (clamped target)
{ "volume": 0.5, "muted": false } // volume
{ "shuffle": true } // shuffleQueue inspection and editing:
curl -s http://192.168.0.40:8182/api/queue | jq
curl -s -XPOST http://192.168.0.40:8182/api/queue/add -d '{"track_ids": [52960027, 274703980]}'
curl -s -XPOST http://192.168.0.40:8182/api/queue/jump -d '{"index": 3}'{
"current_index": 2, "total": 14,
"tracks": [
{ "index": 0, "id": 274703980, "title": "500 Miles High", "artist": "Chick Corea", "duration_secs": 528 },
{ "index": 1, "id": 52960024, "title": "Captain Marvel", "artist": "Chick Corea", "duration_secs": 274 }
]
}curl -s 'http://192.168.0.40:8182/api/search?q=chick%20corea&type=album&limit=5' | jq{
"query": "chick corea", "type": "album", "limit": 5, "offset": 0,
"albums": [
{ "id": "0060753762913", "title": "Light as a Feather", "artist": "Chick Corea",
"tracks_count": 6, "hires": true, "release_date": "1973-01-01" }
],
"tracks": [], "artists": [], "playlists": []
}POST /api/play resolves anything — a track/album/artist id, a Qobuz share URL, or a Deezer link — and starts it:
curl -s -XPOST http://192.168.0.40:8182/api/play -d '{"content": "album:0060753762913"}'
curl -s -XPOST http://192.168.0.40:8182/api/play -d '{"content": "https://open.qobuz.com/album/0060753762913"}'{ "state": "playing", "started": { "kind": "album", "id": "0060753762913", "tracks": 6 } }Other reads follow the same pattern — GET /api/album?id=, /api/artist?id=, /api/similar?id=, /api/discover?section=new-releases, /api/lyrics?id=current, and POST /api/radio / POST /api/reco/playlist. Each returns a JSON envelope with the relevant list; add --ids on the CLI to get bare ids to pipe into queue add.
GET /api/artwork/current is a 302 redirect to the current track's cover. Point an <img> straight at it — it always shows the live art, and because an <img> request sends no Origin, the shield doesn't block it:
<img src="http://192.168.0.40:8182/api/artwork/current" alt="cover">404 when nothing is playing.
GET /api/events is a Server-Sent Events stream: subscribe once and react to everything instead of polling. Each frame is event: <Type> + a data: line carrying the tagged CoreEvent JSON.
curl -sN http://192.168.0.40:8182/api/events: qbzd event stream
event: TrackStarted
data: {"type":"TrackStarted","data":{"track":{"id":52960027,"title":"Spain","artist":"Chick Corea","album":"Light as a Feather","duration_secs":592,"artwork_url":"https://…_600.jpg"},"position_secs":0}}
event: PositionUpdated
data: {"type":"PositionUpdated","data":{"position_secs":1,"duration_secs":592}}
event: VolumeChanged
data: {"type":"VolumeChanged","data":{"volume":0.75}}
event: PlaybackStateChanged
data: {"type":"PlaybackStateChanged","data":{"state":"Paused"}}
Emitted types include TrackStarted, TrackEnded, PlaybackStateChanged, PositionUpdated, VolumeChanged, QueueUpdated, ShuffleChanged, RepeatModeChanged, FavoritesUpdated, PlaylistCreated/Updated/Deleted, LoggedIn/LoggedOut, and error events. Bulky/internal events (full search results, loading/download/navigation hints) are filtered out.
From the CLI, qbzd watch is the same stream as newline-delimited JSON (one data payload per line) — perfect for a status bar:
qbzd watch | jq -r 'select(.type=="TrackStarted") | "▶ " + .data.track.artist + " — " + .data.track.title'▶ Chick Corea — Spain
▶ Chick Corea — 500 Miles High
Every error is the same envelope; the CLI keys its exit code off error.code (never the HTTP status):
{ "error": { "code": "needs_auth", "message": "not logged in to Qobuz", "hint": "run: qbzd login" } }| Exit | Meaning | Example code
|
|---|---|---|
0 |
success | — |
1 |
runtime error | internal |
2 |
usage error | bad_request |
3 |
daemon unreachable | (transport) |
4 |
needs auth | needs_auth |
5 |
audio/device error | audio_unavailable |
6 |
not found / bad id | not_found |
qbzd now; echo "exit=$?" # exit=3 if the daemon isn't running — a monitor can key off thisOn Linux the daemon also publishes the standard MPRIS interface (org.mpris.MediaPlayer2), so for a Plasma/GNOME media widget you need zero of the above — the desktop already speaks MPRIS. Metadata, cover art and play/pause/next/seek/volume all flow through it. Use the HTTP API when you need something MPRIS doesn't cover (search, queue editing, browsing, favorites).
# Anything MPRIS-aware works out of the box, e.g.:
playerctl -p qbzd metadata title
playerctl -p qbzd play-pause#!/bin/sh
# Prints the current track once a second. Pipe to a bar, a notifier, anything.
while :; do
curl -s "$QBZD_HOST"/api/now-playing \
| jq -r '.track | if . == null then "— stopped —" else .artist + " — " + .title end'
sleep 1
doneFor anything event-driven (a widget that updates the instant a track changes), prefer qbzd watch / /api/events over polling.
See also: Headless Daemon (setup & the full CLI) · Casting and Remote Control · Build a Streaming Box
Getting Started
Audio
Appearance
My QBZ
Features
Help