Skip to main content

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 TypeDisplayNet
Minimum Version5.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 images require recent RX firmware

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

SubcommandDescription
bindBinds a button on the given seat to an action, and paints its tile. See bind.
unbindRemoves the binding for a given seat and button. Removes the any-deck binding unless model= names a per-model layout.
listReturns the bound buttons recorded in the database (not the decks currently attached). With a <remote_id> argument, filters to that seat.
statusReports 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.
dialBinds the rotary encoders on a Stream Deck + family deck. Documented separately. See kvm streamdeck dial.
brightnessSets 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.
resetResets the deck mounted at the given seat to its default state. Requires the deck to be currently connected.
repaintForces 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.
iconsSearches 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.
infobarTurns 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 argumentDescription
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:

ModifierDescription
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:

ModifierDescription
notifyOn 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.

ModifierDescription
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.

KeyDeckGridKeys
neoStream Deck Neo4 × 28
mk2Stream Deck MK.25 × 315
scissorStream Deck MK.2 (Scissor Keys)5 × 315
originalv2Stream Deck Original V25 × 315
originalStream Deck Original5 × 315
xlStream Deck XL8 × 432
xlv2Stream Deck XL V28 × 432
plusStream Deck Plus4 × 28
plusxlStream Deck + XL9 × 436
miniStream Deck Mini3 × 26
mini2022Stream Deck Mini (2022)3 × 26

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=neo does not overwrite button 0's any-deck binding. That is what lets a seat hold both.
  • unbind <seat> 0 removes the any-deck binding and leaves the model layouts alone. Name one with model= to remove it. When nothing matches, the error lists which models are bound on that button.
  • bind ... 12 model=neo is 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.

Diagnostic subcommands

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

ArgumentDescription
remote_idThe 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.
buttonA 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.
modelA 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 / bgOptional 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_termA 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 styling
kvm 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 picks
kvm 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 library
kvm streamdeck icons monitor
List a seat's bound buttons
kvm streamdeck list 6cdffb00f077
Set the deck's brightness
kvm streamdeck brightness 6cdffb00f077 60
Turn on the info-bar branding
kvm streamdeck infobar 6cdffb00f077 on
Report the deck attached at a seat
kvm 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).

SubcommandResult keyFields
brightnessstreamdeck_brightnessremote_id, percent, is_set, applied
resetstreamdeck_resetremote_id, pushed
repaintstreamdeck_repaintremote_id, repainted
infobarstreamdeck_infobarremote_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 sendkeys action (bind, see kvm sendkeys)
  • Unknown icon= name, an empty tile-argument value, or an invalid color=/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 12 with model=neo (bind)
  • No action and no notify modifier. 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, or default (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" }
]
}
}
Dial payloads on streamdeck_notify

A 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 typeDisplayNet API command
AddressPOST /api/displaynet/<operation>, or name the operation in the body of a POST /api/displaynet. See Sending commands.
RoleUser
SubcommandOperationParameters
streamdeckkvm_streamdeckaction [args]*
streamdeck_bindkvm_streamdeck_bindremote_id button [action_type] [payload] [icon] [label] [color] [bg] [labelcolor] [notify] [model] [config]
streamdeck_brightnesskvm_streamdeck_brightnessremote_id value
streamdeck_infobarkvm_streamdeck_infobarremote_id mode
streamdeck_listkvm_streamdeck_list[remote_id]
streamdeck_statuskvm_streamdeck_statusremote_id
streamdeck_unbindkvm_streamdeck_unbindremote_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, including kvm sendkeys
  • kvm macro: keyboard hotkey macros using the same action types
  • kvm streamdeck dial: the rotary encoders and touch strip on a + family deck
  • kvm config: the seat-configuration scope model bindings resolve through
  • Workstations: what a workstation is and how its pieces fit together