kvm streamdeck
The kvm streamdeck command binds an Elgato Stream Deck button to an action, turning the deck into a server-side control surface. The deck plugs into a remote station's (RX seat's) USB; pressing a key fires a server action directly, with no host PC involved. Button bindings use the same action types as macros: builtin, command, script, and sendkeys (see kvm macro and, for sendkeys, kvm sendkeys).
Bound keys are also painted with a tile (an icon, a text label, and colors) rendered on the deck's LCD keys when the binding is created (and on every deck mount). Built-in actions get a sensible default icon and label; the optional icon= / label= / color= / bg= / labelcolor= arguments override them.
On a Stream Deck + family deck, the rotary encoders and the touch strip above them are bound separately. See kvm streamdeck dial.
| Command Type | DisplayNet |
| Minimum Version | 5.0 |
Every subcommand addresses the deck by its RX seat (<remote_id>), the receiver where the Stream Deck is plugged in. A seat hosts a single control surface, so the seat is the identifier; the deck's USB serial number is an internal detail that never appears in the API. Button maps therefore belong to a seat, and whatever deck is plugged into that seat drives its buttons.
Key-image rendering needs the receiver's AVP firmware at 2.4.0.110 or newer. On older firmware, buttons still bind and fire, but the keys stay blank.
Scope
Stream Deck bindings live inside seat configurations alongside keyboard macros, and a seat resolves to exactly one configuration, most-specific wins:
rx (a specific remote seat) > global (all seats).
A bind lands in the configuration that seat resolves, so a seat's buttons and its keyboard macros always end up in the same place, and a new binding takes effect immediately. On a seat with nothing assigned to its rx scope that is the Global configuration; on a seat with its own configuration it is that one. unbind searches the same place.
Resolution is whole-configuration replacement, not a merge. See kvm config for the full model.
To author into a configuration a seat does not currently resolve, whether building one before assigning it or editing one that is inactive, add config=<name> to the command.
Binding does not require the seat to be present. bind, unbind, and the dial binding verbs store configuration, so they accept a receiver that is powered off or not yet racked. Name it by its device id while it is absent (see Configuring a seat before it exists). The subcommands that talk to the deck, namely reset, repaint and status, do require it online, and say so distinctly when it is not.
Usage
kvm streamdeck bind <remote_id> <button> [<builtin|command|script|sendkeys> <action...>] [icon=<name>] [label=<text>|-] [color=<rrggbb>] [bg=<rrggbb>] [labelcolor=<rrggbb>] [notify[=<name>]] [model=<key>] [config=<name>]
kvm streamdeck unbind <remote_id> <button> [model=<key>] [config=<name>]
kvm streamdeck list [<remote_id>]
kvm streamdeck status [<remote_id>]
kvm streamdeck brightness <remote_id> <0-100>|default
kvm streamdeck reset <remote_id>
kvm streamdeck repaint <remote_id>
kvm streamdeck icons <search_term>
kvm streamdeck infobar <remote_id> on|off|default
The dial subcommand has its own page. See kvm streamdeck dial.
Subcommands
| Subcommand | Description |
|---|---|
bind | Binds a button on the given seat to an action, and paints its tile. See bind. |
unbind | Removes the binding for a given seat and button. Removes the any-deck binding unless model= names a per-model layout. |
list | Returns the bound buttons recorded in the database (not the decks currently attached). With a <remote_id> argument, filters to that seat. |
status | Reports the deck currently attached at each seat: model, key count and resolution, and which features it has. The mirror of list: list reads configuration, status reads hardware. See status. |
dial | Binds the rotary encoders on a Stream Deck + family deck. Documented separately. See kvm streamdeck dial. |
brightness | Sets the seat's display brightness, 0 to 100. A stored per-seat setting: it is applied to a mounted deck immediately, re-applied on every mount, and accepts a seat that is not present. Pass default to remove the stored value. |
reset | Resets the deck mounted at the given seat to its default state. Requires the deck to be currently connected. |
repaint | Forces a full reset-and-repaint of the deck at the given seat, covering every bound key and the info-bar strip. Use it as insurance after a firmware upgrade or if a key image was dropped in transit. Requires the deck to be currently connected. |
icons | Searches the icon library (built-in icons plus the bundled MDI set) by name and returns matching icon names to use with icon=. Does not require a deck. |
infobar | Turns the DisplayNet branding on the deck's info-bar strip on or off, or resets it to the default. See infobar. |
bind
Binds a button on the given seat (<remote_id>) to an action, and paints its tile. Lands in the configuration that seat resolves, unless config=<name> names another. The trailing tile arguments are optional and may appear in any order after the action:
| Tile argument | Description |
|---|---|
icon=<name> | An icon from the built-in set or the bundled MDI library. Use the icons subcommand to search names. An unknown name is rejected. |
label=<text> | The text row under the icon. Use label=- for an icon-only tile (no text). When omitted, a built-in action uses its default label. |
color=<rrggbb> | The icon color, as a 6-digit hex value (a leading # is allowed). |
bg=<rrggbb> | The tile background color, replacing the default brand gradient ground. |
labelcolor=<rrggbb> | The label color, independent of color=. Without it the label uses the palette neutral, so tinting an icon does not drag its text along. |
Only these tile keys, the notify modifier (below), and config= are consumed as trailing modifiers, so a command payload that contains its own key=value arguments is safe.
The button can also be written into a specific configuration:
| Modifier | Description |
|---|---|
config=<name> | Bind into <name> instead of the configuration the seat resolves. Use it to build a configuration before assigning it, or to edit one that is currently inactive. The configuration must already exist. On unbind, it names the configuration to remove from. Not valid on kvm config button add, where the configuration is already the first argument. |
The button can also announce its press to API clients:
| Modifier | Description |
|---|---|
notify | On press, broadcast a streamdeck_notify event (see Notifications) with an empty name. |
notify=<name> | Same, with <name> as a semantic label an API client keys on. |
When notify is present the action is optional: a button bound with only notify just announces (it takes no DisplayNet action); combined with an action it does both on the same press. This also works on kvm config button add.
Per-model layouts
A seat and a deck are not the same thing. The seat is where the buttons are configured; the deck is whatever hardware happens to be plugged in there today, and a layout drawn for a 15-key MK.2 makes no sense on an 8-key Neo. model=<key> lets one seat hold both, and the attached hardware picks:
kvm streamdeck bind Editor1 0 builtin focus_next # any deck
kvm streamdeck bind Editor1 12 command preset apply Wide model=mk2 # only on an MK.2
Swap the deck and it repaints with the right layout on its own, with no reconfiguration and no operator action.
| Modifier | Description |
|---|---|
model=<key> | Makes this binding specific to one deck model. Omit it and the binding applies to any deck, which is what every binding does by default. Valid keys are listed below; status reports the attached deck's key as model_key. |
Precedence is per button, not per layout. A model layout overrides the buttons it names and inherits the seat's any-deck bindings for every button it does not. So a seat with a full generic layout plus a single model=neo override on button 0 gets the generic layout on a Neo, with button 0 replaced. Whole-layout replacement is what configurations do; the model dimension refines within one.
If a seat has only model-specific bindings, a deck of some other model gets nothing, which is the point. A 15-key layout no longer half-applies to an 8-key deck.
| Key | Deck | Grid | Keys |
|---|---|---|---|
neo | Stream Deck Neo | 4 × 2 | 8 |
mk2 | Stream Deck MK.2 | 5 × 3 | 15 |
scissor | Stream Deck MK.2 (Scissor Keys) | 5 × 3 | 15 |
originalv2 | Stream Deck Original V2 | 5 × 3 | 15 |
original | Stream Deck Original | 5 × 3 | 15 |
xl | Stream Deck XL | 8 × 4 | 32 |
xlv2 | Stream Deck XL V2 | 8 × 4 | 32 |
plus | Stream Deck Plus | 4 × 2 | 8 |
plusxl | Stream Deck + XL | 9 × 4 | 36 |
mini | Stream Deck Mini | 3 × 2 | 6 |
mini2022 | Stream Deck Mini (2022) | 3 × 2 | 6 |
Grids are stated columns × rows. Two models with the same grid still take different keys, so bind generically to cover both.
Because the model is part of a binding's identity rather than a property of it:
- Re-binding button 0 with
model=neodoes not overwrite button 0's any-deck binding. That is what lets a seat hold both. unbind <seat> 0removes the any-deck binding and leaves the model layouts alone. Name one withmodel=to remove it. When nothing matches, the error lists which models are bound on that button.bind ... 12 model=neois rejected, because a Neo has 8 keys. A binding with no model cannot be range-checked this way, because the deck that will eventually run it is unknown at authoring time.
model= works on kvm config button add / remove too, so a configuration can be built with per-model layouts before it is assigned to anything.
status
Reports the control surface currently attached at each seat, from the device descriptor rather than from configuration. With no argument it lists every seat with a deck mounted; with a <remote_id> it reports that seat, including when no deck is present.
It reports the deck's grid as key_cols, key_rows and key_pixels, so a client can draw the deck's real shape without carrying its own table of Elgato models. It also reports model_key, the short key bind model= takes, so a deck discovered here can be named in a binding without guesswork, and the seat's brightness with brightness_is_set saying whether that is a stored override or the default.
Decks whose USB product id is not in the model registry are still reported, with model as "unknown" and the raw product_id, so an unrecognised deck is visible rather than silently absent. An unrecognised deck has no model_key and runs the seat's any-deck layout.
infobar
Turns the DisplayNet branding on the deck's info-bar strip (Elgato Neo) on or off. The setting is per-seat and persists across mounts; it applies to whatever deck is attached. on paints the DisplayNet logo, off paints a blank strip. Takes effect immediately when a strip-capable deck is mounted.
Pass default to remove the stored value so the seat falls back to the shipped default (branding on). This is one of the five per-seat settings, and kvm workstation show reports it with a HasOverride flag.
This is the one subcommand that both stores a setting and paints, so it accepts a seat that is not present: the setting is recorded and applies when the seat turns up. The response's applied field says which happened: true when a deck was repainted, false when the setting was stored for later.
testimage, setfeature, teststrip, and teststrippartial are internal hardware-diagnostic verbs used during control-surface bring-up. They are not part of the supported API surface and may change or disappear without notice.
Arguments
| Argument | Description |
|---|---|
remote_id | The device name or ID of the remote HID extender, the RX seat where the Stream Deck is plugged in. Every kvm streamdeck subcommand except icons uses this; the deck's USB serial is never needed. Matched case-insensitively. |
button | A 0-based key index on a Stream Deck. Key 0 is the top-left key; indices increase left-to-right, top-to-bottom across the device's keys. |
model | A short deck-model key on bind / unbind. See Per-model layouts for the list. Matched case-insensitively. Omitting it means the binding applies to any deck. |
icon / label / color / bg | Optional trailing tile arguments on bind (see bind). icon= is an icon name (validate with icons); label= is the text row, or - for icon-only; color= and bg= are 6-digit rrggbb hex values (a leading # is allowed). |
search_term | A substring matched (case-insensitively) against icon names for icons. Up to 25 matches are returned. |
Examples
Bind buttons to built-in, command, and sendkeys actions with tile stylingkvm streamdeck bind 6cdffb00f077 0 builtin focus_next
kvm streamdeck bind 6cdffb00f077 1 builtin fullscreen_toggle icon=fullscreen color=33cccc
kvm streamdeck bind 6cdffb00f077 2 command preset apply NightShift label=Night bg=203040
kvm streamdeck bind 6cdffb00f077 3 sendkeys "gui+r, type:notepad, enter" icon=console label=Run
Announce a button press to API clients (notify)kvm streamdeck bind 6cdffb00f077 4 notify=PlayCam1
kvm streamdeck bind 6cdffb00f077 5 command connect HDMI Cam1 Wall notify=Cam1Live
One seat, two deck models — the attached hardware pickskvm streamdeck bind Editor1 0 builtin focus_next
kvm streamdeck bind Editor1 1 builtin fullscreen_toggle
kvm streamdeck bind Editor1 12 command preset apply Wide label=Wide model=mk2
kvm streamdeck unbind Editor1 12 model=mk2
Search the icon librarykvm streamdeck icons monitor
List a seat's bound buttonskvm streamdeck list 6cdffb00f077
Set the deck's brightnesskvm streamdeck brightness 6cdffb00f077 60
Turn on the info-bar brandingkvm streamdeck infobar 6cdffb00f077 on
Report the deck attached at a seatkvm streamdeck status 6cdffb00f077
Return value
kvm streamdeck list
Each entry is one bound button. RemoteId carries the seat id the binding belongs to, the same value as the <remote_id> argument. ActionType may be builtin, command, script, sendkeys, or none for a button bound with only the notify modifier, which announces its press and takes no action. Model, Icon, Label, IconColor, Background, and NotifyName appear only when set on the binding. An absent Model means the binding applies to any deck (see Per-model layouts). For a sendkeys binding, any type: text in ActionPayload is masked (type:••••) so typed strings never appear in output. Reports bound buttons from the database, not decks currently attached.
kvm streamdeck list{
"status": "SUCCESS",
"request_id": null,
"result": {
"streamdeck_list": [
{
"RemoteId": "6CDFFB00F077",
"Page": "default",
"Button": 1,
"ActionType": "builtin",
"ActionPayload": "fullscreen_toggle",
"Enabled": true,
"Icon": "fullscreen",
"IconColor": "33cccc"
},
{
"RemoteId": "6CDFFB00F077",
"Page": "default",
"Model": "mk2",
"Button": 12,
"ActionType": "command",
"ActionPayload": "preset apply Wide",
"Enabled": true,
"Label": "Wide"
}
]
},
"error": null
}
kvm streamdeck status
One entry per seat with a deck attached (or the single requested seat). model is "unknown" for a deck whose product_id is not in the model registry, and note carries any capability caveat. key_cols × key_rows is the deck's grid (columns first), and model_key is the short key bind model= takes. Both are absent for an unrecognised deck.
kvm streamdeck status 6cdffb01f5ba{
"status": "SUCCESS",
"request_id": null,
"result": {
"streamdeck_status": [
{
"remote_id": "6cdffb01f5ba",
"remote_name": "RokuTV",
"mounted": true,
"brightness": 75,
"brightness_is_set": true,
"product_id": "0x00C6",
"serial": "AD4MA61311ZB6G",
"model": "Stream Deck + XL",
"model_key": "plusxl",
"keys": 36,
"key_cols": 9,
"key_rows": 4,
"key_pixels": 112,
"info_bar": false,
"touch_strip": true,
"dials": 6,
"key_images": "supported"
}
]
},
"error": null
}
kvm streamdeck icons
An array of matching icon names (built-in names first, then MDI names).
kvm streamdeck icons monitor{
"status": "SUCCESS",
"request_id": null,
"result": {
"streamdeck_icons": ["monitor", "monitor-multiple", "monitor-dashboard"]
},
"error": null
}
Deck control subcommands
brightness, reset, repaint and infobar all return the same envelope.
Each returns a single-object array under its own key, echoing the seat id and the operation result.
kvm streamdeck brightness 6cdffb00f077 60{
"status": "SUCCESS",
"request_id": null,
"result": {
"streamdeck_brightness": [
{ "remote_id": "6CDFFB00F077", "percent": 60, "is_set": true, "applied": false }
]
},
"error": null
}
For brightness, is_set says whether a stored override remains after the call (false after default), and applied whether a mounted deck was repainted (false when the setting was stored for an absent seat, mirroring infobar).
| Subcommand | Result key | Fields |
|---|---|---|
brightness | streamdeck_brightness | remote_id, percent, is_set, applied |
reset | streamdeck_reset | remote_id, pushed |
repaint | streamdeck_repaint | remote_id, repainted |
infobar | streamdeck_infobar | remote_id, enabled, applied |
Binding subcommands
bind and unbind both return the same envelope.
kvm streamdeck bind 6cdffb00f077 0 builtin focus_next{
"status": "SUCCESS",
"request_id": null,
"result": null,
"error": null
}
Errors
Error response{
"status": "ERROR",
"request_id": null,
"result": null,
"error": {
"message": "<description>",
"reason": "KVM API ERROR"
}
}
Common error conditions:
- Unknown Stream Deck action type, or an invalid (negative or non-numeric) button index (
bind) - An invalid keystroke sequence for a
sendkeysaction (bind, seekvm sendkeys) - Unknown
icon=name, an empty tile-argument value, or an invalidcolor=/bg=hex value (bind) - Unknown
model=key. The error lists every valid key (bind,unbind) - A button index the named model does not have, e.g. button
12withmodel=neo(bind) - No action and no
notifymodifier. A binding must have at least one (bind) - No binding found for the given seat, button, and model. The error names which models are bound on that button (
unbind) - No mounted control surface at the given seat (
reset,repaint) - Empty search term (
icons) - Unknown mode. Expected
on,off, ordefault(infobar)
Notifications
streamdeck_update
Sent after every kvm streamdeck bind or unbind, so a button editor can refresh. It carries a compact entry per binding: the seat id (RemoteId), the Button index, the Page, and Model when the binding is model-specific. Clients re-query list for full binding detail.
Model is part of the entry because it is part of the binding's identity: without it a client watching one seat could not tell a change to a Neo layout from a change to the any-deck binding on the same button.
streamdeck_update notification{
"status": "DN_NOTIFICATION",
"request_id": null,
"error": null,
"result": {
"streamdeck_update": [
{ "RemoteId": "6CDFFB00F077", "Button": 0, "Page": "default" },
{ "RemoteId": "6CDFFB00F077", "Button": 12, "Page": "default", "Model": "mk2" }
]
}
}
streamdeck_notify
Sent on each press of a button bound with the notify modifier (see bind). It lets an external controller, a Q-SYS plugin for example, react to a Stream Deck button over the normal notification channel. A button bound with both an action and notify emits this event and runs its action on the same press.
RemoteId is the seat id, Button the 0-based key index, and Name the label from notify=<name> (empty for a bare notify).
streamdeck_notify — button press{
"status": "DN_NOTIFICATION",
"request_id": null,
"error": null,
"result": {
"streamdeck_notify": [
{ "RemoteId": "6CDFFB00F077", "Button": 4, "Page": "default", "Name": "Cam1Live" }
]
}
}
streamdeck_notifyA dial in notify mode broadcasts streamdeck_notify too, carrying Dial and Ticks or Pressed instead of Button and Page. A client cannot switch on the event name alone and must discriminate on which fields are present. See dial notifications for all three shapes.
REST API
| Endpoint type | DisplayNet API command |
|---|---|
| Address | POST /api/displaynet/<operation>, or name the operation in the body of a POST /api/displaynet. See Sending commands. |
| Role | User |
| Subcommand | Operation | Parameters |
|---|---|---|
streamdeck | kvm_streamdeck | action [args]* |
streamdeck_bind | kvm_streamdeck_bind | remote_id button [action_type] [payload] [icon] [label] [color] [bg] [labelcolor] [notify] [model] [config] |
streamdeck_brightness | kvm_streamdeck_brightness | remote_id value |
streamdeck_infobar | kvm_streamdeck_infobar | remote_id mode |
streamdeck_list | kvm_streamdeck_list | [remote_id] |
streamdeck_status | kvm_streamdeck_status | remote_id |
streamdeck_unbind | kvm_streamdeck_unbind | remote_id button [model] [config] |
A parameter in brackets is optional. A parameter marked * takes the remainder of the command line as one string. Pass the text exactly as you would type it on the TCI interface.
Every action except reset, repaint, setfeature, teststrip, testimage and icons has a typed operation taking named fields. The tile modifiers (model, icon, label, notify, config) are separate optional fields rather than key=value tokens, and a missing or unknown field is rejected before the command runs. Dial operations are listed on the kvm streamdeck dial page.
Those six run through kvm_streamdeck, which takes a raw grammar line: action and args, spelled as under Usage above.
See also
kvm: core KVM session commands, includingkvm sendkeyskvm macro: keyboard hotkey macros using the same action typeskvm streamdeck dial: the rotary encoders and touch strip on a + family deckkvm config: the seat-configuration scope model bindings resolve through- Workstations: what a workstation is and how its pieces fit together