Enable and Configure the Show-Control Protocol
Drive Backstage playback, audio, calibration, NDI input routing and node-graph endpoints from an external control system (Crestron, AMX, Q-SYS, or a custom application) using a structured JSON protocol over TCP or UDP. This is the standard remote control interface.
Looking for a different route? Backstage can be driven several ways. Choose the one that fits:
Show-control protocol (this page) -- structured JSON over TCP/UDP with typed domains, named addressing, and asynchronous live feedback. The standard interface, and the one to reach for when a machine drives the show.
Web Console -- the same control surface in a browser. Anything controllable over this protocol can be put on a page Backstage serves, using blocks that mirror these domains. Best when a person rather than a control system drives the show. See Build and Serve Web Pages with the Web Console.
Automation API and MCP -- HTTP, for scripts, integration tools and AI agents. Reaches work this protocol does not, such as importing media and building surfaces. See Drive Backstage Headlessly with the Automation API.
OSC -- built as a node chain in the Node editor, for systems that already speak OSC. See Control Backstage via OSC.
UDP trigger strings -- a custom command vocabulary you build yourself, via the StepperCommander patch. See Trigger Playback via UDP.
Before you start
- Backstage is running with at least one sequence, stepper, or other entity you want to control
- You know the IP address of the Backstage server on your control network
- Your control system can send TCP or UDP messages (plain JSON text, newline-terminated)
- If using the reference control-panel app:.NET 10 SDK installed on a Windows machine
Steps
1. Turn the protocol on
Open Settings and expand Remote Control. Check Enable Show-Control Protocol.
The protocol is disabled by default. Until you enable it, Backstage refuses connections — this is the first thing to check when a controller cannot reach the server.
TCP starts immediately on port 7400, on all interfaces. UDP is off by default; enable it and confirm its port (default 7401) only if you need it.
2. Pick the transports you need
TCP is the right default: it is reliable, and live feedback arrives on the same connection you already have open.
UDP suits fire-and-forget commands on a dedicated control LAN. Because it is connectionless, it also needs a decision about where replies go — the UDP feedback dropdown either answers whichever address sent the datagram, or sends everything to one fixed host and port.
3. Restrict who can reach it
Three independent layers, and it is worth setting them deliberately rather than relying on any one:
|
Layer |
What it decides |
|---|---|
|
Bind interface |
Which network the protocol is reachable on at all. Set it to |
|
IP allowlist |
Who may connect — comma-separated addresses or CIDR ranges, e.g. |
|
Auth token |
Who may issue commands. See Authenticating below for the exchange |
UDP is connectionless and its source address can be spoofed, so the allowlist is weaker protection there than on TCP. Run UDP only on a trusted control LAN.
4. Apply the changes
Port, bind interface and transport changes need Apply / Restart Protocol Server. The auth token and IP allowlist are read live and take effect on the next connection or datagram — no restart.
5. Verify it is listening
From the control machine, open a raw TCP connection to port 7400 with a tool such as ncat, paste a line and press Enter:
{"domain":"sys","action":"ping"}
A healthy server answers:
{"ok":true,"result":{"pong":true}}
A ping works whether or not a token is set, so it is a clean test of reachability on its own.
Authenticating
Leave Auth token empty and every client that gets past the allowlist may issue commands. Set it, and a client must present the token before Backstage will accept anything else.
Send the token as the first message on the connection:
{"domain":"sys","action":"auth","args":{"token":"your-token"}}
Success returns an empty result. A wrong or missing token returns 401 with the message authentication required (sys.auth). The token may also be passed as the message's target instead of in args, for controllers whose templates cannot nest an object.
What works before authenticating
Four sys actions are exempt from the gate, so a client can identify itself and check the server is alive before holding a credential:
|
Action |
Why it is exempt |
|---|---|
|
|
Reachability check |
|
|
Protocol version query, so a client can decide whether to proceed at all |
|
|
The gate itself — necessarily reachable |
|
|
Optional self-identification, which happens before the client is trusted |
Everything else — including list and status — is refused until the token is accepted. Reading state is treated as privileged, not public.
Authentication is per connection
The authenticated flag lives on the connection, not on the client or its address. Every new TCP connection starts unauthenticated and must send sys.auth again — so a controller that reconnects after a dropped link has to re-authenticate as part of its reconnect logic, not just resume sending commands.
Backstage logs the commands it executes so an operator can see what was triggered, but never logs the token, and does not log auth, client, ping, version, list, status, subscribe or unsubscribe at all — the read-only and plumbing traffic that would otherwise flood the log.
A second, separate gate: Companion
A client that identifies itself as Companion through sys.client is also checked against the Enable Bitfocus Companion setting. If that is off, it is refused with 403 — a different code from the auth failure, and a different exemption list: only ping and version still work.
This is a convenience switch for turning Companion off without disturbing other clients. It is not a security boundary — the token and the allowlist are. A client that simply does not identify itself as Companion is unaffected by it.
Message conventions
These apply to every domain, which is why they live here rather than on the element pages.
Framing
The wire format is newline-delimited JSON — exactly one JSON object per line, each terminated with a newline. A message without its terminator is buffered and never processed, which is the single most common integration fault.
A request carries a domain, an action, usually a target, and per-action args. An optional id — any JSON value — is echoed back untouched, which is how an asynchronous client correlates replies when several messages are in flight.
Responses
{"id":17,"ok":true,"result":{ }}
{"id":17,"ok":false,"error":{"code":404,"message":"stepper not found"}}
|
Code |
Meaning |
|---|---|
|
|
Bad request, unknown action, or invalid JSON |
|
|
Authentication required, or the token was wrong |
|
|
The client identified as Companion and Enable Bitfocus Companion is off |
|
|
Unknown domain, or no entity with that name |
A control action answers {"accepted": true}, which means only that Backstage queued it. The work happens asynchronously. Confirm real state through a status read or a subscription — never by assuming the command you sent took effect.
Addressing
Every entity is addressed by the exact name you see in Backstage, case-sensitive. There are no ids or indices to track, and no stable handle behind the name — so renaming an entity silently breaks whatever pointed at it. Each domain's list action returns the current names.
Live feedback
Subscribe once per connection and Backstage pushes status instead of you polling for it:
{"domain":"sys","action":"subscribe","args":{"domains":["seq","step"]}}
Omit domains to subscribe to everything. You then get an immediate snapshot, an event whenever a subscribed entity changes, and a heartbeat roughly every 3 seconds that re-sends current status — so a dropped frame heals itself within a few seconds and you do not need a polling loop of your own. Feedback rate (Hz) controls how often the server looks for changes to push.
Options explained
Settings > Remote Control
|
Setting |
Description |
Values / Range |
|---|---|---|
|
Enable Show-Control Protocol |
Master on/off for the protocol |
Checkbox (off by default) |
|
Bind interface |
Network interface the protocol listens on |
Dropdown: Any (0.0.0.0) or a specific NIC |
|
TCP |
Enable or disable the TCP transport |
Checkbox (on by default when the protocol is enabled) |
|
TCP Port |
Port for the TCP server |
1–65535 (default 7400) |
|
UDP |
Enable or disable the UDP transport |
Checkbox (off by default) |
|
UDP Port |
Port for UDP receive |
1–65535 (default 7401) |
|
UDP feedback |
Where UDP responses and events are sent |
Dropdown: Reply to sender / Fixed host:port |
|
Feedback host |
Address for fixed-target UDP feedback (shown when "Fixed host:port" is selected) |
IP address |
|
Feedback port |
Port for fixed-target UDP feedback |
1–65535 (default 7402) |
|
CRLF line endings |
Terminate responses and events with CRLF instead of LF |
Checkbox (on by default) |
|
Auth token |
Require clients to send |
Text (empty = no auth) |
|
IP allowlist |
Restrict which addresses or CIDR ranges may connect. Read live |
Comma-separated (empty = allow any) |
|
Feedback rate (Hz) |
How often the server checks for and pushes status changes |
1–60 Hz (default 10) |
|
Enable Bitfocus Companion |
Allows clients that identify themselves as Companion. A convenience switch for gating Companion specifically, not a security boundary — the token and allowlist are. Read live |
Checkbox (on by default) |
Choosing a port
7400 and 7401 were picked to stay clear of NDI, Dante, ST 2110, Q-SYS, Crestron and AMX. One known clash remains: DDS/RTPS middleware also uses the 7400 range, so change the port if your site runs it.
The reference control panel
A standalone Backstage Control Panel ships in tools/BackstageControlPanel/ — a C# WPF client that speaks the whole protocol. It exists for testing and as a reference implementation for integrators rather than for production operation.
Enter Host, Port and Token in its connection bar and click Connect; it reconnects on its own if the link drops. It has a tab per entity type, each listing live status with control buttons, and a Protocol JSON log at the bottom. That log is the fastest way to debug an integration: pause it, reproduce the problem, and read the exact JSON exchanged.
Common Mistakes
-
Nothing connects at all — the protocol is off by default. Enable it in Settings, then check the bind interface, the allowlist and the token in that order.
-
Forgetting the newline — every message needs its terminator. Without it the server holds a partial line forever and stays silent.
-
Port or bind change appears to do nothing — those need Apply / Restart Protocol Server. Only the token and allowlist apply live.
-
401 on a
listorstatuscall — reading is privileged too. Only ping, version, auth and client work before authenticating. -
Commands work, then stop after a reconnect — the authenticated flag is per connection. A reconnecting client must send
sys.authagain. -
403 rather than 401 — that is the Companion gate, not the token. The client identified itself as Companion and Enable Bitfocus Companion is off.
-
Case mismatch on names — targets match exactly. Use a domain's
listto read back the real spelling. -
Treating
accepted: trueas done — it means queued. Subscribe or readstatusto confirm. -
Running UDP on a shared network — it is spoofable. Keep it on a trusted control LAN, and prefer TCP for anything bidirectional.
Related
Remote Control — what you can control, element by element, with domains and actions
Build and Serve Web Pages with the Web Console — the same control surface in a browser, for operators rather than control systems
Drive Backstage Headlessly with the Automation API — the Automation API and MCP, for scripts and AI agents
Control Backstage with the Bitfocus Companion Module — Bitfocus Companion, a ready-made client of this protocol
