API Reference
The web interface exposes a REST/HTML API on the resolved web port. In Home Assistant add-on mode the effective port is track-aware (8080 stable, 8081 rc, 8082 beta), and most operators reach it through HA Ingress rather than directly.
Status & Monitoring
Section titled “Status & Monitoring”GET /api/status
Section titled “GET /api/status”Bridge snapshot used by the dashboard and SSE stream. For backward compatibility the top-level object still mirrors one device; multi-device deployments also include a devices array plus bridge-wide fields such as groups, disabled_devices, startup_progress, runtime_mode, and operator_guidance.
{ "player_name": "Living Room Speaker", "connected": true, "bluetooth_connected": true, "has_sink": true, "volume": 48, "devices": [ { "player_name": "Living Room Speaker", "connected": true }, { "player_name": "Kitchen Speaker", "connected": false } ], "groups": [], "disabled_devices": [], "ma_connected": true, "startup_progress": { "status": "complete", "phase": "ready", "current_step": 6, "total_steps": 6, "percent": 100, "message": "Startup complete" }, "runtime_mode": "production", "operator_guidance": { "mode": "healthy", "header_status": { "tone": "success", "label": "1/1 active devices ready", "summary": "All active devices have sinks and are ready for playback." }, "issue_groups": [] }}GET /api/status/stream
Section titled “GET /api/status/stream”Server-Sent Events stream that emits the same bridge snapshot shape as /api/status.
Runtime contract:
- updates are batched with a 100 ms debounce window to avoid event storms;
- the first response starts with a 2 KiB SSE comment padding block so HA ingress/proxies flush immediately;
- a heartbeat comment is sent every 15 seconds;
- sessions are capped at 30 minutes and the server limits the stream to 4 concurrent listeners.
GET /api/startup-progress
Section titled “GET /api/startup-progress”Current startup/shutdown lifecycle snapshot.
{ "status": "running", "phase": "web", "current_step": 4, "total_steps": 6, "percent": 67, "message": "Web interface and event loop ready", "details": { "web_thread": "WebServer" }, "started_at": "2026-03-22T19:00:00+00:00", "updated_at": "2026-03-22T19:00:02+00:00", "completed_at": null}GET /api/runtime-info
Section titled “GET /api/runtime-info”Explains whether the bridge is running in production or demo/mock mode.
Key fields: mode, is_mocked, simulator_active, fixture_devices, fixture_groups, disclaimer, mocked_layers, details, updated_at.
GET /api/bridge/telemetry
Section titled “GET /api/bridge/telemetry”Bridge-level telemetry snapshot assembled from runtime state.
{ "bridge": { "uptime_seconds": 1234, "process_rss_mb": 84.1, "python": "3.13.2", "platform": "Linux-...", "arch": "x86_64", "kernel": "6.8.0", "audio_server": "PulseAudio 17.0", "bluez": "bluetoothctl: 5.79" }, "startup_progress": { "status": "complete" }, "runtime_info": { "mode": "production" }, "subprocesses": [], "event_hooks": { "delivery_mode": "runtime", "summary": { "registered_hooks": 0 } }}GET /api/hooks
Section titled “GET /api/hooks”Returns the runtime webhook registry snapshot:
delivery_mode: "runtime"summarywith registered/success/failure countshooks[]with current registrationsrecent_deliveries[]with the latest delivery attempts
POST /api/hooks
Section titled “POST /api/hooks”Register a runtime-scoped webhook for bridge/device events.
Body parameters:
| Field | Type | Description |
|---|---|---|
url | string | Required absolute http:// or https:// URL |
categories | string[] | Optional event-category filter |
event_types | string[] | Optional event-type filter |
timeout_sec | number | Optional request timeout, default 5.0 |
Notes: registrations are in-memory only; loopback, .local, and private-network targets are rejected.
Response: 201 Created
{ "success": true, "hook": { "id": "550e8400-e29b-41d4-a716-446655440000", "url": "https://example.net/sendspin-events", "categories": ["bridge"], "event_types": ["bridge.startup.completed"], "timeout_sec": 5.0 }}DELETE /api/hooks/<hook_id>
Section titled “DELETE /api/hooks/<hook_id>”Unregister a runtime hook. Returns { "success": true } or 404 when the hook does not exist.
GET /api/diagnostics
Section titled “GET /api/diagnostics”Comprehensive masked diagnostics snapshot. In addition to environment/adapters/sinks/device state, the response includes:
contract_versions(config_schema_version,ipc_protocol_version)startup_progressandruntime_infoma_integration,sink_inputs,subprocesses,event_hooksonboarding_assistant,recovery_assistant,operator_guidancetelemetry(same shape as/api/bridge/telemetry)
GET /api/bugreport
Section titled “GET /api/bugreport”Builds a GitHub-issue-friendly bug-report bundle from masked diagnostics.
Response fields:
| Field | Description |
|---|---|
markdown_short | Short markdown summary for issue bodies or clipboard use |
text_full | Full plain-text report |
suggested_description | Editable issue template derived from diagnostics |
report | Full masked structured report |
suggested_description is intentionally operator-editable. It is seeded from recent issue logs, Bluetooth connection health, device last_error fields, subprocess health, D-Bus / bluetoothd state, MA connectivity, and the top recovery-guidance issue.
GET /api/bugreport/proxy-available
Section titled “GET /api/bugreport/proxy-available”Returns {"available": true|false} indicating whether the GitHub-App issue-submission proxy is configured on this bridge. The web UI uses this to decide whether to offer in-UI “Report a problem” submissions for users without GitHub accounts.
POST /api/bugreport/submit
Section titled “POST /api/bugreport/submit”Creates a GitHub issue through the App proxy on behalf of a user who does not have a GitHub account. Requires the proxy to be configured (see /api/bugreport/proxy-available); otherwise returns 503.
Request body (JSON):
| Field | Description |
|---|---|
title | Issue title (5–200 characters) |
description | Issue description (10–5000 characters) |
email | Contact email, must contain @ |
diagnostics_text | Optional masked diagnostics text to attach (truncated to fit GitHub’s 65 536-char limit) |
Rate-limited per client IP by the proxy. Typical error responses: 400 (validation), 429 (rate limit), 503 (proxy not configured).
GET /api/diagnostics/download
Section titled “GET /api/diagnostics/download”Downloads the masked diagnostics report as a plain-text attachment (diagnostics-<timestamp>.txt).
GET /api/groups
Section titled “GET /api/groups”Returns configured players grouped by MA syncgroup. Groups may include external_members / external_count when the same MA syncgroup also contains players from other bridges.
GET /api/onboarding/assistant
Section titled “GET /api/onboarding/assistant”First-run/setup guidance derived from preflight checks, config, device state, and MA connectivity.
Key fields: runtime_mode, counts, checks[], next_steps[], and checklist.
The checklist always orders steps as: bluetooth, audio, sink_verification, ma_auth, latency.
GET /api/recovery/assistant
Section titled “GET /api/recovery/assistant”Recovery-oriented guidance derived from live device health and startup state.
Key fields: summary, issues[], traces[], safe_actions[], latency_assistant, and known_good_test_path.
GET /api/operator/guidance
Section titled “GET /api/operator/guidance”Unified dashboard guidance surface assembled from onboarding + recovery.
Key fields:
mode—empty_state,progress,attention, orhealthyvisibility_keys— local UI preference keys for dismissible cardsheader_status— compact status pill shown at the top of the dashboardbanner— optional attention banneronboarding_card— optional guided setup cardissue_groups[]— grouped recovery/setup issues with recommended actions
GET /api/version
Section titled “GET /api/version”{ "version": "2.x.y", "build_date": "2026-03-22" }GET /api/health
Section titled “GET /api/health”Lightweight health endpoint. Returns 200 OK with:
{ "ok": true }GET /api/preflight
Section titled “GET /api/preflight”Setup-verification endpoint used by onboarding and diagnostics.
{ "platform": "x86_64", "audio": { "system": "pulseaudio", "socket": "unix:/run/pulse/native", "sinks": 2 }, "bluetooth": { "controller": true, "adapter": "C0:FB:F9:62:D6:9D", "paired_devices": 3 }, "dbus": true, "memory_mb": 2048, "version": "2.x.y", "ok": true}GET /api/recovery/timeline
Section titled “GET /api/recovery/timeline”Structured chronological recovery timeline built from device health and startup events.
{ "summary": { "entry_count": 3 }, "entries": [ { "timestamp": "2026-03-22T19:00:00+00:00", "severity": "warning", "label": "BT reconnect", "summary": "..." } ]}GET /api/recovery/timeline/download
Section titled “GET /api/recovery/timeline/download”Download the current recovery timeline as a CSV attachment (sendspin-recovery-timeline-<timestamp>.csv).
POST /api/checks/rerun
Section titled “POST /api/checks/rerun”Rerun a single safe, non-destructive operator check (e.g. Bluetooth connectivity, audio sink verification).
Body:
{ "check_key": "bluetooth", "device_names": ["Living Room Speaker"] }| Field | Type | Description |
|---|---|---|
check_key | string | Required. The check identifier to rerun |
device_names | string[] | Optional. Scope the check to specific devices |
Response:
{ "check_key": "bluetooth", "summary": "All devices connected", "status": "pass" }Returns 400 if check_key is unknown.
GET /api/latency/recommendations
Section titled “GET /api/latency/recommendations”Return the current latency assistant payload from the recovery assistant. Includes recommended PULSE_LATENCY_MSEC value and explanations.
POST /api/latency/apply
Section titled “POST /api/latency/apply”Persist a recommended Pulse latency value to config.json. Requires a restart to take effect.
Body: { "pulse_latency_msec": 800 }
Response:
{ "success": true, "pulse_latency_msec": 800, "restart_required": true, "summary": "Saved Pulse latency 800 ms. Restart the bridge to apply the new buffer.", "latency_assistant": { }}Playback Control
Section titled “Playback Control”POST /api/pause_all
Section titled “POST /api/pause_all”Pause/resume all players.
Body: { "action": "pause" } or { "action": "play" }
POST /api/group/pause
Section titled “POST /api/group/pause”Pause or resume a specific MA sync group. For action="play", uses the MA REST API if configured so all group members resume in sync; falls back to Sendspin session command.
Body: { "group_id": "abc123", "action": "pause" } — action is "pause" or "play"
POST /api/volume
Section titled “POST /api/volume”Set volume on one or more devices. Supports individual, group, and multi-target modes.
Body parameters:
| Field | Type | Description |
|---|---|---|
volume | integer | Target volume (0–100). Required. |
mac | string | Target a single device by MAC address |
player_name | string | Target a single device by player name |
player_names | string[] | Target multiple devices by name |
group_id | string | Target all devices in a specific MA sync group |
group | boolean | When true, uses MA’s proportional group_volume for sync group members |
force_local | boolean | When true, bypasses MA API and uses direct PulseAudio (pactl) |
If no targeting field is provided (mac, player_name, player_names, group_id), volume is applied to all devices.
Routing logic (when VOLUME_VIA_MA is enabled and MA is connected):
group: true— sendsgroup_volumeonce per unique MA sync group among selected targets. MA applies a proportional delta, preserving relative volumes. Devices not in any sync group receive the exact value via direct PulseAudio.group: false(default) — sendsvolume_setto each target individually via MA API.- The response returns immediately with
"via": "ma". The UI updates when bridge_daemon receives the echo from MA (~500 ms).
Fallback: if MA is offline, VOLUME_VIA_MA is disabled, or force_local: true, volume is set directly via PulseAudio and status updates immediately.
// Individual{ "mac": "AA:BB:CC:DD:EE:FF", "volume": 75 }
// Group (proportional for sync groups, exact for solo devices){ "volume": 40, "group": true }
// Force local pactl{ "mac": "AA:BB:CC:DD:EE:FF", "volume": 50, "force_local": true }POST /api/pause
Section titled “POST /api/pause”Pause or resume a single player. Sends the command via IPC to the target daemon subprocess which forwards it over the existing WebSocket connection to MA.
Body: { "player_name": "Living Room Speaker", "action": "pause" } — action is "pause" or "play"
POST /api/mute
Section titled “POST /api/mute”Toggle mute on a device. When MUTE_VIA_MA is enabled and MA is connected, the mute command is routed through the MA API. Otherwise, mute is applied directly via PulseAudio.
Body: { "mac": "AA:BB:CC:DD:EE:FF", "muted": true }
POST /api/unmute_sink
Section titled “POST /api/unmute_sink”Recovery action: force-unmute the PulseAudio sink for a device, bypassing MA routing. Intended for situations where the PA sink is muted at the system level (for example after a crash or restart) while the application-level mute is already off, which would otherwise leave the speaker silent.
Body: { "player_name": "Living Room Speaker" }
Returns 404 if no matching device is found, 400 if the device has no configured audio sink, or 500 if pactl reports the sink could not be unmuted.
Transport Control
Section titled “Transport Control”POST /api/transport/cmd
Section titled “POST /api/transport/cmd”Send a native Sendspin transport command to a specific device. Bypasses the MA REST API for lower latency by sending the command through the Sendspin Controller WebSocket channel.
Body:
| Field | Type | Description |
|---|---|---|
action | string | Required. One of: play, pause, stop, next, previous, volume, mute, repeat_off, repeat_one, repeat_all, shuffle, unshuffle, switch |
device_index | integer | Required. Zero-based index into the active device list |
value | any | Optional. For volume (0–100) or mute (boolean) |
Returns 400 if the action is invalid or not in the device’s supported_commands set.
Response: { "success": true }
Music Assistant Integration
Section titled “Music Assistant Integration”MA credentials are persisted in config.json (MA_API_URL, MA_API_TOKEN). Successful auth flows automatically trigger group rediscovery so /api/status, /api/groups, and queue metadata can update without a restart.
GET /api/ma/discover
Section titled “GET /api/ma/discover”Start asynchronous Music Assistant discovery.
- In HA add-on mode the bridge prefers add-on-local MA URLs before falling back to saved config / mDNS.
- The response is always async.
{ "job_id": "550e8400-e29b-41d4-a716-446655440000", "status": "running", "is_addon": true }GET /api/ma/discover/result/<job_id>
Section titled “GET /api/ma/discover/result/<job_id>”Poll MA discovery.
- While running:
{ "status": "running", "is_addon": true } - On completion: returns the stored job payload (for example discovered servers or an error)
POST /api/ma/login
Section titled “POST /api/ma/login”Direct Music Assistant login using MA credentials.
Body:
{ "url": "http://192.168.1.10:8095", "username": "ma_user", "password": "ma_pass" }Contract notes:
urlis optional. If omitted, the bridge tries the saved MA URL,SENDSPIN_SERVER, connected Sendspin hosts, then mDNS.- This endpoint is for MA’s own credential flow. It does not switch between HA auth providers.
- On success the bridge saves the long-lived token and triggers rediscovery immediately.
GET /api/ma/ha-auth-page
Section titled “GET /api/ma/ha-auth-page”Returns a self-contained HTML popup document for browser-driven Home Assistant login/MFA.
Query parameter: ma_url
The popup posts its success/failure result back to window.opener; it is not a JSON endpoint.
POST /api/ma/ha-silent-auth
Section titled “POST /api/ma/ha-silent-auth”Silent add-on-mode auth using an existing HA access token.
Body:
{ "ha_token": "<HA access token>", "ma_url": "http://homeassistant.local:8095" }Contract notes:
- Intended for HA add-on runtime only.
- The bridge validates the HA token over the HA WebSocket API, creates an MA token via ingress JSON-RPC, validates that token against the regular MA API, then saves it.
- If a previously saved MA token already matches the same MA instance and still validates, the endpoint returns success without minting a new token.
POST /api/ma/ha-login
Section titled “POST /api/ma/ha-login”Explicit Home Assistant credential flow with optional MFA.
Step 1 — init:
{ "step": "init", "ma_url": "http://192.168.1.10:8095", "username": "ha_user", "password": "ha_pass" }Possible response:
{ "success": true, "step": "mfa", "auth_mode": "ha_direct", "flow_id": "...", "ha_url": "http://haos:8123", "client_id": "http://haos:8123/", "state": "...", "mfa_module_name": "Authenticator app" }Step 2 — mfa:
{ "step": "mfa", "ma_url": "http://192.168.1.10:8095", "flow_id": "...", "ha_url": "http://haos:8123", "client_id": "http://haos:8123/", "auth_mode": "ha_direct", "code": "123456" }A successful completion returns step: "done", saves the MA token, and triggers rediscovery.
GET /api/ma/groups
Section titled “GET /api/ma/groups”Returns cached MA syncgroup players from the MA API. Empty list means discovery has not run yet or MA is not configured.
POST /api/ma/rediscover
Section titled “POST /api/ma/rediscover”Re-run MA syncgroup discovery from the currently saved MA_API_URL / MA_API_TOKEN.
Response: 202 Accepted
{ "success": true, "job_id": "550e8400-e29b-41d4-a716-446655440000", "status": "running" }GET /api/ma/rediscover/result/<job_id>
Section titled “GET /api/ma/rediscover/result/<job_id>”Poll the async rediscovery job.
- While running:
{ "status": "running" } - On completion: returns the stored job payload (for example
syncgroups,mapped_players,groups, or an error)
GET /api/ma/nowplaying
Section titled “GET /api/ma/nowplaying”Returns the bridge’s current MA now-playing cache.
- When MA is inactive:
{ "connected": false } - When active: includes
state,track,artist,album,image_url,elapsed,duration,shuffle,repeat,queue_index,queue_total,syncgroup_id, and optional adjacent-track metadata.
POST /api/ma/queue/cmd
Section titled “POST /api/ma/queue/cmd”Send an asynchronous playback-control command to the active MA queue.
Body:
{ "action": "next", "syncgroup_id": "ma-syncgroup-abc123" }| Field | Description |
|---|---|
action | "next", "previous", "shuffle", "repeat", or "seek" |
value | For shuffle: boolean. For repeat: "off", "all", "one". For seek: seconds |
syncgroup_id | Optional syncgroup target |
player_id | Optional explicit player target |
group_id | Optional legacy group target |
Accepted response:
{ "success": true, "job_id": "550e8400-e29b-41d4-a716-446655440000", "op_id": "6a6f...", "syncgroup_id": "ma-syncgroup-abc123", "queue_id": "up_abc123def456", "accepted": false, "confirmed": false, "pending": true, "ma_now_playing": { "state": "playing" }}The HTTP response is optimistic: it includes a predicted ma_now_playing patch immediately, while final confirmation arrives through the async job plus MaMonitor updates.
GET /api/ma/queue/cmd/result/<job_id>
Section titled “GET /api/ma/queue/cmd/result/<job_id>”Poll the async MA queue-command job.
- While running:
{ "status": "running" } - On completion: returns the stored job payload
GET /api/debug/ma
Section titled “GET /api/debug/ma”Debug dump of MA cache keys, discovered groups, per-client player IDs, and live queue IDs fetched from the MA WebSocket.
POST /api/ma/reload
Section titled “POST /api/ma/reload”Reload MA runtime pieces (credentials, WebSocket monitor, syncgroup cache) without restarting the full bridge service. Reads current MA_API_URL / MA_API_TOKEN from config.json and triggers group rediscovery.
Response (202):
{ "success": true, "job_id": "550e8400-e29b-41d4-a716-446655440000", "status": "running", "monitor_reloaded": true}GET /api/ma/artwork
Section titled “GET /api/ma/artwork”Proxy MA artwork images through the bridge so the web UI can use same-origin image URLs (avoids CORS issues).
Query parameters:
| Param | Type | Description |
|---|---|---|
url | string | Required. Artwork path or full URL on the MA server |
sig | string | Required. HMAC signature for SSRF protection |
Returns the artwork binary with the original Content-Type (e.g. image/jpeg). The MA bearer token is attached only when the URL targets the MA server origin. Returns 400 for invalid URLs or signatures, 413 for images exceeding 10 MB.
Bluetooth Control
Section titled “Bluetooth Control”POST /api/bt/reconnect
Section titled “POST /api/bt/reconnect”Force Bluetooth reconnect.
Body: { "mac": "AA:BB:CC:DD:EE:FF" }
POST /api/bt/pair
Section titled “POST /api/bt/pair”Start pairing (~25 sec). Device must already be in pairing mode.
Body: { "mac": "AA:BB:CC:DD:EE:FF", "adapter": "hci0" }
POST /api/bt/management
Section titled “POST /api/bt/management”Toggle Bluetooth management (Release/Reclaim).
Body: { "player_name": "Living Room", "enabled": false }
POST /api/device/enabled
Section titled “POST /api/device/enabled”Toggle global device enabled/disabled state. Requires a restart to take effect — disabled devices are skipped completely (no client, no BT manager, no listen port).
Body: { "player_name": "Living Room", "enabled": false }
Response:
{ "success": true, "enabled": false, "restart_required": true, "message": "Device will be disabled after restart"}POST /api/bt/scan
Section titled “POST /api/bt/scan”Start a background Bluetooth scan (~10 s). Returns immediately with a job ID.
Response: { "job_id": "550e8400-e29b-41d4-a716-446655440000" }
GET /api/bt/scan/result/<job_id>
Section titled “GET /api/bt/scan/result/<job_id>”Poll for scan results.
Response while running:
{ "status": "running" }Response when complete:
{ "status": "done", "devices": [ { "mac": "AA:BB:CC:DD:EE:FF", "name": "JBL Flip 5" } ]}Response on error:
{ "status": "done", "error": "Scan failed: bluetoothctl timed out" }GET /api/bt/adapters
Section titled “GET /api/bt/adapters”List available Bluetooth adapters.
GET /api/bt/paired
Section titled “GET /api/bt/paired”List currently paired devices (name + MAC).
POST /api/bt/remove
Section titled “POST /api/bt/remove”Remove (unpair) a device from the BlueZ stack.
Body: { "mac": "AA:BB:CC:DD:EE:FF" }
Response: { "ok": true, "mac": "AA:BB:CC:DD:EE:FF" }
POST /api/bt/info
Section titled “POST /api/bt/info”Return detailed bluetoothctl info for a device including pairing/trust/connection status.
Body: { "mac": "AA:BB:CC:DD:EE:FF" }
Response:
{ "mac": "AA:BB:CC:DD:EE:FF", "name": "Living Room Speaker", "alias": "Living Room Speaker", "paired": "yes", "bonded": "yes", "trusted": "yes", "connected": "yes", "icon": "audio-card", "raw": ["Device AA:BB:CC:DD:EE:FF ...", "..."]}POST /api/bt/disconnect
Section titled “POST /api/bt/disconnect”Disconnect a Bluetooth device without removing its pairing.
Body: { "mac": "AA:BB:CC:DD:EE:FF" }
Response: { "ok": true, "mac": "AA:BB:CC:DD:EE:FF" }
POST /api/bt/adapter/power
Section titled “POST /api/bt/adapter/power”Toggle a Bluetooth adapter’s power state.
Body:
| Field | Type | Description |
|---|---|---|
adapter | string | Adapter identifier (hci0, hci1, or MAC address). Empty = default adapter |
power | boolean | true to power on, false to power off (default: true) |
Response: { "ok": true, "power": true }
POST /api/bt/wake
Section titled “POST /api/bt/wake”Wake a device from idle-timeout standby (reconnects Bluetooth and restarts the daemon subprocess).
Body: { "player_name": "Living Room Speaker" }
Returns 409 if the device is not currently in standby.
Response: { "success": true, "message": "Device waking from standby" }
POST /api/bt/standby
Section titled “POST /api/bt/standby”Put a device into standby mode (disconnects Bluetooth and parks the daemon subprocess on a null sink). Reduces power consumption for idle speakers.
Body: { "player_name": "Living Room Speaker" }
Returns 409 if the device is already in standby.
Response: { "success": true, "message": "Device entering standby" }
POST /api/bt/reset_reconnect
Section titled “POST /api/bt/reset_reconnect”Remove a device and re-pair from scratch. Runs asynchronously: remove → power cycle → scan → pair → trust → connect.
Body:
| Field | Type | Description |
|---|---|---|
mac | string | Required. Device MAC address |
adapter | string | Optional. Adapter identifier (hci0 or MAC address) |
Returns 409 if another Bluetooth operation is in progress.
Response: { "job_id": "550e8400-e29b-41d4-a716-446655440000" }
GET /api/bt/reset_reconnect/result/<job_id>
Section titled “GET /api/bt/reset_reconnect/result/<job_id>”Poll for reset & reconnect result.
Response (running): { "status": "running" }
Response (done):
{ "status": "done", "success": true, "connected": true, "mac": "AA:BB:CC:DD:EE:FF" }POST /api/bt/pair_new
Section titled “POST /api/bt/pair_new”Pair a new Bluetooth device by MAC address (no existing bridge client required). Used for devices discovered via scan that are not yet in the bridge config.
Body:
| Field | Type | Description |
|---|---|---|
mac | string | Required. Device MAC address |
adapter | string | Optional. Adapter identifier |
Returns 409 if another Bluetooth operation is in progress.
Response: { "job_id": "550e8400-e29b-41d4-a716-446655440000" }
GET /api/bt/pair_new/result/<job_id>
Section titled “GET /api/bt/pair_new/result/<job_id>”Poll for standalone pair result.
Response (running): { "status": "running" }
Response (done): { "status": "done", "success": true, "mac": "AA:BB:CC:DD:EE:FF" }
System
Section titled “System”GET /api/logs
Section titled “GET /api/logs”Recent log lines from the bridge. Useful for debugging without SSH access.
Query parameters:
lines— number of lines to return (default100)
GET /api/logs/download
Section titled “GET /api/logs/download”Download full service logs (up to 500 lines) as a text file attachment (sendspin-logs-<timestamp>.txt). Reads from journalctl, HA Supervisor, or docker logs depending on the runtime.
POST /api/restart
Section titled “POST /api/restart”Restart the bridge process (causes container/service restart).
POST /api/set-password
Section titled “POST /api/set-password”Set or change the web UI password. Not available in HA addon mode (use HA user management instead).
Body: { "password": "mysecretpassword" } (min 8 characters)
Response: { "success": true }
POST /api/settings/log_level
Section titled “POST /api/settings/log_level”Change log level immediately and persist to config.json. Propagates to all running subprocesses via stdin IPC — no restart needed.
Body: { "level": "debug" } — "info" or "debug"
Response: { "success": true, "level": "DEBUG" }
POST /api/update/check
Section titled “POST /api/update/check”Start an asynchronous version check against GitHub releases. Accepts an optional channel field (stable, beta, rc); defaults to the configured UPDATE_CHANNEL.
Body (optional): { "channel": "stable" }
Response (202): { "job_id": "...", "status": "running", "channel": "stable" }
GET /api/update/check/result/<job_id>
Section titled “GET /api/update/check/result/<job_id>”Poll for async update-check result.
Response (running): { "status": "running", "channel": "stable" }
Response (done):
{ "success": true, "update_available": true, "tag": "v2.28.2", "version": "2.28.2", "current_version": "2.28.1" }GET /api/update/info
Section titled “GET /api/update/info”Get cached update information without triggering a new check.
POST /api/update/apply
Section titled “POST /api/update/apply”Apply a pending update. In HA addon mode, triggers addon update via Supervisor API. In Docker/LXC mode, returns instructions.
Configuration
Section titled “Configuration”GET /api/config
Section titled “GET /api/config”Current configuration from config.json. In HA add-on mode the response also exposes _delivery_channel, _effective_web_port, and _effective_base_listen_port, while WEB_PORT is intentionally returned as null because ingress owns the external port. Per-device preferred_format defaults to flac:44100:16:2 when omitted from stored config.
POST /api/config
Section titled “POST /api/config”Save configuration. Body: JSON config object. Effective add-on track defaults (_delivery_channel, effective ports) are runtime-derived rather than user-settable fields.
GET /api/config/download
Section titled “GET /api/config/download”Download the current config.json as a file attachment. The response includes a Content-Disposition header with a timestamped filename (e.g. config-2026-03-15T10-30-00.json).
POST /api/config/upload
Section titled “POST /api/config/upload”Upload a config.json file. Accepts multipart/form-data with a file field containing the JSON config.
The uploaded file is validated as valid JSON before saving. Sensitive keys (AUTH_PASSWORD_HASH, SECRET_KEY, MA_API_TOKEN) are preserved from the current config and not overwritten by the upload.
Response:
{ "success": true, "message": "Configuration uploaded successfully" }Error response (invalid JSON):
{ "success": false, "error": "Invalid JSON in uploaded file" }POST /api/config/validate
Section titled “POST /api/config/validate”Validate a config payload without persisting it. Returns validation errors, warnings, and the normalized config.
Body: A complete or partial JSON config object.
Response (200 — valid):
{ "valid": true, "errors": [], "warnings": [{ "field": "BLUETOOTH_DEVICES[0].mac", "message": "..." }], "normalized_config": { }}Response (400 — invalid):
{ "valid": false, "errors": [{ "field": "SENDSPIN_PORT", "message": "Invalid SENDSPIN_PORT: abc" }], "warnings": [], "normalized_config": { }}POST /api/ha/areas
Section titled “POST /api/ha/areas”Fetch Home Assistant area suggestions using a transient HA token. Useful for the adapter-to-area mapping UI. Only works in HA addon mode or when HA is reachable.
Body:
| Field | Type | Description |
|---|---|---|
ha_token | string | Required. A valid HA access token |
adapters | object[] | Optional. List of adapter objects to match against HA devices |
include_devices | boolean | Optional. Include HA device details in the response |
Response:
{ "success": true, "areas": [{ "area_id": "living_room", "name": "Living Room" }], "bridge_name_suggestions": ["Living Room Bridge"]}Authentication
Section titled “Authentication”GET /login
Section titled “GET /login”Render the login page. Automatically detects available authentication methods:
- HA addon mode — Home Assistant login flow with 2FA/TOTP support
- MA connected — Music Assistant credential validation (or HA-via-MA)
- Standalone — Local password authentication (PBKDF2-SHA256)
POST /login
Section titled “POST /login”Process login form submission. Validates credentials against the detected backend, enforces CSRF protection, and applies brute-force lockout (configurable via BRUTE_FORCE_* settings).
On success, sets an authenticated session and redirects to the requested page. On failure, re-renders the login page with an error message.
GET /logout
Section titled “GET /logout”Clear the authenticated session and redirect to the login page.
Examples
Section titled “Examples”# Get all player statuscurl http://localhost:8080/api/status
# Subscribe to live status updates (SSE)curl -N http://localhost:8080/api/status/stream
# Set volume to 50% on a specific devicecurl -X POST http://localhost:8080/api/volume \ -H 'Content-Type: application/json' \ -d '{"mac": "AA:BB:CC:DD:EE:FF", "volume": 50}'
# Pause all playerscurl -X POST http://localhost:8080/api/pause_all \ -H 'Content-Type: application/json' \ -d '{"action": "pause"}'
# Pause a specific MA sync groupcurl -X POST http://localhost:8080/api/group/pause \ -H 'Content-Type: application/json' \ -d '{"group_id": "abc123", "action": "pause"}'
# Skip to next track (requires MA API configured)curl -X POST http://localhost:8080/api/ma/queue/cmd \ -H 'Content-Type: application/json' \ -d '{"action": "next"}'
# Start BT scan and poll for resultsJOB=$(curl -s -X POST http://localhost:8080/api/bt/scan | python3 -c "import sys,json; print(json.load(sys.stdin)['job_id'])")curl http://localhost:8080/api/bt/scan/result/$JOB
# Get diagnosticscurl http://localhost:8080/api/diagnostics | python3 -m json.tool
# Change log level at runtimecurl -X POST http://localhost:8080/api/settings/log_level \ -H 'Content-Type: application/json' \ -d '{"level": "debug"}'