> ## 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.

# Authentication and LAN access

> Give each control device its own token, decide which LAN addresses need no token, and keep or retire the legacy authorization.

Every call to the Core API comes from one of these callers. The Core checks them in this order and the first match wins:

1. **This machine.** Calls from the Core machine itself (loopback) are always allowed, with full control.
2. **A device token.** A token you created for a device in VRA Cloud.
3. **Legacy authorization.** Core v1's `EXTERNAL_APP` Basic header, while **Allow legacy authorization** is on. It only opens the [legacy routes](/develop-with-vra/core-control-api/legacy-routes).
4. **LAN access.** An address the studio lets in without a token.

Anything else gets `401 unauthorized`.

A token that belongs to a revoked device, or to a device with a fixed IP address used from another address, ends the check at step 2 with `401`. It does not fall through to legacy authorization or LAN access, even when those would let the caller in without the token.

You manage all of this in VRA Cloud under **Studio settings → Core API**. The page has three sections: **Access**, **Features** and **Devices**. Changes reach the Core within moments and need no restart.

## Devices and tokens

Give every Stream Deck, Companion instance, SKAARHOJ panel or script its own device. You can then see which device did what, limit it, and revoke it without touching the others.

<Steps>
  <Step title="Add the device">
    Open **Studio settings → Core API** and click **Add device** in the
    **Devices** section.
  </Step>

  <Step title="Set what it may do">
    * **Name**: shows up in the request log, for example `Stream Deck studio desk`.
    * **Access**: **Read only** or **Read and control**.
    * **Station**: pin the device to one station. See [Station safety](/develop-with-vra/core-control-api/station-safety).
    * **Fixed IP address**: the token only works from this address.
    * **Limit to features**: leave empty for every feature, or pick the features the device may use.
  </Step>

  <Step title="Copy the token">
    VRA Cloud shows the token once, together with a ready-made **Stream Deck
    URL**, **Companion header** and **SKAARHOJ URL**. Copy what you need. The
    token is not stored, only a hash of it. Lost it? Revoke the device and add
    a new one.
  </Step>
</Steps>

A token looks like `vra_` followed by 32 lowercase letters and digits. **Revoke** stops the token at once and keeps the device in the list: the revoked token answers `401`, whatever LAN access allows. **Delete** stops the token and removes the device.

Each device in the list shows when it last called the Core, and from which address. The Core reports this to VRA Cloud about once a minute, so the time can be up to a minute behind. Without an internet connection the Core keeps the report and sends it once the cloud is back. A device that has never called shows **never**.

### Sending the token

The Core accepts the token in three ways. Use whichever your device supports.

<CodeGroup>
  ```bash Bearer header theme={null}
  curl -H "Authorization: Bearer vra_examp1etoken…" \
    http://192.168.1.50:3002/api/v2/studio/status
  ```

  ```bash Query string theme={null}
  curl "http://192.168.1.50:3002/api/v2/studio/status?token=vra_examp1etoken…"
  ```

  ```bash Basic password theme={null}
  # Any username; the token is the password.
  curl http://device:vra_examp1etoken…@192.168.1.50:3002/api/v2/studio/status
  ```
</CodeGroup>

* **Bearer header**: Companion, scripts and anything else that can send headers.
* **Query string**: the native Stream Deck **Website** action and other devices that can only open a URL. The Core never writes the token to its request log, but the URL may end up in the device's own history.
* **Basic password**: SKAARHOJ and other devices that support HTTP Basic authentication. The username is ignored.

### Access and feature limits

| Device setting | Effect |
| - | - |
| **Read only** | Reads work. Every control route answers `403 read_only`. |
| **Limit to features** | Routes of other features answer `403 forbidden`. |
| **Fixed IP address** | From any other address the token is refused with `401 unauthorized`, also when that address has LAN access. |
| **Station** | Station-scoped calls are refused while another station holds the studio. See [Station safety](/develop-with-vra/core-control-api/station-safety). |

## Access without a token

**Access without a token** in the **Access** section decides which LAN callers need no token at all.

| Option | Who gets in without a token |
| - | - |
| **Off** (default) | Nobody. Every device needs a token. |
| **Listed devices** | Callers whose address matches a listed address or range, for example `192.168.10.25` or `192.168.10.0/24`. Give each entry a label; it shows up in the request log. IPv6 addresses and ranges work too. |
| **Whole LAN** | Every private network address: `10.0.0.0/8`, `172.16.0.0/12`, `192.168.0.0/16`, link-local `169.254.0.0/16`, and IPv6 `fc00::/7` and `fe80::/10`. |

Callers let in this way get **Read and control** access to every feature that is switched on. A browser page cannot use this access, see [Browser guard](#browser-guard).

<Warning>
  **Whole LAN** lets anyone on the studio network change the Core and the studio state. Prefer device tokens, or **Listed devices** with fixed addresses for the panels that cannot send a token.
</Warning>

## Legacy authorization

Core v1 used one shared HTTP Basic header for all callers. The Core keeps accepting it while **Allow legacy authorization** is on, which is the default, so existing integrations keep working.

The legacy header is:

* username `EXTERNAL_APP`,
* password: the base64 of the Core's server id (the base64 of the studio id also works).

Click **Generate Authorization** in **Studio settings → Core Settings** to see the username, the password and the full header.

```bash theme={null}
curl -H "Authorization: Basic <base64 of EXTERNAL_APP:password>" \
  http://192.168.1.50:3002/api/state/state
```

A legacy header opens the **legacy routes only**: `/api/...`, `/status` and `/`. It never opens `/api/v2`: a v2 route called with a legacy header answers `403 forbidden`. The password is made from the server id, which `GET /` hands out to anyone, so it cannot protect the v2 routes. On the legacy routes it gets full control.

<Warning>
  The legacy rules are weak, like on Core v1. From a private network address, `EXTERNAL_APP` is accepted with any password. From a private network address, other VRA app type names, like `OUTPUT_PLAYER`, are also accepted as username without a password; from a public address they are refused. Move your devices to tokens, then switch **Allow legacy authorization** off.
</Warning>

Legacy authorization is checked before LAN access. A device on a LAN address that still sends the `EXTERNAL_APP` header is identified as a legacy caller, so its `/api/v2` calls answer `403 forbidden` even when the studio has LAN access. Remove the header, or replace it with a device token, before you move such a device to `/api/v2`.

## Browser guard

A web page open in a browser on the Core machine or on the studio LAN could otherwise fire Core API calls, for example an image with the source `http://192.168.1.50:3002/api/v2/studio/offair`. Every action is a GET, so the browser would send it without asking.

For callers that are let in by their address only, the Core therefore refuses requests that a browser marks as coming from another site:

* the request carries `Sec-Fetch-Site: cross-site` or `Sec-Fetch-Site: same-site`, or
* the request carries an `Origin` header that is not the Core's own address, including `Origin: null`.

Such a request answers `403 forbidden` (`Forbidden` on the legacy routes). This applies to calls from the Core machine itself (loopback) and to calls let in through **Access without a token**.

* Stream Deck, Companion, SKAARHOJ, monitoring tools and `curl` send neither header and are not affected.
* The reference page the Core serves at `/api/v2/docs` is on the Core's own address, so its calls pass.
* Calls with a device token are not checked: the token already shows the caller is meant to be there. A web tool that must call the Core needs its own device token.

## Refused calls

| Situation | Status | `/api/v2` body | `/api/v2/plain` body | Legacy `/api/...` body |
| - | - | - | - | - |
| No valid token, legacy header or LAN access | `401` | `{"ok":false,"error":"unauthorized",…}` | `ERR unauthorized` | `Unauthorized` |
| Revoked device token, or a token with a fixed IP address used from another address | `401` | `error: unauthorized` | `ERR unauthorized` | `Unauthorized` |
| Read-only device on a control route | `403` | `error: read_only` | `ERR read_only` | `Forbidden` |
| Device not allowed this feature | `403` | `error: forbidden` | `ERR forbidden` | `Forbidden` when the device may not use Legacy routes |
| Legacy header on a `/api/v2` route | `403` | `error: forbidden` | `ERR forbidden` | allowed |
| Browser request from another site, from a loopback or LAN-access caller | `403` | `error: forbidden` | `ERR forbidden` | `Forbidden` |
| Feature switched off, or API switched off | `403` | `error: feature_disabled` | `ERR feature_disabled` | `Forbidden` |

Every `401` carries `WWW-Authenticate: Basic realm="VRASERVERCOREAPI"`, so a browser shows its login prompt. Type any username and the token as the password.

```json theme={null}
{"ok":false,"error":"unauthorized","message":"authentication required: device token, LAN access or legacy authorization"}
```

See [Errors and status codes](/develop-with-vra/core-control-api/errors-and-status-codes) for the full list.

## Switching the API or features off

* **API enabled** in the **Access** section switches the whole API off for the studio. `/api/v2` routes then answer `403 feature_disabled` and legacy routes answer `403 Forbidden`.
* The **Features** section has a **Read** and a **Control** switch per feature: System, Core, Studio, Station, Signals, Macros, Automations, Outputs, Cameras, Variables and Legacy routes. A switched-off feature answers `403 feature_disabled` and disappears from `GET /api/v2`, `openapi.json` and `/api/v2/docs`.

## No authentication needed

These routes answer without a token: `GET /api/v2/health`, `GET /api/v2/openapi.json`, `GET /api/v2/docs`, and the legacy `GET /status`, `GET /status/health` and `GET /`. They stay up when the API is switched off. Switching off **Read** for the System feature also closes `/api/v2/health`.

## Finding out why a call failed

`GET /api/v2/requests` lists the last 100 calls, newest first: time, address, caller, method, path, status and error code. The caller shows how the Core identified it: `loopback`, `device:<name>`, `legacy:EXTERNAL_APP` or `lan:<label>`. Tokens in the path are replaced with `***`.

```bash theme={null}
curl http://localhost:3002/api/v2/requests
```

Run it on the Core machine, where no token is needed, or with a device that may read the System feature.


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