# Local control API v1

Enable Local API in Settings > More settings > Local API and press Apply. Default address:
`http://127.0.0.1:47831/api/v1`. The port (1024-65535) and IPv4 address are configurable.
For LAN access, enter this PC's LAN IPv4 address, for example `192.168.1.50`.
Only that interface is bound; wildcard addresses and hostnames are not accepted.
IP and port persist locally. Old settings default to loopback. No firewall rules are
added automatically: LAN access may require allowing the selected TCP port on the
Windows private-network firewall, restricted to trusted clients. DHCP address changes
require updating the configured IP. Use the displayed address on the other PC.
Media Board must be open. A failed bind never switches ports automatically.

There is deliberately **no authentication**. Every local process can control
playback while the API is enabled. On LAN, any reachable client has the same access.
Never forward this port or expose it through a public proxy. The server binds the
configured IPv4 address (loopback by default), requires the exact numeric Host
and port. Browser navigation and cross-origin JavaScript are allowed, with CORS
for GET/POST and OPTIONS preflight. Credentials are not supported or required.
**Any website you visit may read the catalogue and issue playback commands while
the API is enabled.** Disable it when not needed. Loopback and Host checks are
not authentication against local applications or browser pages. Browser local
network permissions or other browser security policies may still restrict access.

## Endpoints

| Method | Path | Result |
| --- | --- | --- |
| GET | `/buttons` | Buttons from all playlists: id, title, playlistId, playlist, type, isPlaying, isPaused |
| GET | `/status` | Service state and active/paused buttons |
| GET | `/executions/{executionId}` | Audio/video execution state, position and terminal reason |
| POST | `/buttons/{id}/play` | Start, or leave an already playing button unchanged; resume when paused |
| POST | `/buttons/{id}/restart` | Stop this button if active, then start it from its configured start |
| POST | `/buttons/{id}/pause` | Pause the specified execution; optional fadeOutMs |
| POST | `/buttons/{id}/resume` | Resume the specified execution; optional fadeInMs |
| POST | `/buttons/{id}/stop` | Stop this button with inherited audio fade or optional fadeOutMs; an inactive button is a no-op |
| POST | `/buttons/{id}/dmx/sync` | Synchronize only the currently active DMX timeline with an external clock |

Play/restart/stop support bodyless requests. Current builds also accept optional
playback overrides and execution-aware pause/resume/stop, plus GET
`/executions/{executionId}`. See [playback execution reference](local-api-quiz.md)
for JSON bodies, external sequencing, lifecycle states and fade behavior.
Play/restart also accept `loop` and `loopCrossfadeMs` for per-execution media repeat:
`{"externalSequencing":true,"loop":true,"loopCrossfadeMs":1500}`.
The crossfade is audio-only, keeps the same executionId, and does not restart DMX.
`loopCrossfadeMs` is an optional integer 0-60000; zero means a hard loop.
When omitted/null, it follows Mix on transition (positive button Fade out, otherwise
playlist Crossfade duration); with Mix disabled it defaults to zero. A positive
explicit value overrides Mix. It does not enable looping by itself. See the
playback execution reference for duration limits and pause/stop semantics.
DMX sync requires JSON (see below).
Navigation buttons are listed but cannot be played.
Copy the GUID from Configure > Button code, or use GET /buttons. Renaming a
button does not change the ID. Duplicating creates a new ID; after import or
restore query /buttons again, since library workflows may replace identifiers.
Duplicate IDs are rejected rather than selecting an arbitrary button.

## External DMX clock

Start the button with POST `/buttons/{id}/play`. Its response includes
`dmxExecutionId` (also exposed by GET `/buttons` and `/status`). This is null
when that button does not own an active DMX execution. Each new start/restart
generates a new ID; do not reuse one from an earlier execution.

Send POST `/buttons/{id}/dmx/sync` with `Content-Type: application/json`:

```json
{
  "executionId": "REPLACE-WITH-DMX-EXECUTION-ID",
  "sequence": 1,
  "positionMs": 12500,
  "isPlaying": true,
  "forceSeek": false
}
```

All fields except `forceSeek` are required. `sequence` is a nonnegative,
strictly increasing integer per execution. `positionMs` is a finite,
nonnegative time from the start of the external video (not a wall-clock timestamp).
Send the freshest position every 200-250 ms, and immediately on pause, resume
or seek. Set `forceSeek: true` after an intentional video seek.

Corrections occur above 100 ms of drift, on play/pause changes, or on forced seek.
While messages are absent, DMX continues on its local clock (or remains paused
if that was the last state). The next message can realign it. Pausing holds the
current light scene; no audio/video player or other button is paused or sought.
Looping DMX wraps the supplied time by its duration. Non-looping DMX reaches
blackout at its end and completes; restart explicitly to control it again.
This is not latency compensation for the network or wireless lighting hardware.

Success returns HTTP 200 with `executionId`, `sequence`, `corrected`,
`positionMs`, `driftMs` (target minus local time, shortest distance for loops)
and `isPlaying`. The last field is the accepted clock state, not a guarantee
that a non-looping timeline has not just completed.

Errors: 400 `invalid_dmx_sync`; 415 `json_required`; 413 for oversized bodies;
404 `button_not_found`; 422 `no_dmx_timeline`; 409 `dmx_not_active`,
`stale_execution`, `stale_sync`, `dmx_initializing`, `app_busy` or
`command_in_progress`. During initialization or a busy command, try again
with a fresh position and higher sequence. On stale execution or inactive DMX,
stop sending and explicitly decide whether to start a new execution. Never
silently attach delayed messages to a replacement execution.
Bodies are limited to 1024 bytes. Sync never implicitly starts playback.

```powershell
$api = 'http://127.0.0.1:47831/api/v1'
Invoke-RestMethod "$api/buttons"
$id = 'REPLACE-WITH-BUTTON-CODE'
Invoke-RestMethod -Method Post "$api/buttons/$id/play"
Invoke-RestMethod -Method Post "$api/buttons/$id/stop"
```

Open `http://127.0.0.1:47831/api/v1/buttons` or `/api/v1/status` directly
in the browser. Commands still require POST, not address-bar navigation:

```javascript
const api = 'http://127.0.0.1:47831/api/v1';
const buttons = await fetch(`${api}/buttons`).then(r => r.json());
const id = 'REPLACE-WITH-BUTTON-CODE';
const result = await fetch(`${api}/buttons/${id}/play`, { method: 'POST' })
  .then(r => r.json());
```

Responses are JSON. 202 `accepted` means the playback command was submitted,
not that a file has finished loading or a USB device acknowledged output.
Media, overlap, image/text, DMX and playlist playback rules remain managed by
the existing application commands. There is no sample-accurate scheduling.

200: query/no-op/stop. 400: invalid ID or unexpected body. 403: Host
rejected or license limit exceeded. 404: unknown endpoint/button. 405: wrong
method. 409: command in progress, app busy or ambiguous ID. 422: missing media
file or non-media button. 500: command failed. 503: app shutting down.
Do not automatically retry restart after a disconnected request: a command may
have already started. Concurrent commands receive 409, not an unbounded queue.

Port and enable preference are in local settings, not playlist/cloud backups.
Framework-dependent installations require .NET 8 Desktop and ASP.NET Core 8
runtimes; the installer handles both. Self-contained builds include them.
