# API overview The Airframe API runs your graphics from anywhere: a Stream Deck through Companion, a script, a scoreboard feed or your own app. ## How it's shaped Each **output** plays one **composition**, at a **version** you choose. The API has two kinds of call: - **Resources** you read and change: an output's state, its composition and the instances prepared for it. These are ordinary REST endpoints. - **Commands** that act on an output: put a component on air, take it off, change its data, play an event or run a timer. You send them in a batch to [Run commands](https://docs.airframe.tv/api/run-commands), or over the [control WebSocket](https://docs.airframe.tv/api/websocket). ```json { "commands": [ { "command": "update", "scope": "root", "data": { "homeScore": 3 } }, { "command": "in", "component": "score-bug" } ] } ``` ## Base URL Every endpoint lives under `/v1` on the API's host, for example `https://api.airframe.tv/v1`. Requests and responses are JSON. ## Ids and keys Commands name components, layers, events, timers and instances by **id**. Data is named by each field's **key**, such as `homeScore`. You'll find ids in the editor: select a layer or component and its ID is at the top of the properties panel, with a copy button. Ids are yours to change, so give the ones you'll script against readable names like `lower-third` before you publish. ## Authentication Send a workspace API key as a bearer token. Keys are made by owners and admins in **Organisation settings › API keys**, and each one controls every output or just the ones you choose. See [Authentication](https://docs.airframe.tv/api/authentication). ```sh curl https://api.airframe.tv/v1/outputs/OUTPUT_ID/state \ -H "Authorization: Bearer $AIRFRAME_KEY" ``` ## OpenAPI The API describes itself with OpenAPI 3.1 at `/v1/openapi.json`, which you can feed to a client generator, and you can try requests in the browser at `/v1/docs`. ## For AI tools Building an integration with an AI assistant? Give it the docs as Markdown: - [`/llms.txt`](https://docs.airframe.tv/llms.txt) indexes every page, following [llmstxt.org](https://llmstxt.org). - [`/llms-full.txt`](https://docs.airframe.tv/llms-full.txt) is every page in one file, API reference first. - **Copy page** at the top of any page copies it as Markdown, examples included. --- # Authentication API keys belong to the workspace. Each has a name and controls every output, or only the outputs you choose. ## Make a key 1. In Airframe, open **Organisation settings** and find **API keys**. Owners and admins manage keys, and they come with the Solo and Studio plans. 2. Choose **New key**, name it after what will use it, for example `Companion in the gallery`. 3. Choose what it controls: **All outputs**, including ones made later, or **Chosen outputs**. 4. Copy the key. It's shown once. If you lose it, revoke it and make another. Keys look like `afk_…`. Older output tokens, which look like `afo_…`, still work the same way. ## Send it Send the key as a bearer token on every request: ```http Authorization: Bearer afk_… ``` Browsers can't set headers on a WebSocket, so the [control WebSocket](https://docs.airframe.tv/api/websocket) also takes it as `?token=`. > **Treat keys like passwords.** Anyone with a key can run the outputs it controls. Keep keys out of front-end code and version control, and revoke a key straight away if it leaks. ## What a key can do A key runs the show on the outputs it controls, the same as an operator in the control room: - Read an output's state and the composition it plays. - Run [commands](https://docs.airframe.tv/api/commands), over HTTP or the [control WebSocket](https://docs.airframe.tv/api/websocket). - List, create, change and remove [instances](https://docs.airframe.tv/api/list-instances). It can't change what the show is. Editing and publishing compositions, and choosing which version an output plays, happen in Airframe. ## Change or revoke a key Choose **Edit** on a key to rename it or change the outputs it controls; changes apply on its next request. **Revoke** stops it working at once. Each key shows when it was last used, so you can spot ones nothing uses any more. --- # Errors and rate limits Errors come back as JSON with a readable message and a stable code to branch on. ```json { "error": "This API key doesn't control this output", "code": "forbidden" } ``` ## Error codes | Status | Code | What happened | | --- | --- | --- | | 400 | `invalid_request` | The body or path doesn't match the schema. The message says which field is wrong. | | 401 | `unauthorized` | No API key, or the key is wrong or revoked. | | 403 | `forbidden` | The key doesn't control this output. | | 404 | `not_found` | No such output or instance, or it isn't in your workspace. | | 409 | `default_instance` | A component's default instance can't be removed. | | 422 | `command_failed` | A command in a batch couldn't run, so none did. See below. | | 429 | `rate_limited` | Too many commands. Wait the number of seconds in `Retry-After`. | ## Failed commands A batch runs all or nothing. If any command can't run, nothing changes on air, and the response says which command failed by its position in the batch, counting from 0: ```json { "error": "No component lower-thrid", "code": "command_failed", "index": 1 } ``` Common reasons: an id that isn't in the version the output plays, an instance that belongs to another component, sending both `data` and `instance` on `in`, or an output with no composition yet. ## Rate limits API keys can send 200 command batches every 10 seconds each. Past that, [Run commands](https://docs.airframe.tv/api/run-commands) answers `429` with a `Retry-After` header. Put many changes in one batch rather than sending them one by one: a batch of up to 100 commands counts once. --- # Commands Commands are how you run an output: `in`, `out`, `update`, `invoke` and `timer`. Send them in a batch to [Run commands](https://docs.airframe.tv/api/run-commands) or over the [control WebSocket](https://docs.airframe.tv/api/websocket). Each command is an object whose `command` says what it does. A batch runs in order, under one revision, and all or nothing: if one command can't run, none do. ```json { "commands": [ { "command": "update", "component": "lower-third", "data": { "name": "Jane Doe" } }, { "command": "in", "component": "lower-third" } ] } ``` Components, layers, events, timers and instances are named by **id**. Data is named by each field's **key**. A value of `null` clears a key, so the field goes back to what it inherits or its default. ## in Puts a component on air, playing its In. - `component` (string, required): The component's id. - `data` (object): Values to merge into the component's data first. The component keeps them after it goes off air. - `instance` (string): An [instance](https://docs.airframe.tv/api/list-instances) of the component to show. Its data replaces the component's data. Send `data` or `instance`, not both. ```json { "command": "in", "component": "lower-third", "instance": "4c1d…" } ``` If the component is already showing, `in` doesn't play In again. It just takes the new data, exactly as `update` would, and a change to one of its trigger fields plays Change. ## out Takes things off air, playing their Out. By default it acts on one component; `scope` widens it to a layer or everything. - `scope` ("component" | "layer" | "root"): Defaults to `"component"`. - `component` (string): With the component scope: the component's id. - `layer` (string): With `scope: "layer"`: the layer's id. Every component on it goes off air. ```json { "command": "out", "component": "lower-third" } { "command": "out", "scope": "layer", "layer": "sponsors" } { "command": "out", "scope": "root" } ``` `scope: "root"` clears the output: everything goes off air. What comes off air keeps its data. ## update Merges values into data without changing what's on air. On a component that's showing, a change to one of its trigger fields plays its Change. - `scope` ("component" | "layer" | "root"): Defaults to `"component"`. `"layer"` updates a layer's data and `"root"` the composition's global data, which fields inherit from. - `component` (string): With the component scope: the component's id. - `layer` (string): With `scope: "layer"`: the layer's id. - `data` (object, required): Values by field key: strings, numbers, booleans, or `null` to clear. ```json { "command": "update", "scope": "root", "data": { "homeScore": 3, "awayScore": 1 } } ``` ## invoke Plays one of a component's events, such as a custom `Goal` event. - `component` (string, required): The component's id. - `event` (string, required): The event's id. ```json { "command": "invoke", "component": "score-bug", "event": "goal" } ``` ## timer Starts, pauses, resets or sets a countdown or count-up field. Clocks and countdowns to a time follow the wall clock, so they can't be started or stopped. - `timer` (string, required): The timer field's id. - `action` ("start" | "pause" | "reset" | "set", required): `reset` goes back to the timer's starting time. `set` moves it to the time you give. - `ms` (number): For `set`: the time in milliseconds. - `time` (string): For `set`: the time as text instead, such as `"5:00"`. ```json { "command": "timer", "timer": "clock", "action": "set", "time": "45:00" } { "command": "timer", "timer": "clock", "action": "start" } ``` The output runs every timer on its own clock, so every renderer shows the same time. A countdown can fire an event on a component when it reaches zero; set that up on the timer field in the editor. --- # Run commands `POST /v1/outputs/{id}/commands` Runs a batch of [commands](https://docs.airframe.tv/api/commands) on an output, in order. They all run under one revision, and all or nothing: if one can't run, nothing changes on air. ## Path parameters - `id` (string, required): The output's id. It's the last part of the output's browser source URL. ## Body - `commands` (Command[], required): 1 to 100 [commands](https://docs.airframe.tv/api/commands): `in`, `out`, `update`, `invoke` or `timer`. ## Response - `revision` (integer): The output's revision after the batch. It goes up with every change, so you can tell which state came after which. ## Errors | Status | Code | When | | --- | --- | --- | | 400 | `invalid_request` | A command isn't one of the shapes above, or the batch is empty or over 100. | | 403 | `forbidden` | The key doesn't control this output. | | 404 | `not_found` | No such output. | | 422 | `command_failed` | A command couldn't run. `index` says which. Nothing changed. | | 429 | `rate_limited` | Too many batches from this key. Retry after `Retry-After` seconds. | ## Example request ```sh curl -X POST https://api.airframe.tv/v1/outputs/OUTPUT_ID/commands \ -H "Authorization: Bearer $AIRFRAME_KEY" \ -H "Content-Type: application/json" \ -d '{ "commands": [ { "command": "update", "component": "lower-third", "data": { "name": "Jane Doe", "title": "Head coach" } }, { "command": "in", "component": "lower-third" }, { "command": "timer", "timer": "match-clock", "action": "start" } ] }' ``` ### 200 OK ```json { "revision": 42 } ``` ### 422 Failed ```json { "error": "No component lower-thrid", "code": "command_failed", "index": 1 } ``` --- # Get the output's state `GET /v1/outputs/{id}/state` What's on an output now: what's on air, every value it's been given and how its timers read. ## Path parameters - `id` (string, required): The output's id. ## Response - `revision` (integer): Goes up with every change to the output. - `active` (object | null): The version the output plays: `compositionId`, `versionId` and `versionNumber`. Null until it's given a composition. - `onAir` (object): Layers and components by id: `true` on air, `false` off. - `data` (object): The values sent so far, as `global`, `layers` by layer id and `components` by component id. Each holds values by field key. Fields not listed show their defaults. - `timers` (object): Every timer field by id, with its `text` as it reads now and whether it's `running`. - `liveInstances` (object): Which instance each component on air is showing, by component id. To follow changes as they happen, rather than asking again and again, open the [control WebSocket](https://docs.airframe.tv/api/websocket). ## Example request ```sh curl https://api.airframe.tv/v1/outputs/OUTPUT_ID/state \ -H "Authorization: Bearer $AIRFRAME_KEY" ``` ### 200 OK ```json { "revision": 42, "active": { "compositionId": "c_9b2f…", "versionId": "v_51ad…", "versionNumber": 4 }, "onAir": { "lower-third": true, "score-bug": true, "sponsors": false }, "data": { "global": { "homeScore": 3, "awayScore": 1 }, "layers": {}, "components": { "lower-third": { "name": "Jane Doe", "title": "Head coach" } } }, "timers": { "match-clock": { "text": "45:00", "running": true } }, "liveInstances": { "lower-third": "4c1d…" } } ``` --- # Control WebSocket `WS /v1/outputs/{id}/control/ws` A live connection to an output. It tells you everything that changes as it happens, and takes the same [commands](https://docs.airframe.tv/api/commands) as [Run commands](https://docs.airframe.tv/api/run-commands). ## Connect Open a WebSocket to the path above, on `wss://`. Browsers can't set headers on a WebSocket, so pass the API key as `?token=`. From the server side, an `Authorization: Bearer` header works too. ## What it sends - `state` (message): First, the output's whole state, then again whenever the output loads another version. `now` is the output's clock in milliseconds, which timers are measured against. - `patch` (message): Each change after that: `revision` and the `update`, holding only what changed, such as `onAir`, `components` data or `timers`. Apply them in order on top of the state. - `show` (message): The version the output plays and its instances, on connect and whenever they change. - `ack` (message): A batch you sent ran. Carries your batch's `id` and the new `revision`. - `error` (message): A batch didn't run, or a message wasn't understood. Carries your batch's `id` and the failed command's `index` where there is one. ## What you send ```json { "type": "commands", "id": "take-1", "commands": [ … ] } ``` `id` is optional, and comes back on the `ack` or `error` so you can match them up. Batches over the socket follow the same rules as over HTTP: in order, all or nothing. > **Reconnect when it drops.** Networks blip. When the socket closes, wait a moment and connect again: the first message is the whole state, so you're back in step straight away. ## Example request ```js const ws = new WebSocket( `wss://api.airframe.tv/v1/outputs/${outputId}/control/ws?token=${key}`, ); ws.addEventListener("message", (event) => { const message = JSON.parse(event.data); if (message.type === "state") console.log("Now", message.state); if (message.type === "patch") console.log("Changed", message.update); if (message.type === "ack") console.log("Ran batch", message.id); if (message.type === "error") console.error(message.message); }); ws.addEventListener("open", () => { ws.send(JSON.stringify({ type: "commands", id: "take-1", commands: [{ command: "in", component: "lower-third" }], })); }); ``` ### ack ```json { "type": "ack", "id": "take-1", "revision": 43 } ``` ### patch ```json { "type": "patch", "revision": 43, "update": { "onAir": { "lower-third": true } }, "now": 1793000000000 } ``` ### error ```json { "type": "error", "id": "take-1", "index": 0, "message": "No component lower-thrid" } ``` --- # Get the output's composition `GET /v1/outputs/{id}/composition` The composition an output is linked to, the version it plays, and the newest version published, so you can tell when there's an update. ## Path parameters - `id` (string, required): The output's id. ## Response - `composition` (object | null): The linked composition's `id` and `name`, or null if it has none yet. - `active` (object | null): The version it plays: `compositionId`, `versionId` and `versionNumber`. - `latestVersion` (integer | null): The composition's newest version number. When it's higher than `active.versionNumber`, there's a newer version to move to. ## Example request ```sh curl https://api.airframe.tv/v1/outputs/OUTPUT_ID/composition \ -H "Authorization: Bearer $AIRFRAME_KEY" ``` ### 200 OK ```json { "composition": { "id": "c_9b2f…", "name": "Matchday graphics" }, "active": { "compositionId": "c_9b2f…", "versionId": "v_51ad…", "versionNumber": 4 }, "latestVersion": 5 } ``` --- # List instances `GET /v1/outputs/{id}/instances` The instances prepared for an output. An instance is a component with saved data, such as a lower third for each guest, ready to put on air with `in`. ## Path parameters - `id` (string, required): The output's id. ## Response - `instances` (Instance[]): Each has an `id`, the `componentId` it belongs to, a `name` and its `data` by field key. - `liveInstances` (object): Which instance each component on air is showing, by component id. Every component has a **default instance**, with the id `default:` followed by the component's id and `isDefault: true`. You can change its data and take it, but not remove it. Instances belong to the output, not the version: moving to another version of the same composition keeps them. ## Example request ```sh curl https://api.airframe.tv/v1/outputs/OUTPUT_ID/instances \ -H "Authorization: Bearer $AIRFRAME_KEY" ``` ### 200 OK ```json { "instances": [ { "id": "default:lower-third", "componentId": "lower-third", "name": "Lower third", "data": {}, "isDefault": true }, { "id": "4c1d…", "componentId": "lower-third", "name": "Jane Doe, head coach", "data": { "name": "Jane Doe", "title": "Head coach" } } ], "liveInstances": { "lower-third": "4c1d…" } } ``` --- # Create an instance `POST /v1/outputs/{id}/instances` Prepares an instance of a component on an output, so it's ready to take with `in`. It shows in the control room straight away. ## Path parameters - `id` (string, required): The output's id. ## Body - `componentId` (string, required): A component in the version the output plays. - `name` (string, required): What operators see, up to 120 characters. - `data` (object): Values by field key. Defaults to none, so the component's defaults show. ## Response The new instance, with the `id` to send as `instance` on [in](https://docs.airframe.tv/api/commands#in). ## Example request ```sh curl -X POST https://api.airframe.tv/v1/outputs/OUTPUT_ID/instances \ -H "Authorization: Bearer $AIRFRAME_KEY" \ -H "Content-Type: application/json" \ -d '{ "componentId": "lower-third", "name": "Jane Doe, head coach", "data": { "name": "Jane Doe", "title": "Head coach" } }' ``` ### 201 Created ```json { "id": "4c1d…", "componentId": "lower-third", "name": "Jane Doe, head coach", "data": { "name": "Jane Doe", "title": "Head coach" } } ``` ### 404 Not found ```json { "error": "No component lower-thrid in the active version", "code": "not_found" } ``` --- # Update an instance `PATCH /v1/outputs/{id}/instances/{instance}` Renames an instance or changes its data. If the instance is on air, the graphic updates straight away. ## Path parameters - `id` (string, required): The output's id. - `instance` (string, required): The instance's id. ## Body - `name` (string): A new name, up to 120 characters. - `data` (object): Values to merge into the instance's data, by field key. `null` removes a key, so the field shows its default. ## Response The instance as it is now. ## Example request ```sh curl -X PATCH https://api.airframe.tv/v1/outputs/OUTPUT_ID/instances/4c1d… \ -H "Authorization: Bearer $AIRFRAME_KEY" \ -H "Content-Type: application/json" \ -d '{ "data": { "title": "Head coach, Northside FC" } }' ``` ### 200 OK ```json { "id": "4c1d…", "componentId": "lower-third", "name": "Jane Doe, head coach", "data": { "name": "Jane Doe", "title": "Head coach, Northside FC" } } ``` ### 404 Not found ```json { "error": "No such instance", "code": "not_found" } ``` --- # Delete an instance `DELETE /v1/outputs/{id}/instances/{instance}` Removes an instance. If it's on air, the component stays on air with its data; it just no longer shows as that instance. ## Path parameters - `id` (string, required): The output's id. - `instance` (string, required): The instance's id. ## Response `204` with no body. A component's default instance can't be removed, and answers `409`. ## Example request ```sh curl -X DELETE https://api.airframe.tv/v1/outputs/OUTPUT_ID/instances/4c1d… \ -H "Authorization: Bearer $AIRFRAME_KEY" ``` ### 204 Removed No body. ### 409 Default ```json { "error": "A component's default instance can't be removed", "code": "default_instance" } ``` --- # What Airframe is Airframe is for live motion graphics: you design them on a timeline in the browser, and run them on air from a control room or your own systems. ## The parts | Part | What it is | | --- | --- | | **Composition** | A set of graphics for a show, such as *Matchday graphics*. You design it in the editor. | | **Layer** | A level in the composition, stacked over the others. Holds components. | | **Component** | One graphic, such as a lower third or a score bug. It has elements, fields and events. | | **Field** | A value set at show time, such as a name or a score. Elements show fields. | | **Event** | An animation: **In** going to air, **Out** leaving it, **Change** when a field changes, or one you name. | | **Version** | A published, unchanging copy of a composition. | | **Output** | Where graphics go to air: a browser source URL for OBS, vMix or any switcher. It plays one version of one composition. | | **Instance** | A component with saved data, such as a lower third for each guest. | ## How a show runs 1. You build a composition in the editor, and **publish** it as a version. 2. You make an **output**, link it to the composition and choose the version it plays. 3. Your switcher loads the output's URL as a transparent browser source. 4. In the output's **control room**, or from the [API](https://docs.airframe.tv/api), you put components on air, take them off and change their data. The output plays the right animations, and every switcher showing it stays in step. Editing the composition never changes what's on air. When you're happy with changes, publish a new version and move the output to it. ## Where next - [Your first graphic on air](https://docs.airframe.tv/guides/first-graphic) walks through it start to finish. - [The API overview](https://docs.airframe.tv/api) shows how to drive outputs from your own code. --- # Your first graphic on air From an empty workspace to a lower third on air in OBS, in about ten minutes. ## 1. Build a composition 1. In Airframe, choose **New composition** and name it after the show. 2. Add a component for a lower third. Draw a box and two text elements on it. 3. Add two fields, `name` and `title`, and bind each text element to one. 4. Animate its **In** and **Out** on the timeline, or ask Assist to. ## 2. Publish it Choose **Publish** in the editor, with a note like `First version`. That saves it as version 1, which never changes. ## 3. Add an output 1. Open **Outputs** and choose **New output**. 2. Choose your composition and its version. 3. Copy the **Browser source URL** and add it to OBS. [Add an output to OBS Studio](https://docs.airframe.tv/guides/obs) has the settings. ## 4. Take it 1. Open the output's **control room**. 2. Select the lower third, fill in the name and title, and press **Take**. It plays its In, on air in OBS. 3. Press **Clear** to take everything off. ## 5. Drive it from code Make an [API key](https://docs.airframe.tv/api/authentication), then send the same take as a [command](https://docs.airframe.tv/api/commands): ```sh curl -X POST https://api.airframe.tv/v1/outputs/OUTPUT_ID/commands \ -H "Authorization: Bearer $AIRFRAME_KEY" \ -H "Content-Type: application/json" \ -d '{"commands":[{"command":"in","component":"lower-third","data":{"name":"Jane Doe","title":"Head coach"}}]}' ``` --- # Layers, components and data A composition is layers of components. Each component is a graphic with its own elements, fields and events. ## Layers and components **Layers** stack: one higher in the list draws over the ones below. Put graphics that share space on the same layer, such as every lower third, so you can take a whole layer off air at once. **Components** are the graphics: a lower third, a score bug, a full-screen. Each goes on and off air on its own. Every layer and component has an **ID**, shown at the top of the properties panel with a copy button. The control room and the [API](https://docs.airframe.tv/api/commands) use it, so give the ones you'll script against readable ids like `lower-third` before you publish. ## Elements Elements are what you draw: text, rectangles, ellipses, images and groups. Each has a position, size and style, and can be bound to a field, so a text element shows the `name` field. Images take a URL, or a key from the [asset library](https://docs.airframe.tv/guides/assets). Text can use an uploaded font. ## Data Fields hold the values that change during a show. Each has a **key**, such as `homeScore`, which the API uses, and a type: text, number, true or false, colour, image or timer. Fields live in one of three places: - **Global** fields belong to the composition, for things every graphic shares, like team names. - **Layer** fields belong to a layer and the components on it. - **Component** fields belong to one component. A field can **inherit** one further out, so a lower third's `team` shows the global `homeTeam` until it's given a value of its own. ## Events Every component has three events, and you can add more: | Event | Plays when | | --- | --- | | **In** | It goes to air. | | **Out** | It comes off air. | | **Change** | One of its trigger fields changes while it's on air, such as the score. | | **Custom** | You fire it by name from the control room or with [invoke](https://docs.airframe.tv/api/commands#invoke), like `Goal`. | On the timeline, each element in an event moves with a preset or with keyframes. ## Timers A timer field shows a time that runs by itself: a **countdown**, a **count-up**, a countdown **to a time of day**, or a **clock**. Countdowns and count-ups are started, paused, reset and set from the control room or with the [timer command](https://docs.airframe.tv/api/commands#timer). A countdown can fire an event on a component when it reaches zero. --- # Versions and publishing Publishing saves a composition as a numbered version that never changes. Outputs play versions, so editing never touches what's on air. ## Publish Choose **Publish** in the editor and add a short note about what changed. The new version is numbered one after the last. Operators can't publish; everyone else can. ## Outputs Each output plays one version of one composition. Choose it on the **Outputs** page, not in the control room, so nobody switches versions mid-show by accident. When a newer version is published, the output offers it. Moving an output to another version of the **same** composition keeps what's on air, its data and its instances, and the new design takes over at once. Linking a **different** composition starts again with nothing on air. From code, [Get the output's composition](https://docs.airframe.tv/api/get-composition) tells you when a newer version is out. Publishing and moving an output to it are done in Airframe, so a script can't change the show's design mid-broadcast. --- # Assets: images and fonts The asset library holds your workspace's files: images, fonts, video, audio and data. Sort them into folders and use them in any composition by key. ## Upload Open **Assets** and drop files in, or choose **Upload**. Make folders with **+ Folder**, such as `crests` or `fonts`. Files can be up to 100 MB each. | Kind | Files | | --- | --- | | Images | PNG, JPEG, WebP, GIF, AVIF, SVG | | Fonts | WOFF2, WOFF, TTF, OTF | | Video | MP4, WebM, MOV | | Audio | WAV, MP3, OGG | | Data | CSV, JSON | Uploads come with a paid plan, and count towards its storage. ## Keys Every file has a **key**: its folder path and name, such as `crests/arsenal.png`. Use the key wherever an image or font goes: - An image element's source takes a key or a URL. Pick a file from the asset library, or type a key. - A text element's font list includes your uploaded fonts. Keys are looked up live. Replace `crests/arsenal.png` with a new file of the same name and every graphic using it picks up the new one, on air within a minute, without publishing again. ## Keys in data Image fields take keys too, so data can choose a file: send `"crest": "crests/arsenal.png"` in an [update](https://docs.airframe.tv/api/commands#update) and the graphic shows it. > **Name files for the data that picks them.** If your feed sends team codes, name crests `crests/ars.png`, `crests/che.png` and so on, and build the key from the code. --- # Add an output to OBS Studio Put Airframe graphics into OBS as a transparent browser source. It takes about five minutes, and you only do it once per output. > **Before you start.** You need a published version of a composition. Owners, admins and editors create outputs; operators can run one once it exists. ## Create the output 1. In Airframe, open **Outputs** and choose **New output**. 2. Name it after where it goes on air, for example `Gallery PGM`. 3. Choose the composition and the version it plays. You can move it to another version later from the same page. ## Copy the browser source URL Each output has one URL, shown as **Browser source URL** with a copy button. It stays the same for the life of the output, whatever version it plays. ## Add a browser source in OBS 1. In the scene that should show graphics, open **Sources**, choose **+**, then **Browser**. 2. Name it, for example `Airframe · Gallery PGM`, and paste the URL. 3. Set these properties, then choose **OK**. | Property | Set to | Why | | --- | --- | --- | | Width and Height | The composition's size, e.g. `1920 × 1080` | So nothing is scaled. | | Use custom frame rate | On, at your canvas's frame rate | So animations stay smooth. | | Custom CSS | Leave the default | It keeps the page background transparent. | | Shutdown source when not visible | Off | Keeps graphics, timers and data live when you cut away. | | Refresh browser when scene becomes active | Off | A refresh would replay graphics that are already on air. | > **Keep the source in one place.** Put it in a nested scene, or share one source between scenes, rather than adding a copy to each scene. Every copy is its own renderer, and each one plays the animations again. ## Check it works Open the output's control room, select something and press **Take**. It plays its In in OBS. Press **Clear** to take it off. ## Other switchers The same URL works anywhere that shows a web page with transparency: - **vMix**: add a **Web Browser** input with the URL, at your production's resolution. - **TriCaster**: add the URL as a browser input. - **Anything else**: open the URL full screen in a browser and capture the window over a key, or over a solid background for a check. ## Troubleshooting **The graphics have a black or white background.** The Custom CSS was changed or cleared. Restore the default, which keeps the page transparent. **Graphics replay when I switch scenes.** Turn off **Refresh browser when scene becomes active**. **Animations look choppy.** Turn on **Use custom frame rate** and match it to OBS's own, under **Settings › Video**. **Nothing shows.** Check the output has a composition on the Outputs page, that something is on air in its control room, and that the computer running OBS can reach the internet. --- # The control room Each output has a control room for running it live: put graphics on air, change their data, run timers and play events. Open it from the output on the **Outputs** page. The top bar shows the composition and version it plays, and whether the control room is connected. ## Take and Clear The list on the left holds every component and its instances. Select one, then: - **Take** puts it on air with that instance's data, playing its In. If the component is already on air, it just takes the new data, playing Change where a trigger field moves. - **Clear** takes everything off air, playing each component's Out. The **Program** monitor shows what's on air now. ## Instances An instance is a component with saved data, such as a lower third for each guest, prepared before the show so a take is one press. Choose **+ Instance** to add one. Every component has a default instance, which you can edit but not remove. Changing the data of an instance that's on air updates the graphic straight away. Instances belong to the output, so they stay when the output moves to a newer version of its composition. From code, see [instances](https://docs.airframe.tv/api/list-instances). ## Data and timers Global and layer data, shared by every component that inherits it, can be changed while graphics are on air. Timer fields have start, pause, reset and set controls, and run on the output's own clock so every switcher shows the same time. ## Keyboard shortcuts | Keys | Does | | --- | --- | | ↑ ↓ | Select the previous or next instance | | Enter | Take the selected instance | | Shift + Esc | Clear everything. Shift as well, so a stray Escape never clears air. | Shortcuts don't fire while you're typing in a field. ## When it's offline If the control room loses its connection, the status says **Offline** and Take and Clear are disabled until it's back. What's on air carries on: outputs keep playing without the control room. --- # Build a control panel in Retool Build your own control panel in Retool: buttons that put graphics on air, forms that change their data, and a live view of what's showing. > **Before you start.** You need an output with a composition, and an [API key](https://docs.airframe.tv/api/authentication) that controls it. Owners and admins make keys in **Organisation settings › API keys**. ## Add Airframe as a resource A Retool resource stores the API key on Retool's servers. Your key never reaches the browsers of the people using the app. 1. In Retool, open **Resources** and choose **Create new › Resource**, then **REST API**. 2. Name it `Airframe`. 3. Set **Base URL** to `https://api.airframe.tv/v1`. 4. Under **Headers**, add `Authorization` with the value `Bearer afk_…`, using your key. 5. Choose **Create resource**. ## Send a command Make a query that puts a lower third on air with the name typed into a form. 1. In your app, add a **Text Input** named `nameInput` and a **Button** labelled `Take`. 2. In the code panel, add a **Resource query** named `takeLowerThird` that uses the `Airframe` resource. 3. Set the method to **POST**, the path to `outputs/OUTPUT_ID/commands`, and the body type to **JSON**. 4. Set the body to the command, with the name from the input: ```json { "commands": [ { "command": "in", "component": "lower-third", "data": { "name": "{{ nameInput.value }}" } } ] } ``` 5. On the button, add an event handler: on **Click**, **Control query** `takeLowerThird`. Replace `OUTPUT_ID` with the output's id, the last part of its browser source URL, and `lower-third` with your component's id. The [commands](https://docs.airframe.tv/api/commands) page lists every command you can send. ## Clear the output Make a second query, `clearAll`, the same way, with this body, and run it from a **Clear** button: ```json { "commands": [{ "command": "out", "scope": "root" }] } ``` ## Show what's on air 1. Add a query named `outputState` that sends **GET** to `outputs/OUTPUT_ID/state`. 2. Set it to run when the app loads, and in its advanced settings, to run periodically, every 2 seconds or so. 3. Show its data in components, for example a **Table** with data `{{ Object.entries(outputState.data.onAir) }}`, or text reading `{{ outputState.data.data.global.homeScore }}`. 4. Add a success handler to `takeLowerThird` and `clearAll` that triggers `outputState`, so the view updates straight after a take. [Get the output's state](https://docs.airframe.tv/api/state) describes everything it returns. ## Tips - **Batch related changes.** A score change and a goal animation can go in one request, so they land together: an `update` then an `invoke`. - **Mind the rate limit.** Keep polling to every second or two. Each key can send 200 command batches every 10 seconds; see [Errors and rate limits](https://docs.airframe.tv/api/errors). - **Show errors.** A failed command answers `422` with the reason and which command failed. Add a failure handler that shows a notification with `{{ takeLowerThird.error }}` so operators know when a take didn't happen. --- # Roles: who can do what Everyone in a workspace has one of four roles. Seats are unlimited on every plan. | | Owner | Admin | Editor | Operator | | --- | --- | --- | --- | --- | | Run outputs from the control room | Yes | Yes | Yes | Yes | | Prepare and change instances | Yes | Yes | Yes | Yes | | Build and edit compositions | Yes | Yes | Yes | | | Publish versions | Yes | Yes | Yes | | | Choose an output's composition and version | Yes | Yes | Yes | | | Manage API keys | Yes | Yes | | | | Invite people and change roles | Yes | Yes | | | | Billing | Yes | Yes | | | The **owner** is whoever made the workspace. Invite others as admins, editors or operators. **Operators** are for people who run shows but shouldn't change the graphics: they can take, clear and change data, but not edit compositions or switch what an output plays. [API keys](https://docs.airframe.tv/api/authentication) can do what an operator can on the outputs they control.