API¶
DroidDeck's server exposes a REST API and a SignalR hub on port 4787. The phone app and the
configurator use them, and you can use them to build your own clients or automations.
Authentication¶
Every endpoint requires the API key, except GET /api/ping and GET /api/pairing/local-key.
Send the key in one of these ways:
- header
X-API-KEY: <key> - header
Authorization: Bearer <key> - query string
?access_token=<key>(used for the SignalR WebSocket)
GET /api/pairing/local-key returns {key} only to requests from the PC itself (loopback), and
rejects cross-site browser requests. CORS only allows origins on loopback and private network ranges.
Integration failures return 502 with {error}.
Privileged actions¶
Keyboard shortcut, open program, activate window and script run code on the PC, and an HTTP request goes out with the PC's network access, so they are protected even from clients that have the key. Requests that don't come from the PC itself (loopback) cannot:
- run them through
execute; usepresswith a button that was saved on the PC; - create a button with one of these actions, or change its parameters, through
POST profiles. Moving, renaming, recolouring and deleting such buttons is allowed.
A multi-action counts as privileged when any of its steps is. Refused requests return 403 with
{error}.
Connections from a phone over the USB cable arrive on loopback
through adb; the server recognises them and treats them as remote, not as the PC.
Examples¶
# Is the server up?
curl http://192.168.0.10:4787/api/ping
# Toggle Discord mute
curl -X POST http://192.168.0.10:4787/api/StreamDeck/execute \
-H "X-API-KEY: $KEY" -H "Content-Type: application/json" \
-d '{"type":"discord","parameters":{"operation":"toggleMute"}}'
# Mute Spotify
curl -X POST http://192.168.0.10:4787/api/StreamDeck/execute \
-H "X-API-KEY: $KEY" -H "Content-Type: application/json" \
-d '{"type":"mixer","parameters":{"operation":"mute","processName":"Spotify"}}'
Deck: /api/StreamDeck¶
| Method | Route | Description |
|---|---|---|
| GET | profiles |
List profiles. |
| GET | profiles/{id} |
Get a profile. |
| POST | profiles |
Create or update a profile. Broadcasts ReceiveDeckUpdate. |
| DELETE | profiles/{id} |
Delete a profile. Broadcasts ReceiveDeckUpdate. |
| POST | press |
Press a saved button ({profileId, buttonId, gesture}; gesture is tap (default), longPress or doubleTap): the server runs the action stored in the profile for that gesture. This is what the phone uses. |
| POST | execute |
Run an ad-hoc action ({type, parameters}). From outside the PC, privileged actions return 403. |
| GET / POST | layout |
Read / set the grid size ({rows, columns}). |
Action payload¶
{ "type": "hotkey", "parameters": { "keys": "^+m" } }
All parameter values are strings. The table below is a summary; the machine-readable source of
truth is GET /api/actions/schema, which lists every action type with
its parameters (fields[].key), allowed values (options) and when each field applies
(visibleWhen).
type |
Parameters |
|---|---|
hotkey |
keys |
launch_app |
path, arguments? |
activatewindow |
windowName (exact window title) |
media |
command: playpause·play·pause·next·previous·stop; sessionId? |
mixer |
operation: toggleMute·mute·unmute·setVolume·volumeUp·volumeDown; processName or deviceId (default or an endpoint id); deviceKind: output·input; volume (0–100); step (1–50) |
multi |
steps: JSON array string [{"type","parameters","delayMs","if"}]; if is optional: {check, value, negate} with check one of obs_streaming·obs_recording·discord_mute·discord_deaf·toggle_on (value = button id)·app_running·app_focused (value = process)·time_between (value = 08:00-18:00)·weekday (value = 1-5, 0 = Sunday) |
discord |
operation: toggleMute·mute·unmute·toggleDeafen·joinChannel (channelId)·disconnect·inputVolumeUp/Down·outputVolumeUp/Down (delta)·setInputVolume·setOutputVolume (value)·toggleVoiceMode·setVoiceMode (mode: VOICE_ACTIVITY·PUSH_TO_TALK)·userMute (userId)·userVolume (userId, value 0–200) |
obs |
operation: setScene (scene)·toggleRecord·startRecord·stopRecord·toggleStream·toggleVirtualCam·toggleReplayBuffer·saveReplay·toggleInputMute (inputName) |
soundboard |
operation: play·stop; source: myinstants (id, url, title) or discord (soundId, guildId) |
tuya |
deviceId, code, operation: toggle·set; value, valueType |
homeassistant |
operation: toggle·turn_on·turn_off·brightness (step)·scene·script·display·service (service = domain.service, data JSON); entityId |
spotify |
operation: playPause·next·previous·like·shuffle·repeat·volume (step)·playlist (uri, shuffleOn)·nowPlaying |
twitch |
operation: marker (description)·clip·ad (length)·chat (message)·poll (question, choices one per line, duration)·title (title)·status |
youtube |
operation: chat (message)·status |
streamerbot |
action (id or name), args (key=value per line, or JSON) |
vtubestudio |
operation: hotkey (hotkey: id or name)·model (model: model id) |
voicemeeter |
operation: toggle_mute·mute·unmute·set_gain (gain dB)·adjust_gain (step dB) with target: strip·bus and index (1–8)·macro (button)·param_set (parameter, value)·param_toggle (parameter) |
hue |
operation: toggle·on·off·brightness (brightness %)·brightness_step (step %) with target: light·group and id·scene (scene) |
keylight |
host (IP or IP:port); operation: toggle·on·off·brightness (brightness)·brightness_step (step)·temperature (kelvin)·temperature_step (kelvinStep) |
govee |
ip; operation: toggle·on·off·brightness (brightness)·color (color #RRGGBB)·temperature (kelvin) |
mqtt |
topic, payload, qos (0–2), retain (true/false) |
script |
source: inline (language: powershell·python·cmd; code) or file (path); arguments |
http |
method: POST·GET·PUT·PATCH·DELETE; url; headers (one Name: value per line); for POST, PUT and PATCH, contentType (application/json·application/x-www-form-urlencoded·text/plain) and body |
timer |
mode: countdown (durationMin)·stopwatch·pomodoro (workMin, breakMin)·counter (step); operation: toggle·reset·skip·increment·decrement; target: self or the id of another timer button |
windows |
operation: setDefaultAudio (deviceKind: output·input; deviceId; altDeviceId to toggle between two, or none)·lock·sleep·displayOff |
steam |
appId: the numeric Steam app id (from GET /api/steam/games) |
open_profile and back exist only on the phone.
Audio: /api/v1/Mixer, /api/v1/Media¶
| Method | Route | Description |
|---|---|---|
| GET | Mixer/out, Mixer/in |
Output / input devices with their app sessions. |
| GET | Mixer/out/{id}, Mixer/in/{id} |
One device. |
| PUT | Mixer/out/{id}, Mixer/in/{id} |
{session, volume?, mute?}: session is a process id, or -1 for the whole device. |
| GET | Media/sessions |
Media sessions, with thumbnails. |
| GET | Media/sessions/{id} |
One session. |
| POST | Media/sessions/{id}/command |
{command}. |
A profile is {id, name, rows, columns, isDefault, buttons, autoSwitchApps}; a button has action,
longPressAction and doubleTapAction (each {type, parameters} or null). The privileged-action
rule applies to all three.
Two-state buttons add toggle (bool), activeAction (run on the tap that turns it off),
activeLabel, activeIconName and activeIconBase64. Info buttons use dynamicType (cpu_monitor,
memory_monitor, gpu_monitor, network_monitor, clock, weather, now_playing, command,
url, notifications, volume_slider, ai_usage) with dynamicParameters (city, units, command, url,
path, format, interval, app, target…); command and url follow the privileged-action rule.
Large buttons add columnSpan and rowSpan (default 1; row/column is the top-left cell).
| Method | Path | Description |
|---|---|---|
| GET | /api/slider?target=… |
Slider: current {volume, mute, label, max, text}. target: output, input, app (app), obs (inputName), discord_user (userId), monitor, media, tuya (deviceId), homeassistant (entityId), hue (hueTarget, id), govee (ip), keylight (host). max is 100, except Discord (200) and music (duration in seconds). 404 when the target doesn't exist right now; 503 {error} when the integration doesn't respond. |
| PUT | /api/slider |
{target, app, params, volume} (0 to max). Returns the new state. |
| GET | /api/slider/options?target=tuya\|discord_user |
{value, label} list of Smart Life bulbs with brightness or of people in the call. |
Library: backup, Steam, apps¶
| Method | Path | Description |
|---|---|---|
| GET | /api/backup |
Download every profile as a .droiddeck (zip). |
| POST | /api/backup |
Restore a .droiddeck (request body = the file). Same-id profiles are replaced. From outside the PC, refused with 403 if it would create privileged buttons. |
| GET | /api/steam/games |
Installed Steam games ({value: appId, label: name}). |
| GET | /api/steam/icon/{appId} |
{iconBase64}: a square PNG cut from the game's cover. |
| GET | /api/system/apps |
Programs with a visible window (for automatic profile switching). |
| GET | /api/system/info |
{name, version}: the PC's Windows name, used to label paired PCs. |
| GET | /api/info |
Current values of the info buttons, what's playing and the two-state buttons that are on. |
| GET / POST | /api/macro, /api/macro/start, /api/macro/stop |
Keyboard macro recording. Only from the PC itself (403 otherwise); stop returns {steps} in the multi-action format. |
| POST | /api/import/streamdeck |
Imports a .streamDeckProfile (request body = the file, up to 50 MB) as new profiles. Returns {profiles, buttons, unsupported, stripped, firstProfileId}; from outside the PC, privileged buttons come in without their action and stripped counts them. 400 with {error} if it isn't a Stream Deck profile. |
| GET | /api/share/{profileId} |
{data, url, qr, images}: the profile compressed into a link (url is the site's share page with #data); qr says whether it fits in a QR code; images whether custom images were left out. |
| GET | /api/share/{profileId}/qr |
The share link as a PNG QR code. |
| POST | /api/share/import |
{data} from a share link → saved as a new profile. Returns {profileId, name, buttons, stripped}; same privileged-action rule as the Stream Deck import. |
Automations: /api/triggers¶
| Method | Path | Description |
|---|---|---|
| GET | /api/triggers |
The list of automations. |
| PUT | /api/triggers |
Replaces the whole list. Returns the saved list. |
| POST | /api/triggers/{id}/run |
Presses the automation's button now. 404 if the automation or the button doesn't exist. |
An automation is {id, name, enabled, event, time, days, app, profileId, buttonId, if}. event is
schedule (time HH:mm, days like 1-5), app_start / app_exit (app), resume or
startup; if is an optional condition in the multi-action format. Since automations only press saved
buttons, they follow the rules those buttons were created under.
Action types: /api/actions¶
| Method | Path | Description |
|---|---|---|
| GET | schema |
Every action type with its title, icon, category and fields. The editor builds its form from this. |
| GET | state/{type} |
Current live state of a type (the same payload as its SignalR event). |
| GET | timer/targets |
Timer buttons another button can control. |
| GET | windows/audio-devices?kind=output\|input |
Active audio devices ({value, label}). |
The schema is the source of truth for action parameters: the app's editor builds its forms from it,
so a new action type or field shows up here first. Texts follow Accept-Language. An abridged
response:
[
{
"type": "steam",
"title": "Steam game",
"icon": "sports_esports",
"category": "System",
"privileged": false,
"stateful": false,
"editor": "steam",
"stateEvent": null,
"fields": [
{
"key": "appId",
"label": "Game",
"kind": "select",
"required": true,
"optionsSource": "/api/steam/games",
"visibleWhen": null
}
]
}
]
Integrations¶
| Prefix | Endpoints |
|---|---|
/api/discord |
POST config {clientId, clientSecret} · POST connect · GET state · GET guilds · GET channels/{guildId} · GET voice-channel · POST mute · POST deafen · GET soundboard-sounds · POST play-soundboard |
/api/obs |
POST config {host, port, password} · POST connect · GET state · GET scenes · GET audio-inputs · GET sources · GET scene-items?scene= · GET filters?source= |
/api/Soundboard |
GET search?q= · GET trending · POST play · POST stop · GET state · GET devices · GET/POST config |
/api/tuya |
POST pair/start {userCode} · POST pair/poll (410 when the QR expired) · GET state · POST connect · POST devices/refresh · POST command {deviceId, code, value} |
/api/govee |
GET devices: searches the network for Govee devices with LAN Control (about 2 s); list of {value: ip, label} |
/api/integrations/{name} |
homeassistant, spotify, twitch, youtube, hue, mqtt, streamerbot, vtubestudio. GET status {connected, configured, account, message, loginPending, userCode, verificationUri, redirectUri, fields} · POST the setup fields (connects or starts sign-in in the PC's browser) · DELETE forget the account · GET options/{list} (entities?operation=, playlists, Hue items?target=·scenes, Streamer.bot actions, VTube Studio hotkeys·models) · GET callback (OAuth return, no API key) |
Responses follow the request's Accept-Language: with en, action types, field labels and error
messages come in English.
SignalR hub: /deckHub¶
Connect with ?access_token=<key>. The hub only pushes events; commands go through REST.
| Event | Payload |
|---|---|
ReceiveSystemStats |
{cpuUsage, ramUsage, ramTotal, ramAvailable, netUpKBps, netDownKBps, gpuUsage}: every second |
ReceiveMediaStatus |
{playing} |
ReceiveMediaState |
{sessionId, command, playing} |
ReceiveVolumeChange |
{deviceId, type, data} |
ReceiveMuteState |
{processName, muted} |
ReceiveDeckUpdate |
{profileId} |
ReceiveLayoutUpdate |
{rows, columns} |
ReceiveDiscordState |
Connection, mute/deaf, channel, volumes, voice mode, participants |
ReceiveObsState |
Connection, current scene, recording/streaming/virtual cam/replay, scenes, audio inputs |
ReceiveTuyaState |
Connection, push, devices with status and functions |
ReceiveTuyaDeviceState |
{deviceId, status}: changed values only |
ReceiveHueState, ReceiveMqttState, ReceiveStreamerBotState, ReceiveVTubeStudioState |
{connected} (VTube Studio also sends model) |
ReceiveTimerState |
{buttonId: {mode, running, finished, elapsedMs, durationMs, count, phase, cycles}}: on every change |
ReceiveWindowsState |
{defaultOutput, defaultInput}: default audio device ids |
ReceiveAutoProfile |
{profileId, app}: profile of the focused app; profileId null when the app has none |
ReceivePlaybackState |
{playing, title}: soundboard |
ReceiveButtonToggles |
{buttonId: true}: two-state buttons that are on |
ReceiveInfoState |
{buttonId: {text, sub, code, isDay, error}}: weather, command and URL buttons |
ReceiveNowPlaying |
{title, artist, playing, thumbnailBase64} |
ReceiveHomeAssistantState |
{connected, entities: {id: {state, name, unit, brightness, color}}}: entities used by buttons |
ReceiveSpotifyState |
{connected, account, playing, track, artist, liked, shuffle, repeat, volume, device, coverBase64} |
ReceiveTwitchState, ReceiveYouTubeState |
{connected, account, live, viewers, title} |
On connect, the new client receives the current Discord, layout, OBS and Tuya state.
Discovery: UDP 7573¶
Send the ASCII text DroidDeckDiscoveryRequest as a broadcast to port 7573. The server answers with
{"ip": "...", "name": "<machine name>"}. Discovery never returns the API key.
Pairing URI¶
droiddeck://pair?ip=<ip>&port=4787&key=<key>