Skip to content

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.

Get the current application state.

GET /api/status

Response:

{
"layout": "2x2",
"launchpad": "Studio A",
"viewer_count": 4,
"projected_viewer": null,
"version": "1.0.57"
}
FieldTypeDescription
layoutstringCurrent layout name
launchpadstringActive launchpad name (empty if none)
viewer_countintegerNumber of viewer slots in the current layout
projected_viewerinteger or nullIndex of the viewer currently projected fullscreen, or null if not projecting
versionstringApplication version

List all NDI® sources currently visible on the network.

GET /api/sources

Response:

{
"sources": [
{
"name": "CAMERA-1 (Studio)",
"url": "ndi://192.168.1.10/CAMERA-1"
},
{
"name": "GRAPHICS (Control Room)",
"url": "ndi://192.168.1.20/GRAPHICS"
}
]
}
FieldTypeDescription
namestringNDI® source display name
urlstringNDI® source URL
GET /api/viewers

Response:

{
"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
}
]
}
FieldTypeDescription
indexintegerZero-based viewer slot index
sourcestring or nullAssigned source name, or null if empty
connectedbooleanWhether the viewer is actively receiving frames
tallystringTally state: "program", "preview", or "none"
captionstring or nullCaption expression for this viewer, or null if not set
ptz_supportedbooleanWhether the assigned source supports PTZ control
resolutionstring or nullCurrent frame resolution (e.g. "1920x1080"), or null if not connected
fpsnumber or nullCurrent frame rate, or null if not connected
POST /api/viewers/{index}

Request body:

{
"source": "CAMERA-1 (Studio)"
}

The source value must match a name from /api/sources.

Responses:

StatusBodyWhen
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
DELETE /api/viewers/{index}

Responses:

StatusBodyWhen
200{ "ok": true }Viewer cleared
400{ "ok": false, "error": "..." }Invalid viewer index

Switch to a different layout.

POST /api/layout

Request body:

{
"name": "2x2"
}

The name can be a built-in layout or a custom layout name.

Responses:

StatusBodyWhen
200{ "ok": true }Layout switched
404{ "ok": false, "error": "Layout not found" }Layout name not found
GET /api/launchpads

Response:

{
"launchpads": [
{
"name": "Studio A",
"layout": "2x2",
"active": true
},
{
"name": "Studio B",
"layout": "1+5",
"active": false
}
]
}
FieldTypeDescription
namestringLaunchpad preset name
layoutstringLayout used by this launchpad
activebooleanWhether this is the currently active launchpad
POST /api/launchpad

Request body:

{
"name": "Studio B"
}

Responses:

StatusBodyWhen
200{ "ok": true }Launchpad activated
404{ "ok": false, "error": "Launchpad not found" }Launchpad name not found

List all available layouts, including both built-in presets and custom layouts.

GET /api/layouts

Response:

{
"layouts": [
{
"name": "2x2",
"viewers": 4,
"custom": false
},
{
"name": "My Custom Layout",
"viewers": 6,
"custom": true
}
]
}
FieldTypeDescription
namestringLayout name
viewersintegerNumber of viewer slots in this layout
custombooleantrue for user-created layouts, false for built-in presets

Set or clear a caption overlay on a specific viewer. Captions support expression variables that are evaluated in real time.

POST /api/viewers/{index}/caption

Request body (set a caption):

{
"caption": "{source} LIVE"
}

Request body (clear a caption):

{
"caption": null
}

Expression variables:

VariableDescription
{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:

StatusBodyWhen
200{ "ok": true }Caption updated
400{ "ok": false, "error": "Viewer index out of range" }Invalid viewer index

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.

POST /api/project/{index}

Responses:

StatusBodyWhen
200{ "ok": true }Viewer projected
400{ "ok": false, "error": "Viewer index out of range" }Invalid viewer index
DELETE /api/project

Responses:

StatusBodyWhen
200{ "ok": true }Returned to multiviewer layout

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 /api/output

Response:

{
"enabled": true,
"fps": 60,
"name": "LAIKA"
}
FieldTypeDescription
enabledbooleanWhether NDI® output is active
fpsintegerOutput frame rate
namestringNDI® source name advertised on the network
POST /api/output

Request body:

{
"enabled": true
}

Responses:

StatusBodyWhen
200{ "ok": true }Output state updated

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.

POST /api/ptz

Request body:

{
"viewer": 0,
"command": "pan_tilt_speed",
"pan_speed": 0.5,
"tilt_speed": 0.0
}
FieldTypeDescription
viewerintegerZero-based viewer slot index
commandstringPTZ command name (see table below)

Commands and parameters:

CommandParametersDescription
pan_tilt_speedpan_speed (float), tilt_speed (float)Continuous pan/tilt at the given speed. Range -1.0 to 1.0. Send 0.0 to stop.
pan_tiltpan (float), tilt (float)Move to an absolute pan/tilt position
zoom_speedzoom_speed (float)Continuous zoom at the given speed. Range -1.0 to 1.0. Send 0.0 to stop.
zoomzoom (float)Move to an absolute zoom position
recall_presetindex (integer), speed (float, optional)Recall a stored camera preset. Speed defaults to 1.0.
store_presetindex (integer)Store the current position as a preset
focus_auto(none)Trigger auto-focus
white_balance_auto(none)Trigger auto white balance

Responses:

StatusBodyWhen
200{ "ok": true }Command sent
400{ "ok": false, "error": "..." }Invalid viewer index, missing command, or unknown command

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.

GET /api/router

Response:

{
"routes": [
{
"index": 0,
"name": "Output 1",
"target": "CAMERA-1 (Studio)",
"connections": 2
},
{
"index": 1,
"name": "Output 2",
"target": null,
"connections": 0
}
]
}
FieldTypeDescription
indexintegerZero-based route index
namestringRoute display name (advertised as an NDI® source)
targetstring or nullCurrently routed NDI® source name, or null if unrouted
connectionsintegerNumber of NDI® receivers currently connected to this route
POST /api/router

Request body:

{
"name": "Output 1"
}

Responses:

StatusBodyWhen
200{ "ok": true, "index": 0 }Route created (index returned)
500{ "ok": false, "error": "..." }Creation failed
DELETE /api/router/{index}

Responses:

StatusBodyWhen
200{ "ok": true }Route deleted
400{ "ok": false, "error": "..." }Invalid route index

Point a route at an NDI® source. Receivers connecting to the route will be transparently redirected to this source.

POST /api/router/{index}/route

Request body:

{
"source": "CAMERA-1 (Studio)"
}

The source value must match a name from /api/sources.

Responses:

StatusBodyWhen
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

Remove the target from a route. Connected receivers will lose their stream.

DELETE /api/router/{index}/route

Responses:

StatusBodyWhen
200{ "ok": true }Route target cleared
400{ "ok": false, "error": "..." }Invalid route index

All error responses follow the same shape:

{
"ok": false,
"error": "Human-readable error message"
}
Terminal window
# Check what sources are available
curl http://localhost:1928/api/sources
# Assign CAMERA-2 to viewer slot 0
curl -X POST http://localhost:1928/api/viewers/0 \
-H "Content-Type: application/json" \
-d '{"source": "CAMERA-2 (Studio)"}'
# Switch to a 4-up layout
curl -X POST http://localhost:1928/api/layout \
-H "Content-Type: application/json" \
-d '{"name": "2x2"}'
# Load the Studio B launchpad
curl -X POST http://localhost:1928/api/launchpad \
-H "Content-Type: application/json" \
-d '{"name": "Studio B"}'
# Set a caption on viewer 0
curl -X POST http://localhost:1928/api/viewers/0/caption \
-H "Content-Type: application/json" \
-d '{"caption": "{source} LIVE"}'
# Clear a caption
curl -X POST http://localhost:1928/api/viewers/0/caption \
-H "Content-Type: application/json" \
-d '{"caption": null}'
# Project viewer 0 fullscreen
curl -X POST http://localhost:1928/api/project/0
# Exit projection
curl -X DELETE http://localhost:1928/api/project
# Pan a PTZ camera to the right
curl -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 movement
curl -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 1
curl -X POST http://localhost:1928/api/ptz \
-H "Content-Type: application/json" \
-d '{"viewer": 0, "command": "recall_preset", "index": 1}'
# Create a router output
curl -X POST http://localhost:1928/api/router \
-H "Content-Type: application/json" \
-d '{"name": "Output 1"}'
# Route a source to the output
curl -X POST http://localhost:1928/api/router/0/route \
-H "Content-Type: application/json" \
-d '{"source": "CAMERA-1 (Studio)"}'
# Clear a route target
curl -X DELETE http://localhost:1928/api/router/0/route
# Delete a route
curl -X DELETE http://localhost:1928/api/router/0