Unofficial Software Development Kit for your Stream Deck, built in C# for the .NET platform.
Warning
This SDK is under active development and is currently in its alpha stage. That means that there may be breaking changes between releases until it hits 1.0.0.
Note
For the DeckSurf tooling (CLI and plugins), refer to the DeckSurf repository.
dotnet add package DeckSurf.SDKusing DeckSurf.SDK.Core;
using DeckSurf.SDK.Models;
// Enumerate connected Stream Deck devices
var devices = DeviceManager.GetDeviceList();
if (devices.Count == 0)
{
Console.WriteLine("No Stream Deck devices found.");
return;
}
// Use the first device
using var device = devices[0];
// Listen for button presses (filter to Down to avoid double-firing on release)
device.ButtonPressed += (sender, e) =>
{
if (e.EventKind != ButtonEventKind.Down) return;
Console.WriteLine($"Button {e.Id} pressed (type: {e.ButtonKind})");
};
device.StartListening();
// Set a button image (JPEG, PNG, BMP, GIF, or any ImageSharp-supported format)
// Images are automatically resized to match the device's button resolution.
byte[] image = File.ReadAllBytes("icon.png");
device.SetKey(0, image);
// Set brightness (0-100)
device.SetBrightness(80);Tip: Button events fire twice per physical press β once for
Down(pressed) and once forUp(released). Filter onButtonEventKind.Downif you only want to respond once per press.
Buttons are numbered left-to-right, top-to-bottom, starting at 0. Use device.ButtonColumns and device.ButtonRows to understand the grid:
Stream Deck Original (5Γ3):
0 1 2 3 4
5 6 7 8 9
10 11 12 13 14
| Device | Buttons | Layout | Button Resolution |
|---|---|---|---|
| Original / Original 2019 / MK.2 | 15 | 5Γ3 | 72Γ72 px |
| XL / XL 2022 | 32 | 8Γ4 | 96Γ96 px |
| Mini / Mini 2022 | 6 | 3Γ2 | 80Γ80 px |
| Neo | 8 | 4Γ2 | 96Γ96 px |
| Plus | 8 | 4Γ2 | 120Γ120 px |
Images passed to SetKey() are automatically resized to the device's button resolution. For best results, use square images. Non-square images will be stretched.
The SDK uses a structured exception model rooted in DeckSurfException:
| Exception | When | Key Property |
|---|---|---|
DeviceCommunicationException |
USB I/O failure during operation | IsTransient β true if safe to retry |
DeviceDisconnectedException |
Device unplugged mid-operation | DeviceSerial β identifies which device |
DeviceNotFoundException |
Device lookup failed (serial/path not found) | β |
ImageProcessingException |
Unrecognized image format in SetKey or ImageHelper.ResizeImage |
β |
ObjectDisposedException |
Method called on a disposed device | β |
InvalidOperationException |
StartListening() called when already listening |
β |
ArgumentOutOfRangeException |
Button index out of range in SetKey |
β |
IndexOutOfRangeException |
Key index out of range in SetKeyColor |
β |
using DeckSurf.SDK.Exceptions;
try
{
device.SetKey(0, imageData);
}
catch (DeviceCommunicationException ex) when (ex.IsTransient)
{
// USB I/O failure β safe to retry
}
catch (DeviceDisconnectedException ex)
{
// Device was physically unplugged
Console.WriteLine($"Lost device {ex.DeviceSerial}");
}For event-driven error handling, subscribe to DeviceErrorOccurred:
device.DeviceErrorOccurred += (sender, e) =>
{
Console.WriteLine($"Error in {e.OperationName}: {e.Category} (transient: {e.IsTransient})");
};When a device is unplugged, the DeviceDisconnected event fires. After this event, the device instance is unusable β no further events will fire and all method calls will throw ObjectDisposedException. Dispose it and acquire a fresh instance to reconnect:
device.DeviceDisconnected += (sender, e) =>
{
Console.WriteLine("Device disconnected. Attempting to reconnect...");
device.Dispose();
// Re-enumerate to find the device again
if (DeviceManager.TryGetDeviceBySerial(knownSerial, out var newDevice))
{
// Use newDevice
}
};Use serial numbers for stable device identification across re-plugs:
// Find a specific device
if (DeviceManager.TryGetDeviceBySerial("CL12K1A00042", out var myDevice))
{
using (myDevice)
{
myDevice.StartListening();
myDevice.SetKey(0, imageData);
}
}
// Or throw if not found
var device = DeviceManager.GetDeviceBySerial("CL12K1A00042");
// Monitor for device connection changes
DeviceManager.DeviceListChanged += (sender, e) =>
{
Console.WriteLine("USB device change detected β re-enumerate devices.");
};Devices with LCD screens expose screen operations:
if (device.IsScreenSupported)
{
byte[] screenImage = File.ReadAllBytes("banner.jpg");
var resized = ImageHelper.ResizeImage(
screenImage,
device.ScreenWidth,
device.ScreenHeight,
device.ImageRotation,
device.KeyImageFormat);
device.SetScreen(resized, 0, device.ScreenWidth, device.ScreenHeight);
}The SDK is not thread-safe. All events (ButtonPressed, DeviceDisconnected, DeviceErrorOccurred) fire on background threads (thread pool). If you need to update UI or call device methods from event handlers, you must synchronize access:
var deviceLock = new object();
// Safe to call SetKey from a button press handler
device.ButtonPressed += (sender, e) =>
{
if (e.EventKind != ButtonEventKind.Down) return;
lock (deviceLock)
{
device.SetKey(e.Id, highlightImage);
}
};
// WPF/WinForms: marshal to UI thread
device.ButtonPressed += (sender, e) =>
{
Dispatcher.Invoke(() => statusLabel.Text = $"Button {e.Id} pressed");
};All device I/O is synchronous β USB HID has no true async primitives. If you need to avoid blocking the UI thread (WPF, WinForms), use Task.Run at the call site:
// WPF button click handler β offload to thread pool
await Task.Run(() => device.SetKey(0, imageData));
await Task.Run(() => device.SetBrightness(80));This is intentional: the SDK does not wrap sync calls in fake async methods. You control when and where thread pool threads are used.
DeviceWatcher monitors a specific device by serial number and raises events on connect/disconnect:
using DeckSurf.SDK.Core;
using var watcher = new DeviceWatcher("CL12K1A00042");
watcher.DeviceConnected += (sender, device) =>
{
Console.WriteLine($"Device reconnected: {device.DisplayName}");
device.StartListening();
};
watcher.DeviceLost += (sender, e) =>
{
Console.WriteLine("Device lost β waiting for reconnection...");
};
watcher.Start();For raw change notifications, subscribe to DeviceManager.DeviceListChanged:
using DeckSurf.SDK.Core;
DeviceManager.DeviceListChanged += (sender, e) =>
{
foreach (var added in e.Added)
Console.WriteLine($"Connected: {added.Name} ({added.Serial})");
foreach (var removed in e.Removed)
Console.WriteLine($"Disconnected: {removed.Name} ({removed.Serial})");
};The SDK integrates with Microsoft.Extensions.Logging. Configure before creating devices:
using Microsoft.Extensions.Logging;
using DeckSurf.SDK.Core;
DeckSurfConfiguration.LoggerFactory = LoggerFactory.Create(builder =>
{
builder.AddConsole().SetMinimumLevel(LogLevel.Debug);
});
// All subsequent device operations will be logged
var devices = DeviceManager.GetDeviceList();The SDK logs device operations at appropriate levels: Debug for SetKey/SetBrightness calls, Information for StartListening/StopListening, Warning for transient USB errors, and Error for device disconnections.
| Device | Support |
|---|---|
| Stream Deck XL | Full |
| Stream Deck XL (2022) | Full |
| Stream Deck Plus | Full |
| Stream Deck Original | Full |
| Stream Deck Original (2019) | Full |
| Stream Deck MK.2 | Full |
| Stream Deck Mini | Full |
| Stream Deck Mini (2022) | Full |
| Stream Deck Neo | Full |
Core functionality is cross-platform (Windows, macOS, Linux). The SDK uses HidSharp for USB HID communication.
A small number of utility methods (ImageHelper.GetFileIcon(), ImageHelper.GetImageBuffer(Icon)) are Windows-only and are marked with [SupportedOSPlatform("windows")]. All core device operations work cross-platform.
Windows: Works out of the box. Close the Elgato Stream Deck software before running β it holds exclusive access to the device.
macOS: USB HID access may require app entitlements (com.apple.security.device.usb). Without this, GetDeviceList() will return an empty list with no error.
Linux: Configure udev rules for non-root USB access:
# /etc/udev/rules.d/99-streamdeck.rules
SUBSYSTEM=="usb", ATTRS{idVendor}=="0fd9", MODE="0666"
SUBSYSTEM=="hidraw", ATTRS{idVendor}=="0fd9", MODE="0666"Then reload: sudo udevadm control --reload-rules && sudo udevadm trigger
GetDeviceList() returns empty:
- Close the Elgato Stream Deck software (it holds exclusive USB access)
- On Linux, ensure udev rules are configured (see above)
- On macOS, check USB entitlements in your app's codesign profile
- Disconnect and reconnect the device
- Verify the device appears in your OS device manager
Refer to https://docs.deck.surf for tutorials and full API documentation.
This project is licensed under the MIT License. See LICENSE.md for details.
