Stream Deck Control API
The WebSocket protocol behind WolfWave's Stream Deck keys. Protocol v3 command envelope, 12 actions, acks, error codes, and live queue and health frames.
TL;DR: a client on the same Mac connects to WolfWave's WebSocket with the control token, sends JSON commands, and gets an ack for each. This page is the wire protocol, for people building their own client.
Just want Stream Deck keys? Install the Stream Deck plugin instead. You don't need this page.
Connect
Commands share the overlay's WebSocket listener but use their own role and secret.
| Setting | Value |
|---|---|
| URL | ws://127.0.0.1:8765 (port is set in Settings → Stream Widgets) |
| Subprotocol | wolfwave.control.<hex> |
| Where the token lives | Settings → Stream Deck → Stream Deck Control Token |
| Who may connect | Literal loopback only (127.0.0.0/8 or ::1) |
- The server rechecks the selected subprotocol after the handshake.
- An overlay client (
wolfwave.overlay.<hex>) can read state, even over the LAN, but cannot run commands. - There is no per-command token. The connection's role decides.
- Regenerating the control token disconnects current clients.
Full auth model: Security.
Before commands will run
Two switches must be on:
- Stream Widgets server is running (Settings → Stream Widgets).
- Allow Stream Deck commands is on (Settings → Stream Deck). It's on by default.
If the second switch is off, every command gets error: "disabled". The overlay keeps working.
Send a command
Send one JSON text frame:
{ "type": "command", "action": "skip", "protocol": 3, "args": {} }| Field | Rule |
|---|---|
type | Must be "command". Anything else is ignored, with no ack |
protocol | Must be 3. A mismatch gets error: "protocol" |
action | One of the actions. Unknown gets error: "unknown_action" |
args | Optional. Unused in v3. Reserved for later |
WolfWave checks in that order, so an out-of-date client always learns about the version first.
Acks
Every command frame gets one ack, on the same connection:
{ "type": "ack", "action": "skip", "ok": true }
{ "type": "ack", "action": "skip", "ok": false, "error": "music" }actionechoes the token you sent, even an unknown one.errorappears only whenokisfalse.
Actions
action | What it does | Possible error |
|---|---|---|
play_pause | Play or pause Apple Music | unavailable, music |
skip | Skip to the next track | unavailable, music |
hold_queue | Hold the song-request queue | none |
resume_queue | Resume the song-request queue | none |
approve_next | Approve the oldest pending request | empty, full |
clear_queue | Clear the request queue (and Music.app's queue) | unavailable |
block_current | Add the current song title to the blocklist | empty |
overlay_toggle | Hide or show the overlay cards. The connection stays open | unavailable |
announce_song | Post the current track to Twitch chat | twitch, empty |
reject_current | Announce "Request removed" in chat, then skip the playing request | unavailable, empty |
block_requester | Block whoever requested the playing song | empty |
cycle_audience | Move to the next request audience, wrapping around | unavailable |
cycle_audience steps through everyone → subscribers → vipsAndSubs → modsOnly.
Error codes
error | Meaning |
|---|---|
protocol | Your protocol number doesn't match WolfWave's |
unknown_action | WolfWave doesn't know that action token |
unauthorized | Sent with the overlay token, or not from loopback |
disabled | Allow Stream Deck commands is off |
unavailable | The service behind that action isn't ready |
music | Music.app refused the AppleScript call |
empty | Nothing to act on (no song, no pending request, no requester) |
full | The live request queue is at its cap; the request stays pending |
twitch | Not connected to Twitch chat |
Removed in v3
discord_toggle, music_sync_toggle, and cycle_theme are gone.
- A v3 client sending one gets
unknown_action. - A v2 client gets
protocolfirst, since the version is checked first.
Live state frames
WolfWave pushes two frames to every connected client, so keys render without polling:
{ "type": "queue_state", "data": { "count": 3, "pending": 1, "held": false, "audience": "everyone" } }
{ "type": "health", "data": { "music": true, "twitch": true, "discord": false, "discordState": "disconnected", "overlay": true } }queue_state fields:
count: requests in the queue.pending: requests waiting for approval.held: whether the queue is on hold.audience: who may use!sr.
health fields:
music: a track is loaded in Music.app.twitch: connected to Twitch chat.discord: Rich Presence is really connected.discordState:off,connecting,connected, ordisconnected. Tells "switched off" from "broken".overlay: the server is listening and the overlay is visible.
They're sent when:
- the request queue changes;
- Twitch or Discord connects or disconnects;
- a client connects;
- a command succeeds.
Control clients also receive the overlay frames (now_playing, progress, and so on).
While the overlay is hidden, they still get now_playing on every track change, so keys never go stale.
The overlay frames are documented on the OBS Widget page.
Try it from a terminal
With the server on and Music playing, use websocat on the same Mac:
websocat --protocol "wolfwave.control.<your-control-token>" ws://127.0.0.1:8765Paste this and press Return:
{"type":"command","action":"skip","protocol":3}You should see an ack and hear the track skip. Leave it open to watch queue_state and health arrive.
Keeping the plugin in sync
- The Swift side lives in
Services/WebSocket/StreamDeckCommand.swift(StreamDeckControl.protocolVersion). - The plugin mirror lives in
apps/streamdeck/src/wolfwave/protocol.ts(PROTOCOL_VERSION,ACTIONS). - Change one, change the other in the same PR. Bump the version on any breaking change.
- The plugin's hold-to-confirm on Clear Queue and Block Requester is plugin behavior. The app runs those commands whenever they arrive.
See also
- Stream Deck: install and set up the official plugin.
- OBS Widget: the overlay side of the same socket.
- Security: the token handshake.
OBS Widget
How WolfWave's OBS now-playing overlay works. Build pipeline, WebSocket message contract, themes and layouts, URL parameters, transitions, and security.
Security
How WolfWave protects Twitch and local WebSocket credentials with macOS Keychain, App Sandbox, OAuth Device Code flow, and signed updates.