> ## Documentation Index
> Fetch the complete documentation index at: https://docs.visualradioassist.live/llms.txt
> Use this file to discover all available pages before exploring further.

# Routes

> Every Core API v2 route per feature: method, path, the plain-text value and the JSON body.

All routes live under `http://{core}:3002/api/v2`. Every route also has a plain-text twin under `/api/v2/plain/...` that answers one flat value. See [Plain-text routes](/develop-with-vra/core-control-api/plain-text-routes).

The examples on this page use `http://192.168.1.50:3002` as the Core address and `vra_examp1etoken…` as a placeholder device token. See [Authentication and LAN access](/develop-with-vra/core-control-api/authentication-and-lan-access) for the ways to send a token.

## Conventions

* **Reads** answer `GET`.
* **Control routes** answer `GET` and `POST` on the same path. Arguments go in the path or the query string, never in a request body, so a control route works from any device that can open a URL.
* **Control routes are idempotent.** `/on` on a signal that is already on, or `/onair` on a studio that is already on air, answers `ok:true` without applying anything again. Prefer explicit `/on` and `/off` over `/toggle` on devices that cannot show the current state.
* **JSON bodies** are compact, use snake\_case keys and keep `null` values. Every body carries `ok`. Lists sit under `items`.
* **Lookups** by id are exact. Macro and automation names are matched case-insensitively. Signal identifiers are matched case-insensitively too (an exact id or identifier match wins first). Output slot keys are matched case-insensitively, output ids exactly. Cameras are found by id, by name (case-insensitively) or by number; angles and variables by id or by name (case-insensitively).
* Responses carry `Cache-Control: no-store`.

### Query parameters

| Parameter | Applies to | Effect |
| - | - | - |
| `?wait=0` | Control routes | Answer `202` at once with `"pending":true` instead of waiting for the acknowledgement. `false` and `no` work too. |
| `?timeout=<ms>` | Control routes | How long to wait for the acknowledgement, 100–30000 ms. Without it the Core waits up to 5 s. Signal writes and studio changes give up after 2 s on their own. Output commands wait 2 s for the player by default. |
| `?station=<id or name>` | Any route | Refuse with `409 station_mismatch` when another station holds the studio. See [Station safety](/develop-with-vra/core-control-api/station-safety). |
| `?token=<token>` | Any route | A device token, for devices that cannot send headers. |

A control route answers with the state read after the action. With `?wait=0` the body shows the state at the moment the call was accepted, so it can still be the old state.

Arguments are checked before the Core accepts the call, also with `?wait=0`: an unknown signal, a disabled or `OUTPUT` signal, an unknown output, camera, angle or variable, or `/on` on a variable that is not BOOLEAN answers its error straight away instead of `202`.

When a call ends in `504 timeout`, an action that is still waiting its turn inside the Core is dropped, so a device that retries after the `504` does not apply it twice. An action that had already started completes.

## System

Studio scope. Reads only.

| Method | Path | Plain value | JSON body |
| - | - | - | - |
| GET | `/api/v2` | Core version | `version`, `server`, `studio`, `station`, `core`, `api`, `features`, `principal`, `endpoints` |
| GET | `/api/v2/state` | `rev` | One snapshot for pollers, see below |
| GET | `/api/v2/health` | `ok` | `healthy`, `state`, `active`, `studio_id`, `version`. No authentication needed. |
| GET | `/api/v2/clients` | Number of active apps | `items`: `[{type, id, hostname, active}]` |
| GET | `/api/v2/requests` | Number of entries | `items`: the last 100 requests, newest first |
| GET | `/api/v2/openapi.json` | none | The OpenAPI document. No authentication needed. |
| GET | `/api/v2/docs` | none | The HTML reference. No authentication needed. |

`GET /api/v2` describes the API as the caller sees it: which features are on, how the caller authenticated (`principal`) and every endpoint with its methods, path, plain path and parameters.

### State snapshot

`GET /api/v2/state` is the one call to poll when you need several values at once. `rev` is a fingerprint of the snapshot's content, not a counter: it changes when anything in the snapshot changes and stays the same while nothing does. Compare it with the last value you saw and fetch the details only when it differs. Do not compare it for larger or smaller.

`rev` fits in 53 bits, so JavaScript, Companion and browsers hold it exactly. Comparing it as text, for example from the plain twin `/api/v2/plain/state`, works too.

```json theme={null}
{"rev":4821630915738204,"core":{"state":"ACTIVE","active":true},"studio":{"id":"b7e1…","name":"Studio 1","status":"onair","active":true,"state":1},"station":{"id":"4c2a…","name":"Radio 1"},"program":["Morning Show"],"signals":{"items":[{"id":"9f10…","identifier":"audio-director","label":"Audio Director","direction":"INPUT","value_type":"BOOLEAN","enabled":true,"scope":"STUDIO","value":true,"active":true,"source":null,"changed_at":1759744800000}]},"macros":{"items":[{"id":"m-01…","name":"Jingle","enabled":true,"active":false,"current_item_id":null,"last_trigger_at":null}]},"automations":{"items":[{"id":"a-01…","name":"News","enabled":true,"state":"standby"}]},"outputs":{"items":[{"output_id":"o-01…","slot":"main","status":"on_air","station_id":"4c2a…","scene":null,"variant":null,"playout":[]}]},"cameras":{"items":[{"id":"c-01…","name":"Host","number":1,"on_air":true,"preview":false}]},"variables":{"items":[{"id":"v-01…","name":"live","value":"TRUE"}]},"clients":[{"type":"OUTPUT_PLAYER","id":"c-01…","hostname":"studio-pc","active":true}],"ok":true}
```

* `program` lists the titles of the programs on air.
* `outputs` holds the same items as `/api/v2/outputs`, without `at`. `cameras` holds `{id, name, number, on_air, preview}` per camera, `variables` holds `{id, name, value}` per variable.
* A section is `null` when the caller may not read that feature, or when it is not loaded yet. For a device pinned to another station than the one on the studio, `automations`, `outputs` and `variables` are `null` too; `macros` stays, like on the macro routes.

### Health

```bash theme={null}
curl http://192.168.1.50:3002/api/v2/health
```

```json theme={null}
{"healthy":true,"state":"ACTIVE","active":true,"studio_id":"b7e1…","version":"5.1.0","ok":true}
```

`healthy` is `false` while the Core starts. Use the plain twin `/api/v2/plain/health`, which answers `ok`, for a monitoring check such as Zabbix.

## Core

The Core instance state. Studio scope.

| Method | Path | Plain value | JSON body |
| - | - | - | - |
| GET | `/api/v2/core` | `ACTIVE`, `DEACTIVATED`, `STARTING`, … | `{state, active}` |
| GET | `/api/v2/core/state` | `ACTIVE`, `DEACTIVATED`, `STARTING`, … | `{state}` |
| GET | `/api/v2/core/active` | `true` / `false` | `{active}` |
| GET, POST | `/api/v2/core/activate` | `OK` | `{state, active}` |
| GET, POST | `/api/v2/core/deactivate` | `OK` | `{state, active}` |
| GET, POST | `/api/v2/core/toggle` | `OK` | `{state, active}` |
| GET, POST | `/api/v2/core/reload-config` | `OK` | `{reloaded:true}` |
| GET, POST | `/api/v2/core/restart-apps` | `OK` | `{restarted:<number of apps>}` |

`active` is `true` only for `ACTIVE`. Toggle turns any other state into `ACTIVE`. While the Core is `STARTING`, activate, deactivate and toggle answer `503 not_ready`.

`reload-config` pulls the Core configuration from the cloud again. `restart-apps` restarts the VRA apps the Core manages on this machine.

```bash theme={null}
curl -H "Authorization: Bearer vra_examp1etoken…" http://192.168.1.50:3002/api/v2/core/activate
```

```json theme={null}
{"state":"ACTIVE","active":true,"ok":true}
```

## Studio

The studio's on-air status. Studio scope.

| Method | Path | Plain value | JSON body |
| - | - | - | - |
| GET | `/api/v2/studio` | `onair`, `recording` or `offair` | `{id, name, status, active, state}` |
| GET | `/api/v2/studio/status` | `onair`, `recording` or `offair` | `{status}` |
| GET, POST | `/api/v2/studio/onair` | `OK` | `{status, active, state}` |
| GET, POST | `/api/v2/studio/recording` | `OK` | `{status, active, state}` |
| GET, POST | `/api/v2/studio/offair` | `OK` | `{status, active, state}` |
| GET, POST | `/api/v2/studio/toggle` | `OK` | `{status, active, state}` |
| GET, POST | `/api/v2/studio/set?value=` | `OK` | `{status, active, state}` |

* `state` is the number behind the status: `0` off air, `1` on air, `2` recording.
* `active` is `true` for on air and for recording.
* Toggle: off air goes on air, on air or recording goes off air.
* `/studio/set` takes `value=onair`, `recording` or `offair`. It also accepts `on`, `off`, `0`, `1`, `2`, `true` and `false`. Anything else answers `400 invalid_args`.
* While the Core is `STARTING`, studio control answers `503 not_ready`.

```bash theme={null}
curl -H "Authorization: Bearer vra_examp1etoken…" http://192.168.1.50:3002/api/v2/studio/onair
```

```json theme={null}
{"status":"onair","active":true,"state":1,"ok":true}
```

## Station

The station currently on the studio and its program schedule. Studio scope. Reads only.

| Method | Path | Plain value | JSON body |
| - | - | - | - |
| GET | `/api/v2/station` | Station name | `{id, name}` |
| GET | `/api/v2/station/program` | Title of the program on air, empty when none | `{onair:[…], upcoming:[…], previous}` |
| GET | `/api/v2/station/program/title` | Title of the program on air, empty when none | `{title}` |

Each program is `{id, reference_id, title, start, end, duration_ms, hosts:[{id, name}]}`, with `start` and `end` in ISO 8601 UTC. `previous` is the program that was on air last, or `null`. The program routes answer `503 not_ready` until the schedule is loaded.

```json theme={null}
{"onair":[{"id":"p-01…","reference_id":null,"title":"Morning Show","start":"2026-10-06T06:00:00.000Z","end":"2026-10-06T09:00:00.000Z","duration_ms":10800000,"hosts":[{"id":"h-01…","name":"Sam"}]}],"upcoming":[],"previous":null,"ok":true}
```

## Signals

The studio's Signals, read from the Core's live copy and written through the cloud. Studio scope.

`{id}` is the signal id or its identifier.

| Method | Path | Plain value | JSON body |
| - | - | - | - |
| GET | `/api/v2/signals` | Number of signals that are on | `items`: every signal |
| GET | `/api/v2/signals/{id}` | `1` when on, else `0` | One signal |
| GET | `/api/v2/signals/{id}/value` | `1`/`0` for BOOLEAN, the number for INTEGER, the text for STRING | `{id, identifier, value_type, value}` |
| GET, POST | `/api/v2/signals/{id}/on` | `OK` | `{id, identifier, active, value}` |
| GET, POST | `/api/v2/signals/{id}/off` | `OK` | `{id, identifier, active, value}` |
| GET, POST | `/api/v2/signals/{id}/toggle` | `OK` | `{id, identifier, active, value}` |
| GET, POST | `/api/v2/signals/{id}/set?value=` | `OK` | `{id, identifier, active, value}` |

One signal in the list, and the body of `GET /api/v2/signals/{id}`:

```json theme={null}
{"id":"9f10…","identifier":"audio-director","label":"Audio Director","direction":"INPUT","value_type":"BOOLEAN","enabled":true,"scope":"STUDIO","value":true,"active":true,"source":null,"changed_at":1759744800000,"ok":true}
```

`changed_at` is in epoch milliseconds. `active` is `true` when the value is `true`, a number other than 0, or a non-empty text.

### Writing a signal

* `/on`, `/off` and `/toggle` work on BOOLEAN and INTEGER signals. INTEGER signals get `1` or `0`. Other types answer `400 invalid_args`.
* `/set?value=` takes `true`/`false` (also `1`/`0`, `on`/`off`, `yes`/`no`) for BOOLEAN, a whole number for INTEGER and any text for STRING.
* Only enabled `INPUT` signals can be written. A disabled signal answers `409 signal_disabled`, an `OUTPUT` signal answers `403 read_only`.
* The Core waits until its copy of the signal shows the new value. No confirmation within 2 s answers `504 timeout`. Without a studio MQTT connection the write answers `503 not_ready`.

### Audio Director and Camera Assist

The two system signals have short routes. They behave like `/api/v2/signals/audio-director/...` and `/api/v2/signals/camera-assist/...`.

| Method | Path | Plain value |
| - | - | - |
| GET | `/api/v2/audio-director` | `1` when on, else `0` |
| GET | `/api/v2/audio-director/value` | `1` / `0` |
| GET, POST | `/api/v2/audio-director/on` | `OK` |
| GET, POST | `/api/v2/audio-director/off` | `OK` |
| GET, POST | `/api/v2/audio-director/toggle` | `OK` |
| GET | `/api/v2/camera-assist` | `1` when on, else `0` |
| GET | `/api/v2/camera-assist/value` | `1` / `0` |
| GET, POST | `/api/v2/camera-assist/on` | `OK` |
| GET, POST | `/api/v2/camera-assist/off` | `OK` |
| GET, POST | `/api/v2/camera-assist/toggle` | `OK` |

A studio without the system signal answers `404 not_found`.

## Macros

The [Macros](/macros) of the station on the studio, as the Core has loaded them. Studio scope: the [station guard](/develop-with-vra/core-control-api/station-safety) does not apply to these routes, a device pinned to another station reaches them too, and the answers carry no `station` block. A `?station=` you add yourself is still checked, like on every route.

`{id}` is the macro id or its name. Names with spaces need URL encoding, for example `Jingle%20in`.

| Method | Path | Plain value | JSON body |
| - | - | - | - |
| GET | `/api/v2/macros` | Number of macros | `items`: `[{id, name, enabled, active, current_item_id, last_trigger_at}]` |
| GET | `/api/v2/macros/{id}` | `1` while it runs, else `0` | One macro |
| GET | `/api/v2/macros/{id}/active` | `1` while it runs, else `0` | `{id, active}` |
| GET, POST | `/api/v2/macros/{id}/trigger` | `OK` | `{id, name, triggered:true}` |

`last_trigger_at` is in epoch milliseconds, or `null` when the macro has not run. The macro routes answer `503 not_ready` until the Core has loaded the station's macros.

```bash theme={null}
curl -H "Authorization: Bearer vra_examp1etoken…" "http://192.168.1.50:3002/api/v2/macros/Jingle%20in/trigger"
```

```json theme={null}
{"id":"m-01…","name":"Jingle in","triggered":true,"ok":true}
```

The trigger starts the macro and answers straight away. It does not wait for the macro to finish. `?wait=0` only changes the status code to `202`.

## Automations

The [Automations](/automations) of the station on the studio. Station scope. Reads only.

`{id}` is the automation id or its name.

| Method | Path | Plain value | JSON body |
| - | - | - | - |
| GET | `/api/v2/automations` | Number of automations | `items`: `[{id, name, enabled, state}]` |
| GET | `/api/v2/automations/{id}` | State | One automation |
| GET | `/api/v2/automations/{id}/state` | State | `{id, state}` |

The state is `standby`, `activated`, `released` or `stopped`.

```json theme={null}
{"id":"a-01…","state":"activated","ok":true,"station":{"id":"4c2a…","name":"Radio 1"}}
```

## Outputs

The Output Players (v2) of the studio, as they report themselves to the Core, and the commands they take. Station scope: these routes go through the [station guard](/develop-with-vra/core-control-api/station-safety) and every successful answer carries `station: {id, name}`. Output players of the previous generation are not covered.

`{slot}` is the output id or the slot key of the output, for example `main`. Slot keys are matched case-insensitively.

### Reading outputs

| Method | Path | Plain value | JSON body |
| - | - | - | - |
| GET | `/api/v2/outputs` | Number of outputs on air | `items`: one entry per output, see below |
| GET | `/api/v2/outputs/{slot}` | Status | One output |
| GET | `/api/v2/outputs/{slot}/status` | Status | `{output_id, slot, status, at}` |
| GET | `/api/v2/outputs/{slot}/scene` | Key of the scene on air, empty when none | `{output_id, slot, scene}` |
| GET | `/api/v2/outputs/{slot}/scenes` | Number of scenes | `{output_id, slot, items}` |
| GET | `/api/v2/outputs/{slot}/playout` | Phase of the first playout scene, empty when none | `{output_id, slot, playout}` |

One output, as in `items` and the body of `GET /api/v2/outputs/{slot}`:

```json theme={null}
{"output_id":"o-01…","slot":"main","status":"on_air","station_id":"4c2a…","scene":{"key":"weather","name":"Weather","since_at":1759744800000,"cause":"trigger:weather"},"variant":null,"playout":[{"scene":"news-playout","phase":"standby","bank":{"key":"news","name":"News","program_id":null},"clip":null,"rundown":{"key":"r-01…","program_id":"p-01…","next_item":"1.1","on_air_item":null}}],"at":1759744812000,"ok":true,"station":{"id":"4c2a…","name":"Radio 1"}}
```

* `status` is `starting`, `on_air`, `dormant`, `fault` or `offline`. `offline` means the player's last report is older than 15 seconds.
* `scene` is the switched scene on air, or `null`. `variant` is `{key, cause}` or `null`.
* `playout` lists the output's playout scenes. `phase` is `standby`, `taking`, `on_air`, `returning` or `holding`. `bank` is the active clip bank, `clip` the clip on air `{key, label, number}`, `rundown` the rundown with the item marked next and the item on air.
* `at`, `since_at`: epoch milliseconds.
* `/scenes` lists the scenes the output's report names: `{key, name, kind, on_air}`, with `kind` `switched`, `base` or `playout`. The full scene list of a show is in the show itself, not in the API.
* `GET /api/v2/outputs` with `?station=` lists only the outputs of that station.
* An output that no player reports answers `404 not_found`. Without the studio's MQTT connection, or when the Core's MQTT access carries no output topics, the output routes answer `503 not_ready`.

### Controlling an output

Every control route sends one command to the output's player and waits for its acknowledgement, 2 s by default (`?timeout=` changes it). The table names the player action each route sends.

| Method | Path | Player action | Effect |
| - | - | - | - |
| GET, POST | `/api/v2/outputs/{slot}/scenes/{scene}/take` | `scene.take` | Put the scene on air as if its trigger fired. `?hold=1` holds it on air, `?hold=0` releases it. |
| GET, POST | `/api/v2/outputs/{slot}/rundown/next` | `rundown.next` | Take the next rundown item. |
| GET, POST | `/api/v2/outputs/{slot}/rundown/previous` | `rundown.previous` | Take the item before the one on air. |
| GET, POST | `/api/v2/outputs/{slot}/rundown/take?item=` | `rundown.take` | Take the item `<story>.<item>`, for example `item=2.1`. Without `item`, the next item. |
| GET, POST | `/api/v2/outputs/{slot}/rundown/set-next?item=` | `rundown.set_next` | Mark the item that `rundown/next` takes next. Takes nothing. `item` is required. |
| GET, POST | `/api/v2/outputs/{slot}/clips/{number}/play` | `clip.play` | Play a clip of the active bank by its clip number. A clip key works too. `?bank=<key>` picks another bank. |
| GET, POST | `/api/v2/outputs/{slot}/graphics/take-out-all` | `graphic.take_out_all` | Take out every graphic of the playout scene. The clip keeps playing. |
| GET, POST | `/api/v2/outputs/{slot}/reload` | `player.reload` | Reload the player: fetch the show again and start a fresh graphics page. |

`?hold=` takes `1`/`0`, `true`/`false`, `on`/`off` or `yes`/`no`.

The rundown, clip and graphics routes act on one playout scene. Name it with `?scene=<key>`. Without `?scene=`, the Core uses the output's playout scene when it has exactly one. An output with several playout scenes answers `400 invalid_args` and lists them in `scenes`; an output without one answers `404 not_found`.

```bash theme={null}
curl -H "Authorization: Bearer vra_examp1etoken…" \
  http://192.168.1.50:3002/api/v2/outputs/main/scenes/weather/take
```

```json theme={null}
{"output_id":"o-01…","slot":"main","action":"scene.take","ack":{"ok":true,"applied_tick":89481600050,"error":null},"ok":true,"station":{"id":"4c2a…","name":"Radio 1"}}
```

`ack` is the player's answer: `ok`, `applied_tick` (the tick of the player's clock the command took effect on, or `null`) and the player's own error code in `error`. With `?wait=0` the call answers `202` with `"ack":null` and `"pending":true`.

A refused command keeps the player's answer in `ack` and maps its error code onto the API's:

```json theme={null}
{"output_id":"o-01…","slot":"main","action":"rundown.next","ack":{"ok":false,"applied_tick":null,"error":"no_next_item"},"ok":false,"error":"not_found","message":"the player refused rundown.next: no_next_item"}
```

| Player error (`ack.error`) | Status | API error |
| - | - | - |
| `unknown_target`, `unknown_scene`, `unknown_clip`, `unknown_item`, `no_rundown`, `no_clip_bank`, `no_next_item`, `no_previous_item` | `404` | `not_found` |
| `not_on_air`, `rejected_by_limit`, `not_a_playout`, `not_allowed_here` | `409` | `not_active` |
| `invalid_args` | `400` | `invalid_args` |
| `expired` | `504` | `timeout` |
| Any other code | `500` | `error` |
| No answer within the timeout | `504` | `timeout`, with `"ack":null` |

No answer usually means that no player plays the output: it is dormant or offline. A `504` does not withdraw the command, so a player that answers late can still apply it. Read `/api/v2/outputs/{slot}/status` before you send the command again.

Besides the station guard, `?station=` is also held against the station of the output itself: an output that still plays another station's show answers `409 station_mismatch`, with `expected` and `output_station_id`.

## Cameras

The studio's cameras and their angles, with a tally, and the commands that cut to them. Studio scope.

`{id}` is the camera id, its name or its number. Names are matched case-insensitively; a name that two cameras share answers `400 invalid_args`, use the id or the number then. The number is the camera's 1-based position in `/api/v2/cameras`.

### Reading cameras

| Method | Path | Plain value | JSON body |
| - | - | - | - |
| GET | `/api/v2/cameras` | Number of the camera on air, empty when none | `items`: one entry per camera, see below |
| GET | `/api/v2/cameras/{id}` | `1` while on air, `0` when not, empty when unknown | One camera |
| GET | `/api/v2/cameras/{id}/angles` | Number of angles | `{camera:{id, name}, items:[{id, name}]}` |
| GET | `/api/v2/cameras/{id}/on-air` | `1` while on air, `0` when not, empty when unknown | `{id, on_air, preview}` |
| GET | `/api/v2/angles/{id}` | Angle name | `{id, name, camera:{id, name}}` |

`/api/v2/angles/{id}` takes the angle id, or an angle name that only one camera has. Within one camera, use `/api/v2/cameras/{id}/angles/{angle}/trigger`.

One camera, as in `items` and the body of `GET /api/v2/cameras/{id}`:

```json theme={null}
{"id":"c-02…","name":"Guest","number":2,"angles":[{"id":"g-01…","name":"Guest close"},{"id":"g-02…","name":"Guest wide"}],"core":{"action":"SWITCHER","value":"2","switcher_id":null},"on_air":true,"preview":false,"thumb":null,"ok":true}
```

* `core` is the switcher cut the camera stands for: the switcher input in `value`.
* `on_air` and `preview` are the tally: `true` while the camera's input is on the program or preview bus of the live switcher. They are `null` when the Core cannot tell: the camera's cut is not a switcher cut, no switcher matches, or the switcher is offline. Keyers and picture-in-picture count as on air on switchers that report them.
* An angle has no tally of its own. It cuts to its camera's input.
* The camera routes answer `503 not_ready` until the Core has loaded the camera list.

### Controlling a camera

The Core does not move cameras itself. Every control route sends a `CAMERA` commando, which Camera Assist executes, and waits for Camera Assist's acknowledgement, 5 s by default (`?timeout=` changes it).

| Method | Path | Effect |
| - | - | - |
| GET, POST | `/api/v2/cameras/{id}/cut` | Cut the camera to program: its switcher cut. |
| GET, POST | `/api/v2/cameras/{id}/preview` | Take the camera to preview. |
| GET, POST | `/api/v2/angles/{id}/trigger` | Run the angle: the PTZ move, framing and the cut. |
| GET, POST | `/api/v2/cameras/{id}/angles/{angle}/trigger` | Run one of the camera's angles. `{angle}` is the angle id or its name within the camera. |

Camera Assist acknowledges as soon as it found the camera or angle, before the PTZ move and framing are done. The answer reports that acknowledgement, not the end of the move.

```bash theme={null}
curl -H "Authorization: Bearer vra_examp1etoken…" \
  http://192.168.1.50:3002/api/v2/cameras/2/cut
```

```json theme={null}
{"camera":{"id":"c-02…","name":"Guest"},"angle":null,"action":"cut","ack":{"ok":true,"reason":"Camera handled successfully"},"ok":true}
```

`action` is `cut`, `preview` or `trigger`; `angle` is set for a trigger. `ack` is Camera Assist's answer: `ok` and its own `reason`. With `?wait=0` the call answers `202` with `"ack":null` and `"pending":true`.

A refused command keeps Camera Assist's answer in `ack`:

| Camera Assist's answer | Status | API error |
| - | - | - |
| The camera or angle was not found | `404` | `not_found` |
| Blocked: the studio is off air or the Core is not `ACTIVE` | `409` | `not_active` |
| No answer within the timeout | `504` | `timeout` |
| Any other refusal | `500` | `error` |

No answer usually means Camera Assist is not running. Read `/api/v2/cameras/{id}/on-air` before you send the command again. While the Core starts, before it can send commandos, the control routes answer `503 not_ready`.

Moving a camera to an angle without cutting to it, like **Go to base position** in VRA Cloud, is not available through the API yet.

## Variables

The variables of the station on the studio, as the cloud publishes them to the Core. Station scope: these routes go through the [station guard](/develop-with-vra/core-control-api/station-safety) and every successful answer carries `station: {id, name}`.

`{name}` is the variable id or its name. Names are matched case-insensitively.

### Reading variables

| Method | Path | Plain value | JSON body |
| - | - | - | - |
| GET | `/api/v2/variables` | Number of variables | `items`: `[{id, name, display_name, data_type, value}]` |
| GET | `/api/v2/variables/{name}` | The value | One variable |
| GET | `/api/v2/variables/{name}/value` | The value | `{id, name, value}` |

```json theme={null}
{"id":"v-01…","name":"live","display_name":"Live","data_type":"BOOLEAN","value":"TRUE","ok":true,"station":{"id":"4c2a…","name":"Radio 1"}}
```

* `value` is the stored text, or `null` when the variable has no value. A BOOLEAN variable stores `TRUE` or `FALSE`.
* The plain value is the text, empty when there is no value. BOOLEAN variables answer `1` or `0`.
* The Core reads the values the cloud publishes for the station. Without the studio's MQTT connection, or before the first values arrived, the variable routes answer `503 not_ready`.

### Writing a variable

| Method | Path | Effect |
| - | - | - |
| GET, POST | `/api/v2/variables/{name}/set?value=` | Set the value. `value` is required. |
| GET, POST | `/api/v2/variables/{name}/on` | Set a BOOLEAN variable to `TRUE`. |
| GET, POST | `/api/v2/variables/{name}/off` | Set a BOOLEAN variable to `FALSE`. |
| GET, POST | `/api/v2/variables/{name}/toggle` | Flip a BOOLEAN variable. A variable without a value counts as `FALSE`. |

The Core writes variables through VRA Cloud, which stays the owner of the values. A write therefore needs the Core's internet connection, also when the panel is on the studio network.

* Only the station's own **user input** variables of type TEXT, LONG\_TEXT, BOOLEAN, NUMBER or OPTIONS can be written. Other variables, including variables shared by every station of the broadcaster, answer `403 read_only`.
* `/on`, `/off` and `/toggle` only work on BOOLEAN variables. Other types answer `400 invalid_args`; use `/set?value=`.
* The cloud checks the value against the variable's type:

| Type | `value` | Stored as |
| - | - | - |
| BOOLEAN | `true`/`false`, `1`/`0`, `on`/`off` or `yes`/`no`, in any case | `TRUE` or `FALSE` |
| NUMBER | A decimal number, for example `12`, `-3.5` or `.5` | The number, without a leading `+` |
| OPTIONS | One of the variable's options: its value, or its label in any case | The option's value |
| TEXT, LONG\_TEXT | Any text | As is |

A value can be at most 255 characters, or the character limit set on a text variable. A value that does not fit answers `400 invalid_args`.

```bash theme={null}
curl -H "Authorization: Bearer vra_examp1etoken…" \
  http://192.168.1.50:3002/api/v2/variables/live/on
```

```json theme={null}
{"id":"v-01…","name":"live","value":"TRUE","confirmed":true,"ok":true,"station":{"id":"4c2a…","name":"Radio 1"}}
```

`value` is the value as the cloud stored it. `confirmed` is `true` when the Core's copy showed the new value within 2 s. A write the cloud accepted answers `ok:true` also when `confirmed` is `false`; the value then arrives a moment later. With `?wait=0` the call answers `202` with `"pending":true` and the value as sent.

| Refusal | Status | API error |
| - | - | - |
| No variable with that id or name on the station | `404` | `not_found` |
| Not a writable variable | `403` | `read_only` |
| The value does not fit the type, options or length; `/on` on a variable that is not BOOLEAN; no `?value=` | `400` | `invalid_args` |
| Another station took the studio in the meantime | `409` | `station_mismatch` |
| The cloud is unreachable, or the studio has no station | `503` | `not_ready` |
| The cloud did not answer in time | `504` | `timeout` |

The Core sends the station it is on with every write, and the cloud refuses the write when another station holds the studio by then. See [Station safety](/develop-with-vra/core-control-api/station-safety).

## Legacy routes

The routes of Core v1, under `/api/...`, are listed on [Legacy routes](/develop-with-vra/core-control-api/legacy-routes).


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.