This document describes the local WebSocket JSON-RPC protocol used by Elgato Wave Link and exercised by the official Stream Deck “Wave Link 3” plugin.
Notes
- Transport is local-only (
127.0.0.1).- Payloads are JSON-RPC 2.0.
- This protocol is not an official public spec from Elgato; it is based on observable behavior and the plugin’s request/notification shapes.
ws://127.0.0.1:<port>
Wave Link exposes a single port written to a JSON file.
The official Stream Deck plugin attempts, in order:
- Read
ws-info.jsonand use theportvalue. - If the file cannot be read / parsed, scan the fallback port range 1884–1893.
The plugin derives the file path from %APPDATA% by replacing the Roaming segment:
%APPDATA% → ...\AppData\Roaming
becomes
...\AppData\Local\Packages\Elgato.WaveLink_g54w8ztgkx496\LocalState\ws-info.json
So the resulting Windows path is typically:
%LOCALAPPDATA%\Packages\Elgato.WaveLink_g54w8ztgkx496\LocalState\ws-info.json
{ "port": 1884 }The official plugin sets:
Origin: streamdeck://
Your client should set this header for maximum compatibility.
All frames are UTF-8 JSON.
{
"id": 1,
"jsonrpc": "2.0",
"method": "getApplicationInfo",
"params": null
}Notes:
idis an integer chosen by the client.jsonrpcmust be"2.0".paramsis commonlynullfor no parameters (the official plugin may omit params for some calls).
Success:
{ "jsonrpc": "2.0", "id": 1, "result": { } }Error:
{
"jsonrpc": "2.0",
"id": 1,
"error": { "code": -32601, "message": "Method not found" }
}{
"jsonrpc": "2.0",
"method": "inputDevicesChanged",
"params": { "inputDevices": [ /* ... */ ] }
}Notifications have no id.
Method names used by the official Stream Deck plugin:
| Method | Purpose |
|---|---|
getApplicationInfo |
Identify server + protocol revision |
setPluginInfo |
Inform Wave Link which Stream Deck device/plugin is connected (optional for 3rd-party clients) |
getInputDevices |
List input devices and their inputs |
setInputDevice |
Update input(s) on a device (mute/gain/mic-pc-mix/effects/dspEffects) |
getOutputDevices |
List output devices and main output selection |
setOutputDevice |
Update output device (mute/level) OR set main output |
getChannels |
List channels (including apps, per-mix data, and overall channel states) |
setChannel |
Update channel properties (overall mute/level, per mix, channel effects) |
addToChannel |
Route a focused app into a channel |
getMixes |
List mixes |
setMix |
Update mix properties |
setSubscription |
Enable/disable notifications (e.g., focused app, level meters) |
This section lists minimum fields required by the official plugin and the TS Beta 4 types, plus known optional fields reverse-engineered from the JS source.
{
"appID": "EWL",
"name": "Elgato Wave Link",
"interfaceRevision": 1
}- The official plugin checks
appID === "EWL"andinterfaceRevision >= 1.
Result:
{ "inputDevices": [ /* InputDevice[] */ ] }InputDevice (common fields):
id: stringname: stringisWaveDevice: boolean(observed by plugin)inputs: Input[]
Input (common fields):
id: stringname: stringisMuted?: booleanisGainLockOn?: boolean(Hardware level gain-lock state)gain?: { "value": number, "max"?: number }valueis normalized 0..1maxis used by the plugin to map normalized value to dB (actual dB = value × max)
micPcMix?: { "value": number, "isInverted"?: boolean }(normalized 0..1)effects?: Effect[](Software audio effects)dspEffects?: Effect[](Hardware DSP audio effects)
Effect:
id: stringname: stringisEnabled: booleanisSupported: boolean
Result:
{
"mainOutput": { "outputDeviceId": "...", "outputId": "..." },
"outputDevices": [ /* OutputDevice[] */ ]
}OutputDevice:
id: stringname: stringisWaveDevice?: booleanoutputs: Output[]
Output:
id: stringname: stringisMuted?: booleanlevel: number(normalized 0..1)
Result:
{ "channels": [ /* Channel[] */ ] }Channel (common fields):
id: stringname: stringtype: string(e.g.,"Software")isMuted?: boolean(Overall channel mute)level?: number(Overall channel volume, 0..1)image?: ChannelImageapps: App[]mixes: ChannelMix[]effects?: Effect[](Software effects applied to this channel)
ChannelImage:
imgData?: string(Base64 or path string)isAppIcon?: booleanname?: string
App:
id: stringname: string
ChannelMix:
mixId: stringisMuted: booleanlevel: number(0..1)
Result:
{ "mixes":[ /* Mix[] */ ] }Mix:
id: stringname: stringisMuted: booleanlevel: numberimage?: ChannelImage
{
"id": "<inputDeviceId>",
"inputs":[
{
"id": "<inputId>",
"isMuted": true,
"isGainLockOn": false,
"gain": { "value": 0.42 },
"micPcMix": { "value": 0.75 },
"effects": [ { "id": "eq", "isEnabled": true } ],
"dspEffects": [ { "id": "clipguard", "isEnabled": true } ]
}
]
}Two shapes are used:
(A) Update outputs on a device
{
"outputDevice": {
"id": "<outputDeviceId>",
"outputs":[
{ "id": "<outputId>", "isMuted": false, "level": 0.5 }
]
}
}(B) Set main output
{
"mainOutput": {
"outputDeviceId": "<outputDeviceId>",
"outputId": "<outputId>"
}
}The protocol supports partial updates. You can update mixes, the overall channel level, or applied software effects:
{
"id": "<channelId>",
"isMuted": false,
"level": 0.8,
"mixes":[
{ "mixId": "<mixId>", "isMuted": false, "level": 0.9 }
],
"effects":[
{ "id": "<effectId>", "isEnabled": true }
]
}Partial update example:
{ "id": "<mixId>", "isMuted": false, "level": 0.85 }{ "appId": "<appId>", "channelId": "<channelId>" }Enable/disable notifications:
{
"focusedAppChanged": { "isEnabled": true },
"levelMeterChanged": { "isEnabled": true, "type": "channel", "id": "all" }
}levelMeterChanged subscriptions are filterable by:
type:"input" | "output" | "channel" | "mix" | ""id: specific id or"all"subId: specific subId (often used to narrow down mixes)
Notifications observed in the official plugin:
| method | params |
|---|---|
createProfileRequested |
{ deviceType: string, mixes: string[] } |
inputDevicesChanged |
{ inputDevices: InputDevice[] } |
inputDeviceChanged |
{ id: string, inputs: Input[] } (partial updates, merges isGainLockOn, dspEffects, etc.) |
outputDevicesChanged |
{ mainOutput: MainOutput, outputDevices: OutputDevice[] } |
outputDeviceChanged |
{ id: string, outputs: Output[] } (partial updates) |
channelsChanged |
{ channels: Channel[] } |
channelChanged |
{ id: string, isMuted, level, image, effects, mixes, ... } (partial updates) |
mixesChanged |
{ mixes: Mix[] } |
mixChanged |
{ id: string, ... } |
levelMeterChanged |
{ inputDevices: Meter[], outputDevices: Meter[], channels: Meter[], mixes: Meter[] } |
focusedAppChanged |
{ id: string, name: string, channel: { id: string } } |
Meter entry:
id: stringlevelLeftPercentage: numberlevelRightPercentage: number
- Source of truth: This repo prefers behavior verified from the official Stream Deck plugin.
- Wave Link server may include additional fields not covered here; clients should ignore unknown fields.