---
source: "https://docs.lumora.live/advanced/server-api"
url: /advanced/server-api.md
description: >-
  Lumora's Server API: trigger graphics over HTTP from Stream Deck, Companion,
  or automation scripts using a workspace API key. Covers endpoints, parameters,
  examples, and rate limits.
---

# Server API

Lumora exposes an HTTP API for triggering graphics from external tools. Stream Deck via Bitfocus Companion, automation scripts, vision-mixer macros, anything that can fire an HTTP request. Requests are authenticated with a long-lived API key minted from the control panel.

::: tip Client API
If you're running the Lumora Controller application on your Windows PC, you also have access to the **Client API**, documented [here](/advanced/client-api).
:::

## Authentication

Every `/api/*` endpoint requires an API key on the standard `Authorization` header:

```http
Authorization: Bearer lumora_live_sk_<43 chars>
```

Keys are long-lived workspace credentials. Stream Deck, Companion, and CI scripts store one once and reuse it indefinitely.

### Minting a key

1. In the control panel, open Settings → API Keys.
2. Give it a label (e.g. "Stream Deck") then click **Create key**
3. The raw key is shown **once**. Copy it into your automation tool immediately, as you can't retrieve it again.

API keys are available on **Standard**, **Professional**, and **Enterprise** plans. The Free plan does not include API access. Paid plans can mint as many keys as needed.

### Key format

| Part | Length | Detail |
|---|---|---|
| Prefix | 15 chars | `lumora_live_sk_` |
| Body | 43 chars | Base64URL random |
| **Total** | **58 chars** | 256 bits of entropy |

### Scope and safety

* Keys can call any `/api/*` endpoint within the workspace they were minted in.
* Keys **only** work against their own workspace. Using a key from one workspace against a different workspace's URL returns `403`.
* Keys **cannot** mint or revoke other keys. Only a signed-in human operator can manage keys via the modal.
* A leaked key's blast radius is bounded to `/api/*` and can be cut off with one click from Settings → API Keys → **Revoke**.
* Treat keys like passwords: don't commit them to git, don't paste them in screenshots, don't share them between workspaces.

## Endpoints

All endpoints are rooted at `https://app.lumora.live/w/<workspace-id>/`. Your workspace ID is the part of your workspace's URL right after `/w/`. The tables below show paths relative to that root (e.g. `/api/v1/trigger/graphicId/status` means `https://app.lumora.live/w/<workspace-id>/api/v1/trigger/graphicId/status`).

::: tip How graphics route to channels
Trigger endpoints don't take a channel parameter. Each graphic carries a **channel lock** (the set of output channels it's assigned to), and the trigger fires on those channels automatically. Set the lock from the rundown's right-click **Channel ▸** submenu, or via the matching control in the control panel. Status endpoints still take a `channelId` because they ask a per-channel question ("what's live on cg1?").
:::

### `GET` Requests

| Endpoint | Description |
|---|---|
| `/api/v1/trigger/graphicId/status` | Per-graphic status. Returns: `{ "live": true/false, "liveOn": ["cg1", ...] }` |
| `/api/v1/status/channelId` | List all graphics currently live on a channel. Returns: `{ "live": [...] }` |
| `/api/v1/status/channelId/graphicId` | Check if a specific graphic is currently live on a channel. Returns: `{ "live": true/false }` |
| `/api/v1/tally/channelId/pgm` | Returns `true` if the channel is on program, `false` if not |
| `/api/v1/tally/channelId/pvw` | Returns `true` if the channel is on preview, `false` if not |

### `POST` Requests

| Endpoint | Description |
|---|---|
| `/api/v1/trigger/graphicId/on` | Take a graphic live. Fires on every channel in its lock set that is compatible with the graphic's resolution. Returns: `{ "ok": true, "dispatched": ["cg1", ...], "skipped": [...] }` |
| `/api/v1/trigger/graphicId/off` | Take a graphic off air on every channel it's currently live on. Returns: `{ "ok": true, "dispatched": ["cg1", ...] }` |
| `/api/v1/trigger/graphicId/action/actionName` | Fire a named action on a live graphic (e.g. `Advance`, `Reset`). The graphic must be live somewhere. |
| `/api/v1/refresh-all` | Refresh all data sources (Google Sheets, etc.). Same as pressing F9 in the control panel. Every open control panel shows the refresh and its result. Local files are re-read by any open desktop controller app. |

::: tip Push live JSON
For pushing live JSON payloads (esports stats, score tickers, anything that updates faster than a polling cadence allows), see the [Data API](/advanced/data-api). Same authentication, sibling versioned namespace at `/api/v1/data/<slug>`.
:::

## Parameters

| Name | Where to find it |
|---|---|
| `graphicId` | Unique graphic instance ID. Right-click the graphic in the rundown → **Copy Graphic ID**. |
| `channelId` | Output channel ID, e.g. `cg1`. Found in Outputs → Configure Outputs. Used by `/status/...` and `/tally/...` only. Trigger endpoints don't take a `channelId`. |
| `actionName` | Action name as defined in the template, e.g. `Advance`. **Case-sensitive.** |

## Responses

### Success

`POST /trigger/{graphicId}/on` returns the list of channels it actually fired on (`dispatched`) plus anything it had to skip (incompatible channel, channel no longer exists):

```json
{ "ok": true, "dispatched": ["cg1", "cg2"], "skipped": [] }
```

`POST /trigger/{graphicId}/off` returns the channels it cleared:

```json
{ "ok": true, "dispatched": ["cg1", "cg2"] }
```

`POST /trigger/{graphicId}/action/{actionName}` returns:

```json
{ "ok": true }
```

`POST /refresh-all` returns how many data sources it covered and how many failed. `ok` is `false` when any source failed, including a local file that no open desktop controller app re-read during the refresh. When the workspace has local files, the call waits a few seconds for the desktop controller apps to re-read them:

```json
{ "ok": true, "count": 3, "failed": 0, "sheets": { "ok": true, "count": 3, "failed": 0 }, "stats": null }
```

`GET /trigger/{graphicId}/status` returns:

```json
{ "live": true, "liveOn": ["cg1", "cg2"] }
```

### Error

```json
{ "error": "No channels assigned to this graphic.", "reason": "no_channels_assigned" }
```

### Status codes

| Code | Meaning |
|---|---|
| **200** | Success |
| **401** | Missing or invalid token |
| **403** | Workspace ID in the URL doesn't match API key |
| **404** | Channel, graphic, action, or template not found |
| **409** | Trigger conflict. `reason: "no_channels_assigned"` (graphic has no channels in its lock set), `reason: "locked_channel_missing"` (every channel in the lock set has been deleted from the workspace), or "Graphic is not live" on action endpoints. |

## Examples

### Read channel status

```bash
curl -H "Authorization: Bearer lumora_live_sk_<your key>" \
  https://app.lumora.live/w/your-workspace-id/api/v1/status/cg1
```

### Take a graphic on-air

```bash
curl -X POST \
  -H "Authorization: Bearer lumora_live_sk_<your key>" \
  https://app.lumora.live/w/your-workspace-id/api/v1/trigger/<graphicId>/on
```

### Take a graphic off air

```bash
curl -X POST \
  -H "Authorization: Bearer lumora_live_sk_<your key>" \
  https://app.lumora.live/w/your-workspace-id/api/v1/trigger/<graphicId>/off
```

### Fire a custom action

```bash
curl -X POST \
  -H "Authorization: Bearer lumora_live_sk_<your key>" \
  https://app.lumora.live/w/your-workspace-id/api/v1/trigger/<graphicId>/action/Advance
```

### Refresh all data sources

```bash
curl -X POST \
  -H "Authorization: Bearer lumora_live_sk_<your key>" \
  https://app.lumora.live/w/your-workspace-id/api/v1/refresh-all
```

## Common integrations

There's no Lumora-specific plugin for any of these. They all drive the HTTP endpoints above directly. Configure each tool's generic HTTP action with your endpoint URL, set the method (`GET` or `POST`), and add the `Authorization: Bearer ...` header.

* **Stream Deck.** Elgato's built-in **HTTP Request** action (or the **Web Requests** plugin on older firmware) fires a request per button press. One button per endpoint is the typical setup: a "take on" button and a "take off" button per graphic you control regularly. Each graphic carries its own channel lock, so the same button always fires it on the same channels, so you don't need a per-channel button strip.
* **Bitfocus Companion.** Drop these endpoints into Companion's generic HTTP action; the `Authorization` header is set per-button or globally in connection config.
* **OBS / vMix macros.** Most switchers can fire an HTTP POST as part of a macro; use the trigger endpoints with the relevant `graphicId`.
* **Custom scripts.** Any language with an HTTP client works. The endpoints are stateless, so retry logic is straightforward.

## Rate limits

API requests are rate-limited per workspace across two dimensions: a 60-second burst ceiling and a rolling 24-hour daily ceiling. Both apply on every request; whichever runs out first triggers a `429 Too Many Requests` with a `Retry-After` header.

| Plan | Burst (per minute) | Daily |
|---|---|---|
| Standard | 100 | 10,000 |
| Professional | 300 | 30,000 |
| Enterprise | Set per account, contact us | Set per account, contact us |

Stream Deck / Companion typical usage (a handful of triggers per minute) is well within these limits. They exist to protect the workspace from runaway scripts.

Every response carries the standard `RateLimit-Limit`, `RateLimit-Remaining`, and `RateLimit-Reset` headers so you can pace yourself without waiting for a 429.

## Troubleshooting

**`401 Unauthorized`.** The `Authorization` header is missing, malformed, or the key has been revoked. Re-mint a key and update your automation tool. Check for accidental whitespace before/after the key when pasting.

**`404 Not Found`.** Usually the wrong `graphicId` or `channelId`. The graphic ID is shown via the rundown right-click → **Copy Graphic ID**. The channel ID is the short ID (e.g. `cg1`), not the channel name. Find it in Outputs → Configure Outputs.

**`409 Conflict` with `reason: "no_channels_assigned"`.** The graphic has no channels in its lock set. Right-click the graphic in the rundown and pick a channel from the **Channel ▸** submenu, then re-fire.

**`409 Conflict` with `reason: "locked_channel_missing"`.** Every channel in the graphic's lock set has been deleted from the workspace. Either reassign the graphic to a current channel or restore one of the missing channels in Outputs → Configure Outputs.

**`409 Conflict` on an action endpoint.** The graphic isn't currently live anywhere. Custom actions can only fire while the graphic is on-air. Take it on first, then fire the action.

**Triggers fire on some channels but skip others.** The response's `skipped` array lists why: `incompatible_channel` (channel's resolution doesn't match the graphic's `renderRequirements`; a frame-rate-only mismatch never skips a channel) or `channel_missing` (a locked channel was deleted but others survive).
