2026-05-10 09:41:29 -04:00
|
|
|
# TeamsISO Control Surface — REST API
|
|
|
|
|
|
|
|
|
|
TeamsISO can expose a localhost HTTP server so external controllers
|
|
|
|
|
(Bitfocus Companion, Stream Deck plugins, Bome MIDI Translator, custom
|
|
|
|
|
node-RED flows, command-line scripts) can drive it without a UI binding.
|
|
|
|
|
|
|
|
|
|
## Enabling
|
|
|
|
|
|
|
|
|
|
1. Open TeamsISO → Settings → DISPLAY tab.
|
|
|
|
|
2. Tick "Control surface (Stream Deck / Companion)".
|
|
|
|
|
3. Default port is **9755**; change it via the port textbox if needed.
|
2026-05-10 10:01:32 -04:00
|
|
|
4. By default the server binds to `127.0.0.1` only — it is NOT reachable
|
|
|
|
|
from the LAN.
|
|
|
|
|
5. To allow other machines on the same network to drive TeamsISO (the
|
|
|
|
|
"headless host PC + thin client" scenario), tick the nested
|
|
|
|
|
"LAN-reachable" checkbox underneath. The settings panel will display
|
|
|
|
|
the LAN URL (e.g. `http://192.168.1.42:9755/ui`) with a Copy button.
|
2026-05-10 09:41:29 -04:00
|
|
|
|
|
|
|
|
When enabled, the toast confirms `Control surface listening on
|
2026-05-10 10:01:32 -04:00
|
|
|
http://127.0.0.1:9755/` (or the all-interfaces equivalent in LAN mode).
|
|
|
|
|
|
|
|
|
|
### One-time setup for LAN-reachable mode
|
|
|
|
|
|
|
|
|
|
Windows requires elevated permission to bind a non-loopback HTTP listener.
|
|
|
|
|
If you turn on LAN-reachable mode and don't see a connection from another
|
|
|
|
|
machine, run this **once** in an Administrator PowerShell (replace `9755`
|
|
|
|
|
if you've changed the port):
|
|
|
|
|
|
|
|
|
|
```powershell
|
|
|
|
|
netsh http add urlacl url=http://+:9755/ user=Everyone
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
Also confirm the Windows Firewall is letting inbound traffic to that port
|
|
|
|
|
through — `New-NetFirewallRule -DisplayName "TeamsISO Control Surface" -Direction Inbound -Protocol TCP -LocalPort 9755 -Action Allow`
|
|
|
|
|
in an elevated PowerShell, or add it through Windows Defender Firewall →
|
|
|
|
|
Advanced Settings → Inbound Rules.
|
2026-05-10 09:41:29 -04:00
|
|
|
|
|
|
|
|
## Authentication
|
|
|
|
|
|
2026-05-10 10:01:32 -04:00
|
|
|
None — by design. In localhost-only mode, the loopback bind is the
|
|
|
|
|
security model: any process on the operator's machine can hit these
|
|
|
|
|
endpoints, the same threat model as a Stream Deck's USB connection.
|
|
|
|
|
|
|
|
|
|
In LAN-reachable mode, the assumption is a closed/trusted network (a
|
|
|
|
|
production-control LAN, a dedicated show subnet, a private vlan). Any
|
|
|
|
|
machine that can route to the host on the listener port can drive
|
|
|
|
|
TeamsISO. **Do not enable LAN-reachable mode on an untrusted network.**
|
2026-05-10 09:41:29 -04:00
|
|
|
|
|
|
|
|
## Response shape
|
|
|
|
|
|
|
|
|
|
All responses are `application/json` with `Access-Control-Allow-Origin: *`
|
|
|
|
|
so a browser-based control panel served from another origin can call the
|
|
|
|
|
endpoints. Most successful responses include `"ok": true` plus operation-
|
|
|
|
|
specific fields. Errors return HTTP 4xx/5xx with `{"error": "..."}`.
|
|
|
|
|
|
|
|
|
|
## Endpoints
|
|
|
|
|
|
|
|
|
|
### `GET /ui`
|
|
|
|
|
|
|
|
|
|
Self-contained HTML control panel. Open this in a browser to drive
|
|
|
|
|
TeamsISO from a phone, tablet, or second monitor. Lists participants live
|
|
|
|
|
via the same `/ws` WebSocket the rest of the doc describes, and posts to
|
|
|
|
|
the REST endpoints when you click. Single page, no external dependencies,
|
|
|
|
|
loads in <50KB.
|
|
|
|
|
|
|
|
|
|
### `GET /`
|
|
|
|
|
|
|
|
|
|
Returns server info and an endpoint summary. Useful for "is the surface
|
|
|
|
|
alive?" probes.
|
|
|
|
|
|
|
|
|
|
```json
|
|
|
|
|
{
|
|
|
|
|
"product": "TeamsISO",
|
|
|
|
|
"version": "1.0.0.0",
|
|
|
|
|
"endpoints": ["GET /participants", "POST /participants/{id}/iso", ...]
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
### `GET /participants`
|
|
|
|
|
|
|
|
|
|
Snapshot of the current participant list as the UI sees it.
|
|
|
|
|
|
|
|
|
|
```json
|
|
|
|
|
{
|
|
|
|
|
"participants": [
|
|
|
|
|
{
|
|
|
|
|
"id": "1c3e2a8b-...-...",
|
|
|
|
|
"displayName": "Jane",
|
|
|
|
|
"isOnline": true,
|
|
|
|
|
"isEnabled": false,
|
|
|
|
|
"customName": null,
|
|
|
|
|
"stateLabel": "—"
|
|
|
|
|
}
|
|
|
|
|
]
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
### `POST /participants/{id}/iso`
|
|
|
|
|
|
|
|
|
|
Enable or disable an ISO by participant Id. Body or query string:
|
|
|
|
|
|
|
|
|
|
```json
|
|
|
|
|
{ "enabled": true, "customName": "Host" }
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
`enabled` is optional — omitting it toggles the current state. `customName`
|
|
|
|
|
is optional and overrides the auto-generated NDI output name.
|
|
|
|
|
|
|
|
|
|
```sh
|
|
|
|
|
curl -X POST 'http://127.0.0.1:9755/participants/1c3e2a8b-.../iso?enabled=true&customName=Host'
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
### `POST /participants/iso`
|
|
|
|
|
|
|
|
|
|
Same as above but resolves by display name instead of Id. The Id varies
|
|
|
|
|
across meetings; the display name is the operator-stable identifier.
|
|
|
|
|
|
|
|
|
|
```json
|
|
|
|
|
{ "displayName": "Jane", "enabled": true }
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
### `POST /presets/{name}/apply`
|
|
|
|
|
|
|
|
|
|
Apply a saved preset to the live participant list. Walks every participant
|
|
|
|
|
in the meeting, matches by display name, sets the custom output name, and
|
|
|
|
|
reconciles each enable/disable via the engine. Same code path as the
|
|
|
|
|
Presets dialog and the auto-apply-on-launch flow (`PresetApplier.ApplyAsync`).
|
|
|
|
|
|
|
|
|
|
```json
|
|
|
|
|
{
|
|
|
|
|
"ok": true,
|
|
|
|
|
"name": "Friday Show",
|
|
|
|
|
"matched": 4,
|
|
|
|
|
"changed": 2,
|
|
|
|
|
"skipped": 1
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
`matched` is how many participants in the preset were live in the meeting;
|
|
|
|
|
`changed` is how many actually flipped state; `skipped` is preset entries
|
|
|
|
|
with no live counterpart.
|
|
|
|
|
|
|
|
|
|
### `POST /presets/refresh-discovery`
|
|
|
|
|
|
|
|
|
|
Force NDI discovery to rebuild its finder. Useful after Apply Transcoder
|
|
|
|
|
Topology or when Teams restarts mid-show. Returns immediately; the rebuild
|
|
|
|
|
happens on the next poll tick.
|
|
|
|
|
|
|
|
|
|
```sh
|
|
|
|
|
curl -X POST http://127.0.0.1:9755/presets/refresh-discovery
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
### `POST /presets/stop-all`
|
|
|
|
|
|
|
|
|
|
Disable every running ISO. Equivalent to clicking "Stop all ISOs" in the
|
|
|
|
|
header. Returns the count that were running.
|
|
|
|
|
|
|
|
|
|
### `POST /teams/mute` / `/camera` / `/share` / `/leave` / `/raise-hand`
|
|
|
|
|
|
|
|
|
|
Drive the corresponding Microsoft Teams in-call control via UIAutomation.
|
|
|
|
|
Returns one of `Invoked` / `TeamsNotRunning` / `ControlNotFound` /
|
|
|
|
|
`InvokeFailed` in the `result` field.
|
|
|
|
|
|
|
|
|
|
```sh
|
|
|
|
|
curl -X POST http://127.0.0.1:9755/teams/mute
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
### `POST /recording`
|
|
|
|
|
|
|
|
|
|
Toggle per-output recording on or off. Body or query string:
|
|
|
|
|
|
|
|
|
|
```json
|
|
|
|
|
{ "enabled": true, "directory": "D:/recordings/show-2026-05-09" }
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
`directory` is optional when `enabled=false`. Already-running ISOs are not
|
|
|
|
|
retroactively recorded — the operator should disable + re-enable a
|
|
|
|
|
participant to start recording it.
|
|
|
|
|
|
|
|
|
|
### `POST /recording/marker`
|
|
|
|
|
|
|
|
|
|
Drop a timestamped marker into every active recording. Body or query string
|
|
|
|
|
optionally carries a `label`; if omitted, the label defaults to
|
|
|
|
|
`Marker @ HH:mm:ss`. Markers land in each recording's `manifest.json` under
|
|
|
|
|
the `markers[]` array as `{ "offsetMs": 12345.6, "label": "Guest answer" }`.
|
|
|
|
|
|
|
|
|
|
```sh
|
|
|
|
|
curl -X POST 'http://127.0.0.1:9755/recording/marker?label=Guest+answer'
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
### `POST /notes`
|
|
|
|
|
|
|
|
|
|
Append a timestamped line to today's show-notes file at
|
|
|
|
|
`%LOCALAPPDATA%\TeamsISO\Notes\<YYYY-MM-DD>.md`. Body or query string carries
|
|
|
|
|
`text`. Each line is prefixed with `**HH:mm:ss** —`; the file is markdown so
|
|
|
|
|
it renders nicely in any editor.
|
|
|
|
|
|
|
|
|
|
```sh
|
|
|
|
|
curl -X POST 'http://127.0.0.1:9755/notes?text=guest+segment+starts'
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
### `POST /recording/roll`
|
|
|
|
|
|
|
|
|
|
Roll every active recording into a new chunk. Each running pipeline is
|
|
|
|
|
disabled (recorder finalizes its `manifest.json`), waits ~150ms, then re-
|
|
|
|
|
enabled (recorder opens a fresh subdirectory keyed by display name +
|
|
|
|
|
timestamp). Useful for chaptering between show segments — a Stream Deck
|
|
|
|
|
button mapped to this gives operators "next segment" without losing the
|
|
|
|
|
already-recorded footage.
|
|
|
|
|
|
|
|
|
|
```sh
|
|
|
|
|
curl -X POST http://127.0.0.1:9755/recording/roll
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
Response:
|
|
|
|
|
```json
|
|
|
|
|
{ "ok": true, "action": "roll-recording", "rolled": 4 }
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
## WebSocket — live state push
|
|
|
|
|
|
|
|
|
|
For controllers that want to light a button when an ISO goes LIVE without
|
|
|
|
|
polling, connect to:
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
ws://127.0.0.1:9755/ws
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
On connect, the server sends a participants snapshot. Whenever the snapshot
|
|
|
|
|
changes (participant joins/leaves, ISO toggled, custom name edited), a fresh
|
|
|
|
|
snapshot is pushed within 250ms. Format:
|
|
|
|
|
|
|
|
|
|
```json
|
|
|
|
|
{
|
|
|
|
|
"type": "participants",
|
|
|
|
|
"participants": [
|
|
|
|
|
{ "id": "...", "displayName": "Jane", "isOnline": true,
|
|
|
|
|
"isEnabled": true, "customName": "Host", "stateLabel": "LIVE" }
|
|
|
|
|
]
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
Client→server messages are ignored for v1 — all commands go through REST.
|
|
|
|
|
|
|
|
|
|
## OSC over UDP
|
|
|
|
|
|
|
|
|
|
Same command surface, different transport. Enable the OSC bridge in the
|
|
|
|
|
DISPLAY tab (default port **9000** — TouchOSC's default). Bound to
|
2026-05-10 10:01:32 -04:00
|
|
|
`127.0.0.1` by default; honors the same LAN-reachable toggle as the REST
|
|
|
|
|
surface — when LAN mode is on, OSC binds to `0.0.0.0` so a TouchOSC tablet
|
|
|
|
|
on the same network can talk to the host directly.
|
2026-05-10 09:41:29 -04:00
|
|
|
|
|
|
|
|
Address vocabulary:
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
/teamsiso/iso "DisplayName" {0|1} — toggle/set ISO by display name
|
|
|
|
|
/teamsiso/iso/by-id "guid" {0|1} — toggle/set by Id
|
|
|
|
|
/teamsiso/preset "Name" — apply preset
|
|
|
|
|
/teamsiso/teams/mute — UIA toggle mute
|
|
|
|
|
/teamsiso/teams/camera — UIA toggle camera
|
|
|
|
|
/teamsiso/teams/leave — UIA leave
|
|
|
|
|
/teamsiso/teams/share — UIA share tray
|
|
|
|
|
/teamsiso/teams/raise-hand — UIA raise hand
|
|
|
|
|
/teamsiso/refresh-discovery — rebuild NDI finder
|
|
|
|
|
/teamsiso/stop-all — disable every ISO
|
|
|
|
|
/teamsiso/recording {0|1} — recording on/off (default dir)
|
|
|
|
|
/teamsiso/recording/marker "Label" — drop a marker on every active recording
|
|
|
|
|
/teamsiso/recording/roll — roll every active recording into a new chunk
|
|
|
|
|
/teamsiso/notes "Free-form note" — append a timestamped line to today's notes
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
Companion → Surfaces → "Generic OSC" supports outbound OSC; bind a button
|
|
|
|
|
press to e.g. `/teamsiso/iso "Jane" 1`. TouchOSC layouts can use the same
|
|
|
|
|
addresses on the same UDP port.
|
|
|
|
|
|
|
|
|
|
## Bitfocus Companion recipe
|
|
|
|
|
|
|
|
|
|
Companion ships a generic HTTP module. Configure a button:
|
|
|
|
|
|
|
|
|
|
- **Action:** `HTTP: HTTP POST request`
|
|
|
|
|
- **URL:** `http://127.0.0.1:9755/teams/mute`
|
|
|
|
|
- **Body type:** None
|
|
|
|
|
|
|
|
|
|
Or for a participant-specific toggle:
|
|
|
|
|
|
|
|
|
|
- **URL:** `http://127.0.0.1:9755/participants/iso?displayName=Jane&enabled=true`
|
|
|
|
|
|
|
|
|
|
## Stream Deck XL recipe (without Companion)
|
|
|
|
|
|
|
|
|
|
Use the "Web Requests" plugin (or any equivalent). Set the action to a POST
|
|
|
|
|
on the appropriate endpoint above.
|
|
|
|
|
|
|
|
|
|
## Future work
|
|
|
|
|
|
2026-05-10 10:01:32 -04:00
|
|
|
- **HTTPS / token auth** — for deployments that don't have a closed
|
|
|
|
|
network, layer TLS termination + a shared bearer token in front of the
|
|
|
|
|
HttpListener. Out of scope for v1; the LAN-reachable mode is a
|
|
|
|
|
trusted-network feature only.
|