# 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. 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. When enabled, the toast confirms `Control surface listening on 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. ## Authentication 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.** ## 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\.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 `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. 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 - **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.