Web Control API
Laika includes a built-in REST API for remote control. Enable it via Settings > API > Enable Web API. Default port is 1928.
All responses include Access-Control-Allow-Origin: * for browser access.
Base URL: http://<laika-host>:1928
Download OpenAPI Spec (YAML) — import into Postman, Insomnia, or any OpenAPI-compatible tool.
Status
Section titled “Status”Get the current application state.
GET /api/statusResponse:
{ "layout": "2x2", "launchpad": "Studio A", "viewer_count": 4, "projected_viewer": null, "version": "1.0.57"}| Field | Type | Description |
|---|---|---|
layout | string | Current layout name |
launchpad | string | Active launchpad name (empty if none) |
viewer_count | integer | Number of viewer slots in the current layout |
projected_viewer | integer or null | Index of the viewer currently projected fullscreen, or null if not projecting |
version | string | Application version |
Sources
Section titled “Sources”List all NDI® sources currently visible on the network.
GET /api/sourcesResponse:
{ "sources": [ { "name": "CAMERA-1 (Studio)", "url": "ndi://192.168.1.10/CAMERA-1" }, { "name": "GRAPHICS (Control Room)", "url": "ndi://192.168.1.20/GRAPHICS" } ]}| Field | Type | Description |
|---|---|---|
name | string | NDI® source display name |
url | string | NDI® source URL |
Viewers
Section titled “Viewers”List viewer assignments
Section titled “List viewer assignments”GET /api/viewersResponse:
{ "viewers": [ { "index": 0, "source": "CAMERA-1 (Studio)", "connected": true, "tally": "program", "caption": "{source} LIVE", "ptz_supported": true, "resolution": "1920x1080", "fps": 59.94 }, { "index": 1, "source": null, "connected": false, "tally": "none", "caption": null, "ptz_supported": false, "resolution": null, "fps": null } ]}| Field | Type | Description |
|---|---|---|
index | integer | Zero-based viewer slot index |
source | string or null | Assigned source name, or null if empty |
connected | boolean | Whether the viewer is actively receiving frames |
tally | string | Tally state: "program", "preview", or "none" |
caption | string or null | Caption expression for this viewer, or null if not set |
ptz_supported | boolean | Whether the assigned source supports PTZ control |
resolution | string or null | Current frame resolution (e.g. "1920x1080"), or null if not connected |
fps | number or null | Current frame rate, or null if not connected |
Assign a source to a viewer
Section titled “Assign a source to a viewer”POST /api/viewers/{index}Request body:
{ "source": "CAMERA-1 (Studio)"}The source value must match a name from /api/sources.
Responses:
| Status | Body | When |
|---|---|---|
200 | { "ok": true } | Source assigned |
400 | { "ok": false, "error": "Viewer index out of range" } | Invalid index or missing source |
404 | { "ok": false, "error": "Source not found" } | Source name not found on network |
Clear a viewer
Section titled “Clear a viewer”DELETE /api/viewers/{index}Responses:
| Status | Body | When |
|---|---|---|
200 | { "ok": true } | Viewer cleared |
400 | { "ok": false, "error": "..." } | Invalid viewer index |
Layout
Section titled “Layout”Switch to a different layout.
POST /api/layoutRequest body:
{ "name": "2x2"}The name can be a built-in layout or a custom layout name.
Responses:
| Status | Body | When |
|---|---|---|
200 | { "ok": true } | Layout switched |
404 | { "ok": false, "error": "Layout not found" } | Layout name not found |
Launchpads
Section titled “Launchpads”List launchpads
Section titled “List launchpads”GET /api/launchpadsResponse:
{ "launchpads": [ { "name": "Studio A", "layout": "2x2", "active": true }, { "name": "Studio B", "layout": "1+5", "active": false } ]}| Field | Type | Description |
|---|---|---|
name | string | Launchpad preset name |
layout | string | Layout used by this launchpad |
active | boolean | Whether this is the currently active launchpad |
Switch to a launchpad
Section titled “Switch to a launchpad”POST /api/launchpadRequest body:
{ "name": "Studio B"}Responses:
| Status | Body | When |
|---|---|---|
200 | { "ok": true } | Launchpad activated |
404 | { "ok": false, "error": "Launchpad not found" } | Launchpad name not found |
Layouts
Section titled “Layouts”List all available layouts, including both built-in presets and custom layouts.
GET /api/layoutsResponse:
{ "layouts": [ { "name": "2x2", "viewers": 4, "custom": false }, { "name": "My Custom Layout", "viewers": 6, "custom": true } ]}| Field | Type | Description |
|---|---|---|
name | string | Layout name |
viewers | integer | Number of viewer slots in this layout |
custom | boolean | true for user-created layouts, false for built-in presets |
Captions
Section titled “Captions”Set or clear a caption overlay on a specific viewer. Captions support expression variables that are evaluated in real time.
Set or clear a viewer caption
Section titled “Set or clear a viewer caption”POST /api/viewers/{index}/captionRequest body (set a caption):
{ "caption": "{source} LIVE"}Request body (clear a caption):
{ "caption": null}Expression variables:
| Variable | Description |
|---|---|
{source} | NDI® source name assigned to the viewer |
{viewer} | Viewer slot index |
{time} | Current time |
{date} | Current date |
{tally} | Current tally state (program, preview, or none) |
{fps} | Current frame rate |
{res} | Current resolution |
Responses:
| Status | Body | When |
|---|---|---|
200 | { "ok": true } | Caption updated |
400 | { "ok": false, "error": "Viewer index out of range" } | Invalid viewer index |
Projection
Section titled “Projection”Project a single viewer fullscreen, or exit back to the multiviewer layout. The currently projected viewer is also reported in the projected_viewer field of the /api/status response.
Project a viewer fullscreen
Section titled “Project a viewer fullscreen”POST /api/project/{index}Responses:
| Status | Body | When |
|---|---|---|
200 | { "ok": true } | Viewer projected |
400 | { "ok": false, "error": "Viewer index out of range" } | Invalid viewer index |
Exit projection
Section titled “Exit projection”DELETE /api/projectResponses:
| Status | Body | When |
|---|---|---|
200 | { "ok": true } | Returned to multiviewer layout |
Output
Section titled “Output”Get or set the NDI® output state. When enabled, Laika sends the multiviewer as an NDI® source that other receivers on the network can pick up.
Get output status
Section titled “Get output status”GET /api/outputResponse:
{ "enabled": true, "fps": 60, "name": "LAIKA"}| Field | Type | Description |
|---|---|---|
enabled | boolean | Whether NDI® output is active |
fps | integer | Output frame rate |
name | string | NDI® source name advertised on the network |
Toggle output
Section titled “Toggle output”POST /api/outputRequest body:
{ "enabled": true}Responses:
| Status | Body | When |
|---|---|---|
200 | { "ok": true } | Output state updated |
PTZ Control
Section titled “PTZ Control”Send PTZ (Pan/Tilt/Zoom) commands to NDI® sources that support camera control. Check the ptz_supported field in the /api/viewers response to determine which viewers accept PTZ commands.
Send a PTZ command
Section titled “Send a PTZ command”POST /api/ptzRequest body:
{ "viewer": 0, "command": "pan_tilt_speed", "pan_speed": 0.5, "tilt_speed": 0.0}| Field | Type | Description |
|---|---|---|
viewer | integer | Zero-based viewer slot index |
command | string | PTZ command name (see table below) |
Commands and parameters:
| Command | Parameters | Description |
|---|---|---|
pan_tilt_speed | pan_speed (float), tilt_speed (float) | Continuous pan/tilt at the given speed. Range -1.0 to 1.0. Send 0.0 to stop. |
pan_tilt | pan (float), tilt (float) | Move to an absolute pan/tilt position |
zoom_speed | zoom_speed (float) | Continuous zoom at the given speed. Range -1.0 to 1.0. Send 0.0 to stop. |
zoom | zoom (float) | Move to an absolute zoom position |
recall_preset | index (integer), speed (float, optional) | Recall a stored camera preset. Speed defaults to 1.0. |
store_preset | index (integer) | Store the current position as a preset |
focus_auto | (none) | Trigger auto-focus |
white_balance_auto | (none) | Trigger auto white balance |
Responses:
| Status | Body | When |
|---|---|---|
200 | { "ok": true } | Command sent |
400 | { "ok": false, "error": "..." } | Invalid viewer index, missing command, or unknown command |
Router
Section titled “Router”The router creates virtual NDI® sources that act as redirect endpoints. When a receiver connects to a routed source, it is transparently redirected to the target NDI® source with zero additional bandwidth on the sender side. This makes it possible to build flexible routing topologies without duplicating video streams.
List routes
Section titled “List routes”GET /api/routerResponse:
{ "routes": [ { "index": 0, "name": "Output 1", "target": "CAMERA-1 (Studio)", "connections": 2 }, { "index": 1, "name": "Output 2", "target": null, "connections": 0 } ]}| Field | Type | Description |
|---|---|---|
index | integer | Zero-based route index |
name | string | Route display name (advertised as an NDI® source) |
target | string or null | Currently routed NDI® source name, or null if unrouted |
connections | integer | Number of NDI® receivers currently connected to this route |
Create a route
Section titled “Create a route”POST /api/routerRequest body:
{ "name": "Output 1"}Responses:
| Status | Body | When |
|---|---|---|
200 | { "ok": true, "index": 0 } | Route created (index returned) |
500 | { "ok": false, "error": "..." } | Creation failed |
Delete a route
Section titled “Delete a route”DELETE /api/router/{index}Responses:
| Status | Body | When |
|---|---|---|
200 | { "ok": true } | Route deleted |
400 | { "ok": false, "error": "..." } | Invalid route index |
Set a route target
Section titled “Set a route target”Point a route at an NDI® source. Receivers connecting to the route will be transparently redirected to this source.
POST /api/router/{index}/routeRequest body:
{ "source": "CAMERA-1 (Studio)"}The source value must match a name from /api/sources.
Responses:
| Status | Body | When |
|---|---|---|
200 | { "ok": true } | Route target set |
400 | { "ok": false, "error": "..." } | Invalid route index |
404 | { "ok": false, "error": "Source not found" } | Source name not found on network |
Clear a route target
Section titled “Clear a route target”Remove the target from a route. Connected receivers will lose their stream.
DELETE /api/router/{index}/routeResponses:
| Status | Body | When |
|---|---|---|
200 | { "ok": true } | Route target cleared |
400 | { "ok": false, "error": "..." } | Invalid route index |
Error format
Section titled “Error format”All error responses follow the same shape:
{ "ok": false, "error": "Human-readable error message"}Example: curl
Section titled “Example: curl”# Check what sources are availablecurl http://localhost:1928/api/sources
# Assign CAMERA-2 to viewer slot 0curl -X POST http://localhost:1928/api/viewers/0 \ -H "Content-Type: application/json" \ -d '{"source": "CAMERA-2 (Studio)"}'
# Switch to a 4-up layoutcurl -X POST http://localhost:1928/api/layout \ -H "Content-Type: application/json" \ -d '{"name": "2x2"}'
# Load the Studio B launchpadcurl -X POST http://localhost:1928/api/launchpad \ -H "Content-Type: application/json" \ -d '{"name": "Studio B"}'
# Set a caption on viewer 0curl -X POST http://localhost:1928/api/viewers/0/caption \ -H "Content-Type: application/json" \ -d '{"caption": "{source} LIVE"}'
# Clear a captioncurl -X POST http://localhost:1928/api/viewers/0/caption \ -H "Content-Type: application/json" \ -d '{"caption": null}'
# Project viewer 0 fullscreencurl -X POST http://localhost:1928/api/project/0
# Exit projectioncurl -X DELETE http://localhost:1928/api/project
# Pan a PTZ camera to the rightcurl -X POST http://localhost:1928/api/ptz \ -H "Content-Type: application/json" \ -d '{"viewer": 0, "command": "pan_tilt_speed", "pan_speed": 0.5, "tilt_speed": 0.0}'
# Stop PTZ movementcurl -X POST http://localhost:1928/api/ptz \ -H "Content-Type: application/json" \ -d '{"viewer": 0, "command": "pan_tilt_speed", "pan_speed": 0.0, "tilt_speed": 0.0}'
# Recall PTZ preset 1curl -X POST http://localhost:1928/api/ptz \ -H "Content-Type: application/json" \ -d '{"viewer": 0, "command": "recall_preset", "index": 1}'
# Create a router outputcurl -X POST http://localhost:1928/api/router \ -H "Content-Type: application/json" \ -d '{"name": "Output 1"}'
# Route a source to the outputcurl -X POST http://localhost:1928/api/router/0/route \ -H "Content-Type: application/json" \ -d '{"source": "CAMERA-1 (Studio)"}'
# Clear a route targetcurl -X DELETE http://localhost:1928/api/router/0/route
# Delete a routecurl -X DELETE http://localhost:1928/api/router/0