Skip to content
WolfWave
Developers

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.

SettingValue
URLws://127.0.0.1:8765 (port is set in Settings → Stream Widgets)
Subprotocolwolfwave.control.<hex>
Where the token livesSettings → Stream Deck → Stream Deck Control Token
Who may connectLiteral 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:

  1. Stream Widgets server is running (Settings → Stream Widgets).
  2. 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": {} }
FieldRule
typeMust be "command". Anything else is ignored, with no ack
protocolMust be 3. A mismatch gets error: "protocol"
actionOne of the actions. Unknown gets error: "unknown_action"
argsOptional. 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" }
  • action echoes the token you sent, even an unknown one.
  • error appears only when ok is false.

Actions

actionWhat it doesPossible error
play_pausePlay or pause Apple Musicunavailable, music
skipSkip to the next trackunavailable, music
hold_queueHold the song-request queuenone
resume_queueResume the song-request queuenone
approve_nextApprove the oldest pending requestempty, full
clear_queueClear the request queue (and Music.app's queue)unavailable
block_currentAdd the current song title to the blocklistempty
overlay_toggleHide or show the overlay cards. The connection stays openunavailable
announce_songPost the current track to Twitch chattwitch, empty
reject_currentAnnounce "Request removed" in chat, then skip the playing requestunavailable, empty
block_requesterBlock whoever requested the playing songempty
cycle_audienceMove to the next request audience, wrapping aroundunavailable

cycle_audience steps through everyone → subscribers → vipsAndSubs → modsOnly.

Error codes

errorMeaning
protocolYour protocol number doesn't match WolfWave's
unknown_actionWolfWave doesn't know that action token
unauthorizedSent with the overlay token, or not from loopback
disabledAllow Stream Deck commands is off
unavailableThe service behind that action isn't ready
musicMusic.app refused the AppleScript call
emptyNothing to act on (no song, no pending request, no requester)
fullThe live request queue is at its cap; the request stays pending
twitchNot 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 protocol first, 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, or disconnected. 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:8765

Paste 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

On this page