Control room: graphics commands, presets and external data sources on an overlay # Overview > What an overlay is, the two endpoints that drive graphics on it, and where the ids come from. An overlay is the per-match graphics session. It is the browser source that your vision mixer loads. Everything you fire over REST lands on an overlay. Browser source, 1920×1080 ```http https://overlay.ligr.live/production-3b2b1c0e-… ``` Two endpoints drive graphics on an overlay. Both need the `overlays:write` scope. | Endpoint | Use it when | | ----------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- | | `POST /v2/overlays/{overlayId}/graphics/commands` | You address a graphic by name or UUID and you own every variable value. See [Graphics commands](/control-room/graphics-commands/). | | `POST /v2/overlays/{overlayId}/control-room/graphics` | You fire a control room preset by id, and the server fills in the values the operator configured. See [Presets](/control-room/presets/). | A third endpoint feeds data into the theme, not commands. See [External data sources](/control-room/data-sources/). ## What a control room is [Section titled “What a control room is”](#what-a-control-room-is) A control room is the button layout an operator uses during a match. Your organization owns it, and it uses one theme. Each button is a preset. A preset names one graphic and the values of the variables the operator can see. The dashboard groups presets into sections. A competition’s theme profile can assign a default control room. An overlay can select another control room for the same theme. Fire the same preset over REST, and an automation does what the operator does in manual mode. ## Set up a code graphic control room [Section titled “Set up a code graphic control room”](#set-up-a-code-graphic-control-room) Use [Set up through REST](/control-room/setup/) to prepare the full session before opening the dashboard. The sequence creates the competition, teams, venue, match, theme profile, room, sections, presets, and manual overlay. It returns a `controlRoomUrl` that opens the finished setup. Publish and activate your code graphic first through [REST or the CLI](/graphics-sdk/push-and-publish/). The same flow supports plain HTML and any browser rendering library. ## Manual mode [Section titled “Manual mode”](#manual-mode) Dashboard preset commands and both REST command endpoints update manual graphic state. The overlay must use manual mode to display that state. Sending a REST command does not switch the overlay into manual mode. The **MANUAL** switch is unavailable when no overlay is selected, the overlay follows a controller, or its ad type is **Free**. For **Free** overlays, change the ad type to **Brands** or **No Brands** in overlay settings before enabling manual mode. For linked overlays, select the controller and enable manual mode there. Theme activation is separate from manual mode. Reload existing overlay pages after activating a different theme version. ## Check what is on screen [Section titled “Check what is on screen”](#check-what-is-on-screen) No endpoint reads the live state of an overlay. `manualGraphicState` comes back only from the command you just sent. It proves the server accepted the command. It does not prove a graphic appeared. Open the monitoring view of the overlay to see the result. Read `monitoringUrl` from the overlay response. `GET /v2/overlays/{overlayId}` and `POST /v1/overlays` return it. `GET /v2/control-rooms/{roomId}` returns it for every overlay that uses the room when the key has `overlays:read`. Do not build the URL yourself. Monitoring view, safe to open at any time ```http https://overlay.ligr.live/monitoring-{key} ``` The example shows the production host, `overlay.ligr.live`. Other environments use their own overlay host, and `monitoringUrl` always carries the correct one. Keep the monitoring tab visible and focused. Background tabs pause animation. A graphic’s element exists only while it is shown. Do not use DOM presence to check visibility. Use `monitoring-` for every check of your own. The `production-` URL of the same key belongs to the vision mixer. One production session can hold an overlay at a time, so opening that URL during a match can take the session from the mixer, or fail because the mixer already holds it. A production session also counts against ad metrics and the account balance. A monitoring view does neither. A command can answer `200` for a graphic that never appears. The graphic loads, and its own visibility never turns on. Check these in order. * The overlay uses manual mode. See [Manual mode](#manual-mode) above. * The theme version you activated holds the graphic version you published. * A Rive graphic binds its artboard visibility to the reserved `hide` variable. A graphic that binds visibility to a variable of its own accepts every command and never appears. See [How it works](/rive-graphics/how-it-works/). * A code graphic declares each variable you send in its manifest. See [Graphics commands](/control-room/graphics-commands/) for what happens to a value the graphic does not declare. ## Linked overlays [Section titled “Linked overlays”](#linked-overlays) An overlay can follow another overlay. The follower shows what the controller shows. Send commands to the controller. The API applies the state to the controller and to every follower in one write. A command sent to a follower fails. ## Where the ids come from [Section titled “Where the ids come from”](#where-the-ids-come-from) | Id | Where you get it | | -------------------------------- | ------------------------------------------------------------------------------------------------------------------- | | `overlayId` | A match with `include=o`, or [create an overlay](/rest/operations/createoverlay/) | | `graphicUuid` and graphic `name` | `GET /v2/themes/{themeId}/code-graphics` or `GET /v2/themes/{themeId}/rive-graphics` returns `graphicId` and `name` | | `presetId` | `POST` or `GET /v2/control-rooms/{roomId}/presets` | | `competitionThemeSettingId` | `POST` or `GET /v2/competitions/{competitionId}/theme-profiles` | | Data source `alias` | [External data sources](/control-room/data-sources/) | To list every preset of a room with its UUID and configured values, call `GET /v2/control-rooms/{roomId}/presets`. Find the room with `GET /v2/control-rooms?themeId={themeId}`. Find the overlays of a match ```bash curl 'https://api.ligr.live/rest/v2/matches/1188213?include=o' \ -H 'Authorization: Bearer YOUR_READ_KEY' ``` Find the overlays of every match in a competition ```bash curl 'https://api.ligr.live/rest/v2/matches?competitionId=4821&include=o&page=1&pageSize=50' \ -H 'Authorization: Bearer YOUR_READ_KEY' ``` ## The state you get back [Section titled “The state you get back”](#the-state-you-get-back) Every command returns the overlay id and the full `manualGraphicState`. The state is a map keyed by graphic UUID. Each entry holds the `variableValues` in force and the preset that set them. Response ```json { "overlayId": 2300003, "manualGraphicState": { "8efdf988-1f4d-43ac-873b-06b9b4f3e379": { "_ligr_presetId": 1091, "variableValues": { "CustomText": "Centre Court", "hide": false } } } } ``` Keys that start with `_ligr_` are server bookkeeping. Read them if you want. Never send them. # External data sources > Push a table, a key-value list or one value into a theme data source, as JSON or as CSV, and every overlay of the competition updates. A theme can declare external data sources: a standings table, a fact file, a sponsor line. Your graphics read them. You fill them over REST, from a script, a spreadsheet or an automation tool. ```http POST /v2/data-sources/{competitionThemeSettingId}/{alias} ``` Needs the `data-sources:write` scope. One data source is one `alias` in one competition theme setting. Each push replaces the whole snapshot. Every overlay of that competition receives the new data at once. ## Where the ids come from [Section titled “Where the ids come from”](#where-the-ids-come-from) | Id | Where you get it | | --------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `competitionThemeSettingId` | `GET /v2/competitions/{competitionId}/theme-profiles`: the `id` of a theme profile. Also under the competition, then the theme, in the dashboard | | `alias` | The data sources of the theme in the dashboard. Each data source shows its alias, its shape and its schema. To create a data source over REST, see [Data schemas](/rive-graphics/theme-resources/#data-schemas) | The dashboard offers a Google Apps Script per theme that pushes a sheet to this endpoint. Find it under the theme settings of the competition. ## Two payload formats [Section titled “Two payload formats”](#two-payload-formats) Send `data` or `csv`. A body with neither returns 400. ### Pre-formatted JSON [Section titled “Pre-formatted JSON”](#pre-formatted-json) Send the object that matches the schema of the data source. Use this format for a nested schema and for any integration that already has structured data. Push JSON ```bash curl -X POST 'https://api.ligr.live/rest/v2/data-sources/8123/standings' \ -H 'Authorization: Bearer YOUR_WRITE_KEY' \ -H 'Content-Type: application/json' \ -d '{ "data": { "rows": [ { "team": "Eagles", "score": 42 }, { "team": "Hawks", "score": 38 } ] } }' ``` ### CSV [Section titled “CSV”](#csv) Send the sheet as one string. The server parses it into the shape of the data source. Use this format for flat tables, for example from a spreadsheet export or a copy and paste. Push CSV ```bash curl -X POST 'https://api.ligr.live/rest/v2/data-sources/8123/standings' \ -H 'Authorization: Bearer YOUR_WRITE_KEY' \ -H 'Content-Type: application/json' \ -d '{ "csv": "team,score\nEagles,42\nHawks,38" }' ``` CSV cannot express a nested object. If the schema nests objects, build the JSON yourself. ## How CSV is parsed [Section titled “How CSV is parsed”](#how-csv-is-parsed) * The delimiter is detected from the first line. Tab wins, then comma, then semicolon. * Quoted fields can hold the delimiter and line breaks. Two double quotes inside a quoted field are one quote. * Empty rows are dropped. * `true` and `false` become booleans. An empty cell and the word `null` become `null`. * A cell that is a number of 15 characters or fewer becomes a number. Longer digit strings stay strings, so phone numbers and long ids survive. * A cell that holds a JSON array of strings, numbers or booleans becomes that array, for example `["a","b"]`. ## Shapes [Section titled “Shapes”](#shapes) The shape of a data source is fixed in the theme. The parser turns the sheet into that shape. | Shape | Sheet | Result | | ------ | ----------------------------------------------------- | ------------------------------------------- | | `rows` | Row 1 holds the headers. Each later row is one record | `{ "rows": [ { "header": value, … }, … ] }` | | `kv` | Each row is `key,value` | `{ "key": value, … }` | | `cell` | One cell | `{ "value": value }` | Sheet ```text name,score,tags,active Alice,95,"[""sports"",""music""]",true Bob,88,"[""art""]",false ``` Stored snapshot, shape rows ```json { "rows": [ { "name": "Alice", "score": 95, "tags": ["sports", "music"], "active": true }, { "name": "Bob", "score": 88, "tags": ["art"], "active": false } ] } ``` ### Parse hints [Section titled “Parse hints”](#parse-hints) Add `parse` next to `csv` when the sheet is not in the default layout. | Hint | Values | Meaning | | ------------- | ---------------------- | ------------------------------------------------------------------- | | `orientation` | `row` (default), `col` | `col` reads headers down the first column and one record per column | | `headerIndex` | `1` (default) | The 1-based row, or column, that holds the headers | Headers in the first column ```json { "csv": "name\tAlice\tBob\nscore\t95\t88", "parse": { "orientation": "col" } } ``` ## Schema validation [Section titled “Schema validation”](#schema-validation) The server validates the result against the schema of the data source. A mismatch does not reject the push. The data is stored, the response carries a `warning`, and the dashboard shows the warning on the data source. Fix the data and push again to clear it. ## The response [Section titled “The response”](#the-response) ```json { "success": true, "alias": "standings", "competitionThemeSettingId": 8123, "lastPushedAt": "2026-09-08T01:14:02.000Z", "warning": "Schema validation warning: /rows/0/score: must be number" } ``` `warning` is present only when validation failed. ## Errors [Section titled “Errors”](#errors) | Status | Cause | | ------ | ------------------------------------------------------------------------------------------------------ | | 400 | Neither `data` nor `csv`, an empty CSV string, or a CSV the parser cannot read | | 403 | The key lacks `data-sources:write`, or a competition theme setting that your organization does not own | | 404 | No competition theme setting with that id, or no data source with that alias in the theme | # Graphics commands > Show, update, hide and hideAll on one graphic of an overlay, by UUID or by name, with the variable values you choose. `POST /v2/overlays/{overlayId}/graphics/commands` fires one command on one graphic. You name the graphic and you send the variable values. The server applies nothing else. Use [Presets](/control-room/presets/) instead when you want the values an operator configured. That endpoint is `POST /v2/overlays/{overlayId}/control-room/graphics`, and it takes `action` with `presetId` rather than `command` with `graphicUuid`. Both write the same state. The Rive import walkthrough uses the preset form; see [Import through REST](/rive-graphics/import-through-rest/). ## The request [Section titled “The request”](#the-request) Show a graphic with values ```bash curl -X POST 'https://api.ligr.live/rest/v2/overlays/2300003/graphics/commands' \ -H 'Authorization: Bearer YOUR_WRITE_KEY' \ -H 'Content-Type: application/json' \ -d '{ "command": "show", "graphicUuid": "8efdf988-1f4d-43ac-873b-06b9b4f3e379", "variableValues": { "CustomText": "Centre Court" } }' ``` | Field | Required | Meaning | | ---------------- | ---------- | --------------------------------------------------------------------------------------- | | `command` | yes | `show`, `update`, `hide` or `hideAll` | | `graphicUuid` | one of two | The UUID of the graphic. It must be a graphic of the theme of the overlay | | `name` | one of two | The name of the graphic in the theme of the overlay. The server resolves it to the UUID | | `variableValues` | no | A map of variable name to value. Values are strings, numbers, booleans or `null` | `hideAll` takes no target. Every other command needs `graphicUuid` or `name`. If you send both, `graphicUuid` wins. If you send neither, the response is 400. `name` is the human name of the graphic, as the theme shows it. Do not pass a UUID in `name`. ## What each command does [Section titled “What each command does”](#what-each-command-does) | Command | Effect on the graphic | | --------- | ------------------------------------------------------------------- | | `show` | The graphic is visible with the `variableValues` you send | | `update` | The same as `show`. The graphic is visible with the values you send | | `hide` | The graphic is hidden. Its last values stay in the state | | `hideAll` | Every graphic on the overlay is hidden. Media playback stops | `show` and `update` replace the variable values of the graphic. They do not merge with the values in force. Send the complete set on every call. A variable you omit falls back to its default in the graphic. Do not send `hide` in `variableValues`. The command sets it. Update the score on a visible scoreboard ```json { "command": "update", "name": "Scoreboard", "variableValues": { "scoreA": 1, "scoreB": 0 } } ``` Hide one graphic ```json { "command": "hide", "graphicUuid": "8efdf988-1f4d-43ac-873b-06b9b4f3e379" } ``` Clear the screen ```json { "command": "hideAll" } ``` ## Variable values [Section titled “Variable values”](#variable-values) A graphic declares its control variables in the theme. Each variable has a name, a type and a default. `GET /v2/control-rooms/{roomId}/presets` lists them per graphic, with the configured value. Send the JSON type that matches the variable type: `boolean`, `number`, `string`, `enum`, and entity references such as `team`, `player` and `fact`. See [Control variable types](/graphics-sdk/manifest/#types) for the full catalogue. A value you send for a variable the graphic does not declare is stored and ignored. ## Errors [Section titled “Errors”](#errors) | Status | Cause | | ------ | ------------------------------------------------------------------------------------------------------------------------- | | 400 | No `graphicUuid` and no `name`, or the overlay has no theme so `name` cannot resolve | | 403 | The key lacks `overlays:write` | | 404 | The overlay belongs to another organization, or the theme of the overlay has no graphic with that `graphicUuid` or `name` | The command fails on an overlay that follows another overlay. Send it to the controller. See [Linked overlays](/control-room/#linked-overlays). ## The response [Section titled “The response”](#the-response) The response is the overlay id and the full `manualGraphicState`, keyed by graphic UUID. See [The state you get back](/control-room/#the-state-you-get-back). The dashboard and every open browser source receive the same state at the same moment. # Presets > Fire a control room preset by id. The server resolves the graphic and the values the operator configured, then applies your overrides. `POST /v2/overlays/{overlayId}/control-room/graphics` fires one preset. A preset is one button in a control room. The server resolves the graphic the preset points at, applies the values the operator set on the preset, then applies your overrides on top. Use [Graphics commands](/control-room/graphics-commands/) instead when you own every value. ## The request [Section titled “The request”](#the-request) Show a preset ```bash curl -X POST 'https://api.ligr.live/rest/v2/overlays/2300003/control-room/graphics' \ -H 'Authorization: Bearer YOUR_WRITE_KEY' \ -H 'Content-Type: application/json' \ -d '{ "action": "show", "presetId": 1091 }' ``` | Field | Required | Meaning | | ---------------- | -------- | --------------------------------------------------------- | | `action` | yes | `show`, `update` or `hide` | | `presetId` | yes | The numeric `id` returned by preset creation or listing | | `variableValues` | no | Overrides. A value here wins over the value on the preset | `update` sends new values to a preset graphic that is already on air. The live values become the preset values plus your `variableValues`, the same as a `show`. If the graphic is not on air, `update` returns 400 and changes nothing. Use `show` first. `update` writes the same command as an `update` through [Graphics commands](/control-room/graphics-commands/), and also records the preset id. For `hideAll`, use [Graphics commands](/control-room/graphics-commands/). Show a preset and override one value ```json { "action": "show", "presetId": 1091, "variableValues": { "CustomText": "Centre Court" } } ``` Update a preset that is on air ```json { "action": "update", "presetId": 1091, "variableValues": { "CustomText": "Court 2" } } ``` Hide a preset ```json { "action": "hide", "presetId": 1091 } ``` ## How values are resolved [Section titled “How values are resolved”](#how-values-are-resolved) The values in force after `show` come from three layers. A later layer wins. 1. The defaults of the graphic in the theme. The server does not copy these into the state. The renderer reads them at draw time. 2. The values the operator set on the preset. Only variables the preset exposes carry a value. 3. Your `variableValues`. So a `show` with no `variableValues` shows exactly what the button shows. A `show` with overrides changes only the variables you name. Do not send `hide` in `variableValues`. The action sets it. ## Extensions [Section titled “Extensions”](#extensions) A control room can define a preset as an extension of a base preset. An example is a lower-third variant that adds a line to the base lower-third. The layering is a control room setting. Fire the extension preset by id and the server does the rest. * `show` on an extension shows it over the base and keeps the base values. * `hide` on an extension reverts the graphic to the base preset. * `hide` on a base preset hides the graphic, extension included. ## Errors [Section titled “Errors”](#errors) | Status | Cause | | ------ | ------------------------------------------------------------------------------------ | | 400 | `action` is not `show`, `update` or `hide`, or `presetId` is missing | | 400 | `update` targets a preset graphic that is not on air | | 403 | The key lacks `overlays:write` | | 404 | The overlay or the preset belongs to another organization, or the preset was deleted | The command fails on an overlay that follows another overlay. Send it to the controller. See [Linked overlays](/control-room/#linked-overlays). ## The response [Section titled “The response”](#the-response) The response is the overlay id and the full `manualGraphicState`. The entry for the graphic carries `_ligr_presetId` with the preset you fired. See [The state you get back](/control-room/#the-state-you-get-back). ## Where to find a preset id [Section titled “Where to find a preset id”](#where-to-find-a-preset-id) Call `GET /v2/control-rooms/{roomId}` with `themes:read` to read the room, its theme, and every preset with its graphic name and exposed variables. Call `GET /v2/control-rooms/{roomId}/presets` to list preset IDs, graphic IDs, sections, and configured values. Use `GET /v2/control-rooms?themeId={themeId}` to discover the room. Create a preset with `POST /v2/control-rooms/{roomId}/presets` and `themes:write`. Send `sectionId`, `graphicId`, `name`, optional `variableValues` keyed by manifest variable name, and optional `exposeVariables`. `exposeVariables` names fact, stat, team, player and match variables that the operator picks live. The API reads the active published graphic manifest and creates the preset’s internal structure. Change a preset with `PUT /v2/control-rooms/{roomId}/presets/{presetId}`: rename it, move it to another section, or replace its variable set. Every preset response lists `exposedVariables`. See [Set up through REST](/control-room/setup/) for the complete sequence. The control room in the dashboard shows the same presets as buttons. # Set up through REST > Prepare a code or Rive graphic, competition, match, presets, and manual overlay, then open the finished control room. Create the resources through REST, then open the returned control-room URL. This flow supports code graphics and native Rive graphics. You need an organization and an [API key](/get-started/authentication/). The key needs `themes:write`, `competitions:write`, `teams:write`, `venues:write`, `matches:write`, and `overlays:write`. Each write scope also allows reads for that resource. The person opening the control room needs an authenticated dashboard session with access to the same organization. ## 1. Publish the graphic [Section titled “1. Publish the graphic”](#1-publish-the-graphic) Follow [Publish without the CLI](/graphics-sdk/push-and-publish/#without-the-cli) to create the theme, upload the bundle, and publish the graphic. For native Rive, follow [Import through REST](/rive-graphics/import-through-rest/) through theme activation. Publish a theme version, then activate that exact version through REST. Save the theme’s numeric `id` as `THEME_ID` and the graphic’s UUID as `GRAPHIC_ID`. | Operation | Route | | ----------------------------- | -------------------------------------------------------------- | | Create the theme | `POST /v2/themes` | | Create the graphic | `POST /v2/themes/{themeId}/code-graphics` | | Request signed upload URLs | `POST /v2/themes/{themeId}/code-graphics/{graphicId}/uploads` | | Upload files | `PUT` each returned upload URL with the file body | | Record the files and manifest | `PUT /v2/themes/{themeId}/code-graphics/{graphicId}/bundle` | | Publish the graphic | `POST /v2/themes/{themeId}/code-graphics/{graphicId}/versions` | | Publish the theme snapshot | `POST /v2/themes/{themeId}/versions` | | Activate that snapshot | `PUT /v2/themes/{themeId}/active-version` | The remaining examples use Bash, `curl`, and `jq`. Run them in one terminal. Set `LIGR_API_KEY`, `THEME_ID`, and `GRAPHIC_ID` from your authentication and publishing steps. Terminal ```bash set -euo pipefail export LIGR_API_URL="${LIGR_API_URL:-https://api.ligr.live/rest/v2}" export LIGR_REST="${LIGR_API_URL%/v2}" api() { local method="$1" route="$2" shift 2 curl --fail-with-body --silent --show-error \ -X "$method" "$LIGR_REST/$route" \ -H "Authorization: Bearer $LIGR_API_KEY" \ -H 'Content-Type: application/json' "$@" } ``` Set `LIGR_API_URL` to your target environment before continuing. The Rive tutorial and the CLI read the same variable. The default is `https://api.ligr.live/rest/v2`. This page calls `v1` and `v2` routes, so `LIGR_REST` removes the `/v2` suffix. ## 2. Create the room and preset [Section titled “2. Create the room and preset”](#2-create-the-room-and-preset) A room belongs to your organization and one theme. A section groups its preset buttons. Terminal ```bash ROOM_ID=$(api POST v2/control-rooms --data "$(jq -n \ --argjson themeId "$THEME_ID" \ '{themeId: $themeId, name: "Match control room"}')" | jq -er '.id') SECTION_ID=$(api POST "v2/control-rooms/$ROOM_ID/sections" \ --data '{"name":"Match graphics"}' | jq -er '.id') PRESET_ID=$(api POST "v2/control-rooms/$ROOM_ID/presets" --data "$(jq -n \ --arg sectionId "$SECTION_ID" --arg graphicId "$GRAPHIC_ID" \ '{sectionId: $sectionId, graphicId: $graphicId, name: "Scorebug", variableValues: {}}')" \ | jq -er '.id') ``` The graphic must appear in the theme’s active published version. Preset creation reads that published manifest, including its variable types and defaults. Only variables named in `variableValues` or `exposeVariables` appear as editable fields on the preset. The API stores the internal preset structure for you. Set `variableValues` by manifest variable name, for example `{"CustomText":"Centre Court"}` when your graphic defines `CustomText`. `variableValues` accepts string, number, boolean and enum variables. Name fact, stat, team, player and match variables in `exposeVariables`; the preset shows them and the operator picks the value live. Do not send `hide`; the show and hide actions control visibility. Unknown names and invalid values fail before the API creates the preset. Expose an event picker and a statistic picker ```bash api POST "v2/control-rooms/$ROOM_ID/presets" --data "$(jq -n \ --arg sectionId "$SECTION_ID" --arg graphicId "$GRAPHIC_ID" \ '{sectionId: $sectionId, graphicId: $graphicId, name: "Scorebug", variableValues: {AddedTime: "0"}, exposeVariables: ["Event", "SingleStat"]}')" | jq ``` The response lists `exposedVariables`: every variable the operator can see, with or without a stored value. To change a preset later, send `PUT /v2/control-rooms/{roomId}/presets/{presetId}`. Send `name` or `sectionId` alone to rename or move it. Send `variableValues` and `exposeVariables` together to replace its variable set; the stored set becomes exactly what you send. To reuse resources, list rooms with `GET /v2/control-rooms?themeId={themeId}`. List their sections and presets with `GET /v2/control-rooms/{roomId}/sections` and `GET /v2/control-rooms/{roomId}/presets`. ## 3. Create the competition, venue, and teams [Section titled “3. Create the competition, venue, and teams”](#3-create-the-competition-venue-and-teams) Discover competitions through `GET /v1/competitions` and venues through `GET /v1/venues`. List a competition’s teams with `GET /v1/competitions/{competitionId}/teams`. Use the returned IDs to skip the corresponding creation calls. List grades through REST. Set `GRADE_ID` to the returned ID that matches your competition. The example uses adult mixed football; change the metadata for your competition. Terminal ```bash api GET v1/competitions/grades | jq . # Set GRADE_ID to the selected numeric id before continuing. COMPETITION_ID=$(api POST v1/competitions --data "$(jq -n \ --argjson gradeId "$GRADE_ID" \ '{name: "My competition", sport: "football", age: "Adults", gender: "Mixed", gradeId: $gradeId}')" \ | jq -er '.id') VENUE_ID=$(api POST v1/venues --data '{"name":"Main ground"}' | jq -er '.id') create_team() { api POST v1/teams --data "$(jq -n \ --arg name "$1" --arg abbreviation "$2" \ --argjson gradeId "$GRADE_ID" --argjson competitionId "$COMPETITION_ID" \ --argjson venueId "$VENUE_ID" \ '{name: $name, abbreviation: $abbreviation, sport: "football", age: "Adults", gender: "Mixed", gradeId: $gradeId, competitionIds: [$competitionId], defaultVenueId: $venueId}')" | jq -er '.id' } HOME_TEAM_ID=$(create_team 'Home team' HOME) AWAY_TEAM_ID=$(create_team 'Away team' AWAY) ``` ### Set a team logo [Section titled “Set a team logo”](#set-a-team-logo) Graphics show the team logo as `logoUrl`. Upload the logo in two calls. The API does not fetch images from a URL. Use a PNG or JPEG file of 500 KB or less. Declare its type, size, and SHA-256 first. The response holds `uploadId`, a signed `url`, and the `headers` for the upload. Terminal ```bash LOGO=home-crest.png SESSION=$(api POST "v1/teams/$HOME_TEAM_ID/logo/uploads" --data "$(jq -n \ --argjson size "$(wc -c < "$LOGO" | tr -d ' ')" \ --arg sha256 "$(shasum -a 256 "$LOGO" | cut -d ' ' -f 1)" \ '{contentType: "image/png", size: $size, sha256: $sha256}')") curl --fail-with-body --silent --show-error -X PUT "$(jq -er '.url' <<< "$SESSION")" \ -H "x-amz-checksum-sha256: $(jq -er '.headers["x-amz-checksum-sha256"]' <<< "$SESSION")" \ --data-binary "@$LOGO" api PUT "v1/teams/$HOME_TEAM_ID/logo" --data "$(jq -n \ --argjson uploadId "$(jq -er '.uploadId' <<< "$SESSION")" '{uploadId: $uploadId}')" | jq '{id, logoUrl}' ``` Do the PUT before `expiresAt`. Send the `uploadId` within one hour. The API checks the size, the SHA-256, and the image type, then stores the logo. Each upload sets a logo one time. To change the logo, open a new upload. To remove the logo, send `DELETE /v1/teams/{teamId}/logo`. A team without a logo shows the club logo, if the club has one. ## 4. Create the match and theme profile [Section titled “4. Create the match and theme profile”](#4-create-the-match-and-theme-profile) Set `MATCH_DATE` to your kickoff time in ISO 8601 format, including its timezone. The example uses team competitors. Other sports can use player competitors. `competitorsType` is the match format: `singles`, `doubles`, `teams` or `group`. `entityType` is the kind of each row in `competitors`: `team` or `player`. For example, a doubles match has `competitorsType: doubles` and four `entityType: player` rows. A group match also has `player` rows. Terminal ```bash MATCH_ID=$(api POST v2/matches --data "$(jq -n \ --arg date "$MATCH_DATE" --argjson competitionId "$COMPETITION_ID" \ --argjson venueId "$VENUE_ID" --argjson home "$HOME_TEAM_ID" --argjson away "$AWAY_TEAM_ID" \ '{competitionId: $competitionId, venueId: $venueId, date: $date, competitorsType: "teams", competitors: [ {entityId: $home, entityType: "team", meta: {isHome: true}}, {entityId: $away, entityType: "team", meta: {isHome: false}} ]}')" | jq -er '.id') PROFILE_ID=$(api POST "v2/competitions/$COMPETITION_ID/theme-profiles" --data "$(jq -n \ --argjson themeId "$THEME_ID" --argjson roomId "$ROOM_ID" \ '{themeId: $themeId, name: "Broadcast profile", defaultControlRoomId: $roomId}')" | jq -er '.id') ``` The theme must support the competition’s sport and have an active published version. The first profile becomes the competition default. This flow also assigns the profile and room explicitly to the overlay. To set competition-specific colors or labels, include `themeVariableValues` when creating the profile. Read `variables` from `GET /v2/themes/{themeId}/versions/{activeVersion}`. Use those variable IDs, for example `[{"variableId":"accent","value":"#0055ff"}]`. The response returns the saved values. Unknown IDs and invalid types fail before creation. To reuse a profile, call `GET /v2/competitions/{competitionId}/theme-profiles`. ## 5. Create the manual overlay [Section titled “5. Create the manual overlay”](#5-create-the-manual-overlay) Terminal ```bash OVERLAY=$(api POST v1/overlays --data "$(jq -n \ --argjson matchId "$MATCH_ID" --argjson profileId "$PROFILE_ID" --argjson roomId "$ROOM_ID" \ '{name: "Broadcast overlay", matchId: $matchId, competitionThemeSettingId: $profileId, controlRoomId: $roomId, autoMode: false, adType: "noBrands"}')") OVERLAY_ID=$(jq -er '.id' <<< "$OVERLAY") CONTROL_ROOM_URL=$(jq -er '.controlRoomUrl' <<< "$OVERLAY") api GET "v1/overlays/$OVERLAY_ID" | jq '{id, autoMode, adType, competitionThemeSettingId, controlRoomId, controlRoomUrl}' printf '%s\n' "$CONTROL_ROOM_URL" ``` The read response confirms the saved profile, room, and manual mode. `controlRoomUrl` includes the organization, match, overlay, and room IDs. It opens the saved setup without a theme-profile editor or overlay-settings visit. `autoMode: false` requires `adType: "noBrands"` or `"brands"`. The API rejects manual setup with `"free"`. An update preserves fields you omit, including the ad type. ## 6. Open the finished control room [Section titled “6. Open the finished control room”](#6-open-the-finished-control-room) Open `CONTROL_ROOM_URL` in your authenticated browser. Select **GFX In** on the preset and check the preview. If the preset exposes variables, edit them and select **Update GFX**. Select **GFX Out** to hide it. You can also fire the same prepared preset through REST: Terminal ```bash api POST "v2/overlays/$OVERLAY_ID/control-room/graphics" --data "$(jq -n \ --argjson presetId "$PRESET_ID" '{action: "show", presetId: $presetId}')" ``` ## 7. Check a prepared room without a dashboard login [Section titled “7. Check a prepared room without a dashboard login”](#7-check-a-prepared-room-without-a-dashboard-login) Use this procedure when you cannot open the dashboard, for example from an automation. The key needs `themes:read` to read the room, `overlays:read` to see its overlays, and `overlays:write` to fire presets. 1. Read the room. The response holds the theme, the sections, and every preset with its `graphicId`, `graphicName` and `exposedVariables`. Terminal ```bash ROOM=$(api GET "v2/control-rooms/$ROOM_ID") jq '{theme, presets: [.presets[] | {id, name, graphicName, exposedVariables}]}' <<< "$ROOM" ``` 2. Open the monitoring view of the overlay. Read `monitoringUrl` from the room; do not build it. The room lists `overlays` only when the key has `overlays:read`. Terminal ```bash jq -er --argjson id "$OVERLAY_ID" '.overlays[] | select(.id == $id) | .monitoringUrl' <<< "$ROOM" ``` 3. Fire each preset and check the monitoring view after each command. Terminal ```bash for PRESET in $(jq -r '.presets[].id' <<< "$ROOM"); do api POST "v2/overlays/$OVERLAY_ID/control-room/graphics" --data "$(jq -n \ --argjson presetId "$PRESET" '{action: "show", presetId: $presetId}')" > /dev/null read -r -p "Preset $PRESET on air? Press Enter to continue." done ``` 4. Update a preset that is on air. Send the values to change in `variableValues`. Terminal ```bash api POST "v2/overlays/$OVERLAY_ID/control-room/graphics" --data "$(jq -n \ --argjson presetId "$PRESET_ID" '{action: "update", presetId: $presetId, variableValues: {CustomText: "Court 2"}}')" ``` 5. Hide each preset. Terminal ```bash api POST "v2/overlays/$OVERLAY_ID/control-room/graphics" --data "$(jq -n \ --argjson presetId "$PRESET_ID" '{action: "hide", presetId: $presetId}')" ``` See [Check what is on screen](/control-room/#check-what-is-on-screen) if a graphic does not appear. ## Theme versions [Section titled “Theme versions”](#theme-versions) Publishing another theme version does not change the active version unless you request activation. Use [version inspection, activation, and rollback](/guides/publish-a-theme-version/) through REST. Reload existing overlay pages after changing the active version.