Rive graphics: beta: native Rive imports, source privacy, bindings, publication and control-room setup
# Rive graphics
> Beta. Import a native Rive runtime or source project, bind live sports data, and publish it through REST.
Beta Native Rive creation and its REST routes are in beta. Check `GET /v2/themes/{themeId}/rive-graphics/capabilities` before each import. A native Rive graphic stores exact `.riv` runtime bytes, one artboard, one state machine, and LIGR configuration. LIGR renders it through the native Rive runtime inside an isolated browser frame. You can start in the dashboard Rive Graphics Builder or use the REST API. Both paths use the same backend preparation and create the same working graphic. Create, drop, and replace actions share this preparation and publication workflow. | Input | What LIGR does | Source archive | | -------------------------- | ------------------------------------------------------------------ | ----------------------------------------------------------- | | `.riv` | Inspects and stores the supplied runtime unchanged | Unavailable. Runtime revisions already preserve these bytes | | `.rev` | Extracts the project and compiles a runtime | Exact uploaded file, with explicit retention | | ZIP with `.riv` and assets | Inspects the unchanged runtime and stores supplied playback images | Complete original archive, with explicit retention | | ZIP with `.rev` | Selects the editor file and compiles a runtime | Complete original archive, with explicit retention | | Complete RML project ZIP | Validates `rive.yaml`, dependencies, and output, then compiles | Complete original project archive, with explicit retention | RML means Rive Markup Language. A lone `.rml` file is not a general project interchange format. A complete project contains `rive.yaml` and every referenced dependency. ZIP sibling files must use the exact `uniqueFilename` reported by Rive. Duplicate matching filenames remain unresolved. LIGR does not guess a match from the display name. Supplied playback images remain available when source retention is off. A runtime with missing external images can become a draft. Its metadata marks those images as unresolved. Resolve them through the builder’s image uploads or bindings before using the graphic on air. Known default image dimensions remain available even when the external image is missing. Use these pages: | Page | What it covers | | ---------------------------------------------------------- | ------------------------------------------------------------------------------- | | [How it works](/rive-graphics/how-it-works/) | Runtime isolation, bindings, lifecycle, versions, and control-room updates | | [Import through REST](/rive-graphics/import-through-rest/) | Executable upload, polling, selection, creation, publication, and setup calls | | [Source files](/rive-graphics/source-files/) | Consent, temporary processing, cleanup, and exact original downloads | | [Local RML project](/rive-graphics/local-project/) | The licensed scorebug project, pinned compiler, and verified LIGR configuration | ## Rive and the Rive CLI [Section titled “Rive and the Rive CLI”](#rive-and-the-rive-cli) [Rive](https://rive.app) is a tool for interactive, real-time graphics. Designers build a graphic in the Rive editor. Rive exports it as a `.riv` runtime file or saves it as a `.rev` editor file. The [Rive CLI](https://rive.app/docs/cli/overview) builds the same files in a terminal, without the editor. It reads RML, a text format for Rive scenes. It compiles RML to a `.riv` file and shows a live preview while you edit. [Local RML project](/rive-graphics/local-project/) names the CLI version that LIGR tests with. ## Build graphics with an AI agent [Section titled “Build graphics with an AI agent”](#build-graphics-with-an-ai-agent) The Rive CLI works with a general AI agent from Anthropic (Claude), OpenAI (ChatGPT or Codex), or another provider. The agent writes the RML text. You make the graphic with prompts, not code. 1. Run `rive create` to start a project. It writes `AGENTS.md` and `CLAUDE.md` with instructions for the agent. 2. Describe the graphic to the agent, or give it a design image. The agent writes the scene in RML. 3. Watch the live preview. Tell the agent what to change. 4. Import the project into a theme. See [Import through REST](/rive-graphics/import-through-rest/). 5. Bind the graphic to live match data and control variables. See [How it works](/rive-graphics/how-it-works/). The result is a live overlay graphic. The score, the clock and the lineups update from the match. Operators show, hide and change the graphic from the control room. Give the agent this documentation too. [For AI agents](/ai-agents/) lists the machine-readable pages and the rules an agent must follow. ## Rive graphics and the Graphics SDK [Section titled “Rive graphics and the Graphics SDK”](#rive-graphics-and-the-graphics-sdk) Native Rive graphics do not implement the code-graphics `ligr.gfx.v1` protocol. They do not use `@ligrsystems/graphics-sdk` or its browser `createGraphic()` function. Browser SDK `createGraphic()` starts a code-graphic runtime listener in a web page. REST graphic creation stores a native Rive candidate in a theme. Use the [Graphics SDK](/graphics-sdk/) for HTML, CSS, and JavaScript graphics. The SDK installation guides identify the matching package release. Native Rive imports do not require those packages. ## Security boundaries [Section titled “Security boundaries”](#security-boundaries) Browser expression functions and native runtime handles stay inside the isolated renderer. The application supplies selected runtime bytes and fetches permitted public resources without cookies or referrers. Public image URLs can disclose renderer data through request URLs. The resource bridge executes in the browser and is not a server-side fetch proxy. Theme-writing API keys are trusted authoring credentials. Static expression validation complements browser isolation and does not replace careful authoring. Code graphics retain browser network access. Their module and font assets require anonymous CORS.
# Command line
> Import, edit, pull, push and publish a Rive graphic with the ligr-graphic CLI.
`ligr-graphic` sends every change as one REST operation. It needs no browser and no dashboard session. Install it with the commands in [Code graphics](/graphics-sdk/). Use a write API key with `themes:read` and `themes:write`. Keep it in `LIGR_API_KEY`. Never print it. The CLI sends requests to production by default. For another environment, set `LIGR_API_URL` to its full REST base, including `/rest/v2`, or pass `--base-url`. For `rive` commands, `apiUrl` in `rive.json` comes first. ## Import a file [Section titled “Import a file”](#import-a-file) Terminal
```bash
export LIGR_API_KEY=""
npx ligr-graphic rive import ./continental-scorebug.zip \
--theme 203 --name "Continental scorebug"
```
The command uploads the file, starts preparation, waits for the result, and creates the graphic. It writes `rive.json` with the theme id and the graphic id. Later commands read that file. Add `--retain-source` to keep the editable source. Read [Source files and privacy](/rive-graphics/source-files/) first. An archive with more than one project answers with a list of entries. Pass the exact entry: Terminal
```bash
npx ligr-graphic rive import ./bundle.zip --theme 203 --name "Bug" --select scorebug/rive.yaml
```
A file with more than one artboard pauses the import. The command prints each choice. Finish it: Terminal
```bash
npx ligr-graphic rive settings select --artboard 0 --state-machine 0
```
## Edit one value at a time [Section titled “Edit one value at a time”](#edit-one-value-at-a-time) Terminal
```bash
npx ligr-graphic rive control add stage --type enum --options "GROUP A,FINAL" --default "GROUP A"
npx ligr-graphic rive bind set Main.homeScore '$d.1.score'
npx ligr-graphic rive bind expose Main.badge --name "Home badge" --entity team --expr '$d.1.id' --required
npx ligr-graphic rive input set on '!$v.hide.value'
npx ligr-graphic rive asset set crest.png --expr '$d.1.logo.url'
npx ligr-graphic rive expr add leader '$d.1.score > $d.2.score ? 1 : 2'
npx ligr-graphic rive schema add tennisRankings
npx ligr-graphic rive settings set --renderer canvas --scale 0.5
```
Bind the visibility of the graphic to the reserved `hide` variable. The server sets `hide` on every show command and every hide command. See [How it works](/rive-graphics/how-it-works/). Each command reads the draft, then sends the change with the timestamp it read. A draft that changed in the meantime answers `409 TARGET_CHANGED`. The CLI reads it again and retries once. Opening a graphic in the dashboard Graphics Builder takes its edit lock. While that builder tab is open, every CLI write answers `409 LOCKED`. The message names the editor. Close the builder tab, then run the command again. ## Edit many values at once [Section titled “Edit many values at once”](#edit-many-values-at-once) Terminal
```bash
npx ligr-graphic rive pull
# edit the "config" block of rive.json
npx ligr-graphic rive push --dry-run
npx ligr-graphic rive push
```
`rive pull` writes the working configuration into the `config` block of `rive.json`. `rive push` compares that block with the draft. It sends one operation for each change. The server never receives the whole configuration. `--dry-run` prints the operations and sends none. A push is not atomic. Each operation stands alone. If one operation fails, the operations before it stay applied. The command prints the count it applied and the operation that failed. Run `rive pull`, then push again. A push removes a control variable or a user expression only when nothing refers to it. A referenced value answers `422 UNKNOWN_REFERENCE`. Add `--force` to remove it. `--force` blanks every expression that refers to the removed value. `pull` and `push` do not manage manual list elements. Use `rive bind list-add`, `rive bind list-rm` and `rive bind list-order` for those. ## Publish [Section titled “Publish”](#publish) Terminal
```bash
npx ligr-graphic rive publish --theme-version
```
`--theme-version` publishes a theme version that pins the new graphic version. The command prints the theme version. Replace `203` with your theme ID and `12` with that version. Inspect the version, then activate it when no match is live: Terminal
```bash
npx ligr-graphic theme inspect --theme 203 --version 12
npx ligr-graphic theme activate --theme 203 --version 12
```
Reload overlay pages to load the active version. Caution `--activate` sets the new theme version active in the same command. A person decides when to activate. Coding agents never add `--activate`. See rule 6 in [Rules for agents](/ai-agents/#rules-for-agents). ## Scripts [Section titled “Scripts”](#scripts) Add `--json` to any command. The command prints one JSON document and no human lines. Terminal
```bash
npx ligr-graphic rive list --theme 203 --json | jq -r '.[] | "\(.graphicId) \(.name)"'
```
## Files [Section titled “Files”](#files) | File | Purpose | | ----------- | ------------------------------------------------------------------------------------ | | `rive.json` | `themeId`, `graphicId`, optional `apiUrl`, and the `config` block that `pull` writes | Pass `--theme` and `--graphic` to work without `rive.json`.
# Edit through REST
> Change control variables, bindings, inputs, assets, user expressions, schemas and settings of a working Rive graphic.
Every change the graphics builder makes to a working Rive graphic is one REST operation. Each operation locks the graphic for the request, validates the whole configuration, saves, and publishes a live update to open builder tabs. All routes sit under `/rest/v2/themes/{themeId}/rive-graphics/{graphicId}`. Write routes need `themes:write`. Each write returns `updatedAt`, `entityId`, and the full `stateMachine`. ## Optimistic concurrency [Section titled “Optimistic concurrency”](#optimistic-concurrency) Send `ifUnchangedSince` with the `updatedAt` you last read. A changed draft answers `409 TARGET_CHANGED`. Opening a graphic in the dashboard Graphics Builder takes its edit lock. While that builder tab is open, every REST write answers `409 LOCKED`. Close the builder tab, then retry. The `ligr-graphic` CLI sends `ifUnchangedSince` for you and retries once. See [Command line](/rive-graphics/command-line/). ## Errors [Section titled “Errors”](#errors) An error body has `message` and `code`. A `4xx` answer can also have `details`, an array with one entry for each problem. | Field | Content | | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `code` | The error code, for example `INVALID_CONFIGURATION`, `UNKNOWN_REFERENCE` or `INVALID_EXPRESSION`. | | `details[].field` | The path of the value that failed. The path points into the request body or into the graphic configuration, for example `controlVariables[stage].defaultValue` or `dataBindings[Main.homeScore].expression`. | | `details[].reason` | What is wrong with the value, for example `must be a JSON number`. | 422 response
```json
{
"message": "INVALID_CONFIGURATION",
"code": "INVALID_CONFIGURATION",
"details": [{ "field": "controlVariables[stage].defaultValue", "reason": "must be a JSON number" }]
}
```
A `500` answer has only `message`. The `ligr-graphic` CLI prints each detail on its own line as `field: reason`. ## Control variables [Section titled “Control variables”](#control-variables) Terminal
```bash
api POST "themes/$THEME_ID/rive-graphics/$GRAPHIC_ID/control-variables" \
--data '{"name":"stage","type":"enum","options":["GROUP A","FINAL"],"defaultValue":"GROUP A"}'
api PATCH "themes/$THEME_ID/rive-graphics/$GRAPHIC_ID/control-variables/$CONTROL_ID" --data '{"defaultValue":"FINAL"}'
api DELETE "themes/$THEME_ID/rive-graphics/$GRAPHIC_ID/control-variables/$CONTROL_ID?force=true"
```
Types: `string`, `number`, `boolean`, `enum`, and the entity types `team`, `player`, `match`, `fact`, `stat`, `teamStat`, `period`, `set`, `round`, `court`. Entity types carry a server-derived `dataSchema` in the response. `hide` is reserved. A control variable default must have the JSON type of the variable. Send `0`, not `"0"`. Removing a variable that an expression references answers `422 UNKNOWN_REFERENCE`; `force=true` blanks those expressions. ## Data bindings [Section titled “Data bindings”](#data-bindings) Binding ids contain dots, so URL-encode them. Terminal
```bash
B="themes/$THEME_ID/rive-graphics/$GRAPHIC_ID/data-bindings"
api PATCH "$B/Main.homeScore" --data '{"expression":"$d.1.score"}'
api PUT "$B/Main.badge/exposure" --data '{"name":"Home badge","expression":"$d.1.id","required":true,"entityType":"team"}'
api PUT "$B/Main.badge/image-transform" --data '{"fit":"cover","outWidth":256,"outHeight":256}'
api PUT "$B/Main.players%5B%5D/source-array" --data '{"arrayPath":"1.lineup"}'
api POST "$B/Main.players%5B%5D/elements"
api DELETE "$B/Main.players%5B%5D/elements/0"
api PUT "$B/Main.players%5B%5D/elements/order" --data '{"order":[1,0]}'
```
Exposure and image transforms apply to image bindings only. A source array applies to one list binding per graphic. A source array is a path into the match data without the `$d` prefix, for example `1.lineup` for team one’s lineup. ## Inputs, assets, user expressions, schemas, settings [Section titled “Inputs, assets, user expressions, schemas, settings”](#inputs-assets-user-expressions-schemas-settings) Terminal
```bash
G="themes/$THEME_ID/rive-graphics/$GRAPHIC_ID"
api PATCH "$G/inputs/$INPUT_ID" --data '{"expression":"!$v.hide.value"}'
api PATCH "$G/assets/crest.png" --data '{"expression":"$d.1.logo.url"}'
api PUT "$G/assets/crest.png/exposure" --data '{"name":"Crest","required":true,"entityType":"team"}'
api POST "$G/user-expressions" --data '{"name":"leader","expression":"$d.1.score > $d.2.score ? 1 : 2"}'
api PUT "$G/schemas/tennisRankings"
api PATCH "$G/settings" --data '{"renderer":"canvas","scale":0.5,"name":"Scorebug","sports":["football"]}'
```
## Graphic lifecycle [Section titled “Graphic lifecycle”](#graphic-lifecycle) Terminal
```bash
api GET "themes/$THEME_ID/rive-graphics"
api POST "$G/duplicate" --data '{"name":"Scorebug copy"}'
api DELETE "$G"
```
Delete is refused with `409 TARGET_NOT_EMPTY` while the active theme version pins a published version of the graphic. Publish, theme snapshots and activation are unchanged; read [Import through REST](/rive-graphics/import-through-rest/) step 6.
# How Rive imports work
> Follow source bytes through preparation, bindings, exact graphic versions, theme snapshots, and the control room.
LIGR separates source preparation, native runtime playback, sporting expressions, and publication. Each boundary preserves one clear artifact. ## Lifecycle [Section titled “Lifecycle”](#lifecycle) Rive source-to-control-room lifecycle Upload → backend preparation ↙ or ↘ **.riv or runtime ZIP**Inspect unchanged **Source project**Compile ↘ rejoin ↙ Ready candidate + metadataResolve any missing images ↓ Select + configure ↓ Working graphic ↓ Saved revision ↙ contains ↘ **Runtime + supplied playback images**Stored independently of source retention **Original archive**Only when retained ↓ publish runtime Graphic version ↓ Theme snapshot → Activate → Preset + room + overlay Upload a .riv file for unchanged inspection, or upload a supported source project for compilation. Both paths use backend preparation and create a ready candidate with metadata. Resolve any missing external images. Select and configure the candidate to create a working graphic. A saved revision contains runtime bytes and supplied playback images. It contains the private original only when retention was selected. Publish a graphic version and theme snapshot, activate the snapshot, then add the graphic to a control room. A `.riv` file enters inspection without recompilation. LIGR preserves its bytes exactly. Supported `.rev` and RML projects compile before inspection. All inputs use the same backend preparation. The dashboard and REST API consume its validated candidate metadata. LIGR stores supplied playback images with the runtime, independently of optional source retention. Preparation runs asynchronously. It returns a candidate only after validating the result and inspecting its native structure. An ambiguous ZIP returns `selection-required`; send one exact candidate `entry` to continue. Choose one artboard index and one state-machine index from the returned metadata. LIGR never guesses between multiple valid choices. A ready candidate can still contain unresolved external images. Read its asset metadata and resolve those images in the builder. ## Properties, expressions, and controls [Section titled “Properties, expressions, and controls”](#properties-expressions-and-controls) Rive property bindings describe authored runtime properties such as `Main.homeScore`. They do not know football rules or LIGR’s live match shape. A LIGR expression connects a property to live data. For football, `$d.1.score` means displayed team one’s score. Configured team reversal can make displayed team one differ from the match home team. Control variables use `$v..value`. They hold operator choices such as a tournament stage. The reserved `hide` variable controls native visibility and cannot appear in a preset. | Rive property | LIGR expression | Live value | | ---------------- | ------------------- | ---------------------------- | | `Main.homeCode` | `$d.1.abbreviation` | Displayed team-one code | | `Main.awayCode` | `$d.2.abbreviation` | Displayed team-two code | | `Main.homeScore` | `$d.1.score` | Displayed team-one score | | `Main.awayScore` | `$d.2.score` | Displayed team-two score | | `Main.stage` | `$v.stage.value` | Preset or show-command value | | `Main.on` | `!$v.hide.value` | Runtime visibility | Expressions execute inside LIGR’s isolated native renderer. They use the existing expression compiler and never expose native handles to the parent application. ## Show and hide timing [Section titled “Show and hide timing”](#show-and-hide-timing) A hide command sets `hide` to `true`. Your state machine then plays its out animation. The overlay keeps the graphic on screen for 2 seconds after the hide command. Then the overlay fades the graphic out over 0.4 seconds and removes it. Keep each out animation shorter than 2 seconds. The fade cuts off a longer out animation. A new value can arrive while the out animation plays. For example, the next preset changes `stage`. By default, the graphic shows the new value at once. To keep the old value during the exit, set `exitMode: "deferred"` on the control variable. The overlay then holds the old value for 3 seconds before it applies the new value. `hide` never defers. A graphic can load while it is already visible. The state machine can then skip its in animation. To play the in animation on the first show, set `deferInitialShow: true` in the graphic settings. The renderer then applies `hide: true` for one frame before it applies the real value. Terminal
```bash
npx ligr-graphic rive control add stage --type string --default FINAL --exit-mode deferred
npx ligr-graphic rive control set "$CONTROL_ID" --exit-mode deferred
npx ligr-graphic rive settings set --defer-initial-show
```
The same changes through REST: Terminal
```bash
G="themes/$THEME_ID/rive-graphics/$GRAPHIC_ID"
api PATCH "$G/control-variables/$CONTROL_ID" --data '{"exitMode":"deferred"}'
api PATCH "$G/settings" --data '{"deferInitialShow":true}'
```
The `api` helper comes from [Import through REST](/rive-graphics/import-through-rest/). ## Default image sizes [Section titled “Default image sizes”](#default-image-sizes) Image bindings can include `imageDimensions: { width, height }` in pixels. These dimensions describe the image selected by the runtime’s default instance, including nested instances and the first inspected list item. They describe the image itself, not the artboard or the displayed bounds after animation and layout. The runtime’s default can differ from an instance marked as default in the editor. LIGR uses authored asset dimensions when available, or supported embedded image metadata. It does not download external images during preparation. An empty image, missing dimensions, or an unresolved reference leaves `imageDimensions` absent. Use `pathSegments` to identify nested properties when names contain dots. The builder fills **Width (px)** and **Height (px)** when you first expose an image binding. You can edit or clear either value. Reopening the form preserves those choices. A compatible replacement refreshes intrinsic metadata while preserving your configured exposure sizes. ## Worked scorebug [Section titled “Worked scorebug”](#worked-scorebug) The [local project](/rive-graphics/local-project/) compiles artboard `Main` and state machine `Broadcast`. The REST attachment stores the table’s bindings and a `stage` variable with default `FINAL`. The graphic publication freezes those runtime bytes and that configuration as graphic version 1. A theme publication then pins graphic version 1 in a new theme snapshot. Activation remains a separate request. A preset stores `stage: "GROUP A"`. A show command can override it with `stage: "ROUND OF 16"`. Live football data remains authoritative under `$d`, so a new fact changes `Main.homeScore` from 2 to 3. ## Version behavior [Section titled “Version behavior”](#version-behavior) A working graphic is mutable. Edit it through the operation routes described in [Edit through REST](/rive-graphics/edit-through-rest/). Each graphic publication creates an immutable positive version. Exact published runtime downloads never rebuild or follow a later working copy. Published playback images and configuration remain pinned to that graphic version. A theme snapshot freezes its selected graphic versions. Publishing a theme snapshot does not activate it unless you request activation. Existing overlay pages keep their loaded snapshot until reload. Read [Publish a theme version](/guides/publish-a-theme-version/) for inspection, activation, and rollback.
# Images, preview and evaluate
> Supply a missing image, play a working draft, and check every data binding.
A Rive runtime can reference an image that is not inside the file. LIGR marks that image `unresolved`. The graphic renders without it. Supply the image to make the graphic complete. ## Supply a missing image [Section titled “Supply a missing image”](#supply-a-missing-image) Find the unresolved assets first. Terminal
```bash
api GET "themes/$THEME_ID/rive-graphics/$GRAPHIC_ID" | jq '.stateMachine.assets[] | select(.unresolved)'
```
Create an upload session. Send the exact byte count and the lowercase SHA-256 of the file. Terminal
```bash
api POST "themes/$THEME_ID/rive-graphics/images" \
--data '{"filename":"crowd.png","size":48213,"sha256":"6c1b…","contentType":"image/png"}'
```
The response holds `fileId`, `url`, `expiresAt` and `headers`. Upload the exact bytes with one PUT. Send each header from `headers` unchanged. `x-amz-checksum-sha256` is the base64 form of the SHA-256 digest. It is not the hex value you sent. Terminal
```bash
curl -X PUT "$URL" -H "content-type: image/png" -H "x-amz-checksum-sha256: $CHECKSUM" --data-binary @crowd.png
```
Then supply the file id to the asset. Terminal
```bash
api PUT "themes/$THEME_ID/rive-graphics/$GRAPHIC_ID/assets/crowd.png/file" --data '{"fileId":901}'
```
The asset stops being unresolved. The response holds the full `stateMachine`. The same `fileId` works as `defaultFileId` on an asset exposure. Supported types are PNG, JPEG and WebP. The limit is 25 MB. The upload session belongs to the theme. Use the same theme that owns the graphic. The builder shows the same control. Open the Assets tab and select **Upload image** on a row marked **Missing image**. ## Embed a placeholder for every bound image [Section titled “Embed a placeholder for every bound image”](#embed-a-placeholder-for-every-bound-image) Every Image that a view-model property binds needs an embedded placeholder asset in the Rive file. An Image with no asset can blank the whole artboard in the web runtime. LIGR replaces the embedded asset at runtime, so any small image works as the placeholder. ## Give every replaceable image a fixed box [Section titled “Give every replaceable image a fixed box”](#give-every-replaceable-image-a-fixed-box) LIGR sends a bound or exposed image to the Rive runtime at its own pixel size. LIGR does not resize it to the placeholder. The Rive runtime draws an Image outside a layout at the pixel size of the image it holds. The scale of the Image node then multiplies that size. The placeholder size has no effect after LIGR replaces the image. For example, an Image node with scale 2 draws a 256×256 crest at 512×512. The same node draws a 1000×1000 crest at 2000×2000. Crests from different sources then draw at different sizes in the same slot. Put each replaceable Image inside a layout of the slot size, and set `fit` on the Image. The runtime then scales every image into the layout box. A 200×200 crest and a 1000×1000 crest draw at the same size. scene.rml
```xml
```
Use `contain` to show the full image, or `cover` to fill the box and crop the edges. Bind the view-model image property to `Home crest` as usual, or expose the `crest` asset. If the Image cannot sit in a layout, keep the node scale at 1. Then set an image transform on the data binding with a fixed output size: Terminal
```bash
api PUT "themes/$THEME_ID/rive-graphics/$GRAPHIC_ID/data-bindings/Main.homeCrest/image-transform" \
--data '{"fit":"contain","outWidth":96,"outHeight":96}'
```
LIGR resizes each image to 96×96 before the runtime draws it. An image transform applies to data bindings only. An exposed asset without a binding needs the layout. ## Exposed images [Section titled “Exposed images”](#exposed-images) An exposed image makes one image slot on each entity of its type. For example, an image exposed with `entityType: "team"` adds a slot to every team. The binding expression returns the id of the entity, for example `$d.1.id` for team one. LIGR then shows the image that the operator uploaded for that entity. An operator uploads the image in the dashboard. Open the team page and select the **Customize** tab. Player pages and competition pages have the same control. If the entity has no upload, the graphic shows the default file of the exposure. Set the default file with `defaultFileId` on the exposure. ## Preview the working draft [Section titled “Preview the working draft”](#preview-the-working-draft) Terminal
```bash
api GET "themes/$THEME_ID/rive-graphics/$GRAPHIC_ID/preview?scenario=Ace%20on%20First%20Serve"
```
The response holds one signed URL for the runtime file and one for each stored image. Every URL expires after five minutes. `scenario` returns the demonstration data sequence of that name. Omit `scenario` to receive `null`. List the scenario names of a sport with `GET /v2/scenarios/{sport}`. ## Evaluate the bindings [Section titled “Evaluate the bindings”](#evaluate-the-bindings) Terminal
```bash
api POST "themes/$THEME_ID/rive-graphics/$GRAPHIC_ID/evaluate" \
--data '{"scenario":"Ace on First Serve","variables":{"stage":"FINAL"}}'
```
The response holds one row per data binding in `bindings` and one row per user expression in `userExpressions`, plus `unresolved`, the count of rows that failed. `mode` says what the server did. `full` means the server produced a value for each binding. `static` means the server checked syntax and references only. A static response holds no `value`. Today this route runs in `static` mode only. It checks that each binding and user expression parses, and that every `$v` and `$u` reference names a control variable or user expression that exists. It never runs the expression, so it never reports a value. To see the computed values, open the graphic in the dashboard Graphics Builder and select a match. The builder preview renders the graphic with the data of that match. Evaluate is a `POST`, so it needs `themes:write`. ## Command line [Section titled “Command line”](#command-line) Terminal
```bash
ligr-graphic rive asset file crowd.png ./crowd.png
ligr-graphic rive asset expose crowd.png --name "Crowd" --entity team --default-file ./crowd.png
ligr-graphic rive preview --scenario "Ace on First Serve" --open
ligr-graphic rive eval --scenario "Ace on First Serve" --var stage=FINAL
ligr-graphic rive push --watch
```
`rive eval` exits 0 only when every binding and user expression resolves. Use it in a pipeline. `rive push --watch` applies `rive.json` again after each save. Press Ctrl+C to stop.
# Import through REST
> Upload, prepare, inspect, select, configure, publish, activate, and connect a native Rive graphic to a control room.
This tutorial uses a dedicated example theme. It does not reuse or activate an unrelated production theme. It creates a theme, graphic, graphic version, theme snapshot, control room, section, and preset. Overlay setup also needs your test match and overlay IDs. Use a write API key with `themes:read` and `themes:write`. Keep it in `LIGR_API_KEY`; never print it. ## 1. Prepare the terminal [Section titled “1. Prepare the terminal”](#1-prepare-the-terminal) Terminal
```bash
export LIGR_API_URL="${LIGR_API_URL:-https://api.ligr.live/rest/v2}"
api() {
method=$1
path=$2
shift 2
curl --fail-with-body --silent --show-error \
-X "$method" "$LIGR_API_URL/$path" \
-H "Authorization: Bearer $LIGR_API_KEY" \
-H 'Content-Type: application/json' "$@"
}
```
Set `LIGR_API_URL` once. The CLI reads the same variable. The default REST base URL is `https://api.ligr.live/rest/v2`. Each `api` call on these pages takes a path relative to that base, for example `themes`. The API key and theme must belong to the same organization. Check capability before uploading source: Terminal
```bash
THEME=$(api POST themes --data '{"name":"Rive REST tutorial","sports":["football"]}')
THEME_ID=$(jq -er '.id' <<< "$THEME")
api GET "themes/$THEME_ID/rive-graphics/capabilities" | jq
```
If `sourceImports` is false, source preparation is unavailable in this environment. Compile externally and upload `.riv` when runtime inspection remains available. ## 2. Load the downloaded example [Section titled “2. Load the downloaded example”](#2-load-the-downloaded-example) Complete [Local RML scorebug project](/rive-graphics/local-project/) in the same terminal first. That guide downloads the public ZIP and configuration, then exports their absolute paths. Terminal
```bash
: "${PROJECT:?Complete the local RML project guide first}"
: "${UPLOAD:?Complete the local RML project guide first}"
: "${CONFIG:?Complete the local RML project guide first}"
test -f "$PROJECT/rive.yaml"
test -f "$UPLOAD"
test -f "$CONFIG"
SIZE=$(wc -c < "$UPLOAD" | tr -d ' ')
SHA256=$(shasum -a 256 "$UPLOAD" | awk '{print $1}')
```
## 3. Create and complete the upload [Section titled “3. Create and complete the upload”](#3-create-and-complete-the-upload) Choose source retention now. This example sends explicit `false`. Supplied playback images are preserved even when the original archive is not retained. Terminal
```bash
UPLOAD_SESSION=$(api POST "themes/$THEME_ID/rive-graphics/uploads" --data "$(jq -n \
--arg filename continental-scorebug.zip \
--argjson size "$SIZE" \
--arg sha256 "$SHA256" \
'{filename: $filename, size: $size, sha256: $sha256, retainSource: false}')")
JOB_ID=$(jq -er '.jobId' <<< "$UPLOAD_SESSION")
CANDIDATE_ID=$(jq -er '.candidateId' <<< "$UPLOAD_SESSION")
UPLOAD_URL=$(jq -er '.url' <<< "$UPLOAD_SESSION")
UPLOAD_CHECKSUM=$(jq -er '.headers["x-amz-checksum-sha256"]' <<< "$UPLOAD_SESSION")
UPLOAD_HEADERS=$(mktemp)
curl --fail-with-body --silent --show-error -D "$UPLOAD_HEADERS" -o /dev/null \
-X PUT "$UPLOAD_URL" \
-H "Content-Length: $SIZE" \
-H "x-amz-checksum-sha256: $UPLOAD_CHECKSUM" \
--data-binary "@$UPLOAD"
VERSION_ID=$(awk 'BEGIN{IGNORECASE=1} /^x-amz-version-id:/{gsub("\\r", "", $2); print $2}' "$UPLOAD_HEADERS")
test -n "$VERSION_ID"
rm "$UPLOAD_HEADERS"
```
Send `Content-Length` and every header returned in the upload-session `headers` map. The signature requires the checksum header, and storage rejects a body that does not match it. Send the opaque `x-amz-version-id` from the completed upload to preparation. Terminal
```bash
api POST "themes/$THEME_ID/rive-graphics/imports/$JOB_ID/prepare" \
--data "$(jq -n --arg versionId "$VERSION_ID" '{versionId: $versionId}')" | jq
```
## 4. Poll and select [Section titled “4. Poll and select”](#4-poll-and-select) Terminal
```bash
while :; do
STATUS=$(api GET "themes/$THEME_ID/rive-graphics/imports/$JOB_ID")
STATE=$(jq -er '.status' <<< "$STATUS")
printf '%s\n' "$STATE"
case "$STATE" in
ready) break ;;
selection-required)
jq '.candidates' <<< "$STATUS"
: "${SELECTED_ENTRY:?Set SELECTED_ENTRY to an exact entry returned in candidates}"
jq -e --arg entry "$SELECTED_ENTRY" '.candidates | any(.entry == $entry)' <<< "$STATUS" >/dev/null
api POST "themes/$THEME_ID/rive-graphics/imports/$JOB_ID/selection" \
--data "$(jq -n --arg entry "$SELECTED_ENTRY" '{entry: $entry}')" >/dev/null
;;
failed|cancelled|expired) jq '.error' <<< "$STATUS"; exit 1 ;;
esac
sleep 2
done
jq '{metadata, runtime, preview}' <<< "$STATUS"
```
Use an `entry` exactly as returned in `candidates`. This archive has one project. For another archive, select its returned entry explicitly before continuing. A `ready` result means preparation completed. It does not mean every external image has been supplied. Read unresolved asset status and optional default image dimensions before attachment: Terminal
```bash
jq '.metadata.assets[] | select(.unresolved)' <<< "$STATUS"
jq '.metadata.viewModels[].defaultInstanceBindings[]? |
select(.type == "image") | {path, pathSegments, imageDimensions}' <<< "$STATUS"
```
A draft can contain unresolved external images. Resolve them through image uploads or bindings in the builder. Preparation does not fetch external image URLs. Missing dimensions remain absent; do not substitute the artboard size. ## 5. Configure and create the working graphic [Section titled “5. Configure and create the working graphic”](#5-configure-and-create-the-working-graphic) The inspected fixture uses artboard index 0 and state-machine index 0. Its separate configuration binds live football data and the `stage` control variable. [The local RML project](/rive-graphics/local-project/) holds the full `selection` and `configuration` schema: `artboardIndex`, `stateMachineIndex`, `controlVariables` and `bindings`. Bind the visibility of the graphic to the reserved `hide` variable, as the example does with `{ "id": "Main.on", "expression": "!$v.hide.value" }`. The server sets `hide` on every show and hide command. The server adds `hide` to `controlVariables` and lists it there. Do not change or delete it. A graphic that binds visibility to some other variable still accepts those commands and answers `200`. That graphic never appears or disappears. See [How it works](/rive-graphics/how-it-works/). Terminal
```bash
CREATE_BODY=$(jq -n --slurpfile config "$CONFIG" \
'{name: "Continental scorebug", selection: $config[0].selection,
configuration: $config[0].configuration}')
GRAPHIC=$(api POST "themes/$THEME_ID/rive-graphics/candidates/$CANDIDATE_ID/graphics" \
--data "$CREATE_BODY")
GRAPHIC_ID=$(jq -er '.graphicId' <<< "$GRAPHIC")
ASSET_ID=$(jq -er '.assetId' <<< "$GRAPHIC")
api GET "themes/$THEME_ID/rive-graphics/$GRAPHIC_ID" | jq
api GET "themes/$THEME_ID/rive-graphics/$GRAPHIC_ID/files/metadata?version=working&assetId=$ASSET_ID" | jq
```
### Replace an existing Rive asset [Section titled “Replace an existing Rive asset”](#replace-an-existing-rive-asset) Use the same preparation and attachment endpoints for replacement. Set `GRAPHIC_ID` to your existing graphic and `UPLOAD` to the new `.rev`, `.riv`, or project ZIP. Read the working draft before creating the upload session. Terminal
```bash
WORKING=$(api GET "themes/$THEME_ID/rive-graphics/$GRAPHIC_ID")
OLD_ASSET_ID=$(jq -er '[.assets[] | select(.type == "rive")] |
if length == 1 then .[0].id else error("Select one Rive asset ID explicitly") end' <<< "$WORKING")
UPDATED_AT=$(jq -er '.updatedAt' <<< "$WORKING")
SIZE=$(wc -c < "$UPLOAD" | tr -d ' ')
SHA256=$(shasum -a 256 "$UPLOAD" | awk '{print $1}')
UPLOAD_SESSION=$(api POST "themes/$THEME_ID/rive-graphics/uploads" --data "$(jq -n \
--arg filename "$(basename "$UPLOAD")" --argjson size "$SIZE" --arg sha256 "$SHA256" \
--arg graphicId "$GRAPHIC_ID" --arg assetId "$OLD_ASSET_ID" --arg updatedAt "$UPDATED_AT" \
'{filename: $filename, size: $size, sha256: $sha256, retainSource: false,
target: {graphicId: $graphicId, assetId: $assetId, updatedAt: $updatedAt}}')")
```
Continue step 3 from the `JOB_ID` assignment, then complete step 4. The API acquires its own temporary editing lock. Do not send a dashboard lock token. Conversion does not hold that lock; attachment acquires it again and verifies the original target timestamp and asset. Inspect the prepared metadata and review your selection and binding overrides before attachment. Use the step 5 attachment endpoint and record its returned `assetId`; replacement creates a new runtime asset ID. Send only `bindings`, `inputs` and `assets` on a replacement. Control variables, user expressions and layout are edited through the operation routes; a replacement that includes them is refused with `422 INVALID_CONFIGURATION`. See [Edit through REST](/rive-graphics/edit-through-rest/). Submit reviewed `bindings`, `inputs`, and `assets` overrides for the incoming metadata instead. Compatible bindings retain their configured exposure sizes, including values that you explicitly cleared. The incoming runtime refreshes intrinsic `imageDimensions`; those dimensions do not overwrite your exposure settings. `LOCKED` means another editor currently owns the graphic. Retry after that editor releases it. `TARGET_CHANGED` means the saved draft changed during preparation. Read the new draft and start a new replacement import. If the attachment response is lost, retry the same candidate with the identical request body. An already completed attachment returns the same graphic and asset IDs. Replacement changes only the working draft. Earlier published versions retain their exact runtime, playback images, configuration, and any retained source files. Publish and activate separately when the replacement is ready. ## 6. Publish, snapshot, and activate [Section titled “6. Publish, snapshot, and activate”](#6-publish-snapshot-and-activate) Terminal
```bash
GRAPHIC_VERSION=$(api POST "themes/$THEME_ID/rive-graphics/$GRAPHIC_ID/versions" | jq -er '.version')
THEME_VERSION=$(api POST "themes/$THEME_ID/versions" --data "$(jq -n \
--arg graphicId "$GRAPHIC_ID" --argjson version "$GRAPHIC_VERSION" \
'{graphics: [{graphicId: $graphicId, version: $version}], activate: false}')" | jq -er '.version')
api PUT "themes/$THEME_ID/active-version" \
--data "$(jq -n --argjson version "$THEME_VERSION" '{version: $version}')" | jq
```
These are three separate mutations. Activation changes what new overlay page loads will render. The theme version also pins every graphic you do not list in `graphics`. Each of those graphics is pinned at its latest published version. ## 7. Create the control-room preset [Section titled “7. Create the control-room preset”](#7-create-the-control-room-preset) Terminal
```bash
ROOM=$(api POST control-rooms --data "$(jq -n --argjson themeId "$THEME_ID" \
'{themeId: $themeId, name: "Continental scorebug room"}')")
ROOM_ID=$(jq -er '.id' <<< "$ROOM")
SECTION=$(api POST "control-rooms/$ROOM_ID/sections" --data '{"name":"Native graphics"}')
SECTION_ID=$(jq -er '.id' <<< "$SECTION")
PRESET=$(api POST "control-rooms/$ROOM_ID/presets" --data "$(jq -n \
--arg sectionId "$SECTION_ID" --arg graphicId "$GRAPHIC_ID" \
'{sectionId: $sectionId, graphicId: $graphicId, name: "Continental scorebug",
variableValues: {stage: "GROUP A"}}')")
PRESET_ID=$(jq -er '.id' <<< "$PRESET")
```
Create a test match, theme profile, and manual overlay through [Set up through REST](/control-room/setup/). Use this theme and room when that guide requests `themeId` and `defaultControlRoomId`. Terminal
```bash
api POST "overlays/$OVERLAY_ID/control-room/graphics" --data "$(jq -n \
--argjson presetId "$PRESET_ID" \
'{action: "show", presetId: $presetId, variableValues: {stage: "ROUND OF 16"}}')" | jq
```
The show override changes `Main.stage`. Match facts remain authoritative for bindings under `$d`. This call fires a preset, so it carries the values an operator configured on that preset. To fire the graphic without a preset, and send every value yourself, use [Graphics commands](/control-room/graphics-commands/): `POST /v2/overlays/{overlayId}/graphics/commands` takes `command` with `graphicUuid` or `name` instead of `action` with `presetId`. Both endpoints write the same state. Pick the preset endpoint when an operator owns the values, and the command endpoint when your own system does. Use `POST /imports/{jobId}/cancel` for an unattached import you will not use. Read [Source files and privacy](/rive-graphics/source-files/) before retaining or downloading originals.
# Local RML scorebug project
> Compile the licensed Continental scorebug, inspect its native contract, and keep LIGR configuration separate.
The [complete authored project ZIP](/examples/rive-scorebug/continental-scorebug.zip) contains `rive.yaml`, `scene.rml`, the Barlow Semibold font, and their license. The 229-line `scene.rml` contains one scorebar, eight bound text fields, and one visibility layer. It starts hidden on a transparent 1920×1080 artboard. Setting `Main.on` to `true` fades and slides the scorebar into view. Setting it to `false` hides the scorebar. The project contains no scripts or shaders. ## Download the project [Section titled “Download the project”](#download-the-project) Keep one terminal open through this guide and the linked REST tutorial. The commands export absolute paths for every later step. Terminal
```bash
export DOCS_ORIGIN="${DOCS_ORIGIN:-https://docs.ligr.live}"
export TUTORIAL_DIR="${TUTORIAL_DIR:-$HOME/ligr-rive-scorebug}"
mkdir -p "$TUTORIAL_DIR/project"
export TUTORIAL_DIR="$(cd "$TUTORIAL_DIR" && pwd)"
export PROJECT="$TUTORIAL_DIR/project"
export UPLOAD="$TUTORIAL_DIR/continental-scorebug.zip"
export CONFIG="$TUTORIAL_DIR/ligr-configuration.json"
curl --fail-with-body --location \
"$DOCS_ORIGIN/examples/rive-scorebug/continental-scorebug.zip" \
--output "$UPLOAD"
curl --fail-with-body --location \
"$DOCS_ORIGIN/examples/rive-scorebug/ligr-configuration.json" \
--output "$CONFIG"
unzip -q -o "$UPLOAD" -d "$PROJECT"
```
`PROJECT`, `UPLOAD`, and `CONFIG` now contain absolute customer workspace paths. ## Compile and inspect [Section titled “Compile and inspect”](#compile-and-inspect) LIGR tests this project with Rive CLI 1.2.0. Install that version with the Rive installer on macOS or Linux: Terminal
```bash
curl -fsSL https://releases.rive.app/cli/install.sh | RIVE_VERSION=1.2.0 sh
```
For Windows and Homebrew, see [Rive CLI: Getting started](https://rive.app/docs/cli/getting-started). A Homebrew install is not under `~/.rive`. Set `RIVE_BIN` to its path. Then run the pinned compiler directly against the extracted project. Terminal
```bash
export RIVE_BIN="${RIVE_BIN:-$HOME/.rive/bin/rive}"
test "$("$RIVE_BIN" --version)" = "rive 1.2.0"
(
cd "$PROJECT"
"$RIVE_BIN" . --verify --format=json
"$RIVE_BIN" inspect . --json
"$RIVE_BIN" . --once --format=json
)
RUNTIME="$PROJECT/build/continental-scorebug.riv"
test "$(shasum -a 256 "$RUNTIME" | awk '{print $1}')" = \
99ff8f04e54e36863cbbd717ba611f38207120174740f5713027050f339889c8
```
The build produces `build/continental-scorebug.riv` with SHA-256 `99ff8f04e54e36863cbbd717ba611f38207120174740f5713027050f339889c8`. Inspection must report artboard `Main`, state machine `Broadcast`, view model `Main`, and default instance `Default`, with no problems. A different Rive CLI version can produce a different SHA-256 from the same project. The pinned browser runtime for the verified tutorial is 2.41.0. ## Learn RML and preview without a browser [Section titled “Learn RML and preview without a browser”](#learn-rml-and-preview-without-a-browser) Rive documents RML and the CLI in the [Rive CLI docs](https://rive.app/docs/cli/overview). To print the RML properties of one object type, run `rive schema `, for example `rive schema Text`. Render one frame to a PNG file to check a change without a browser: Terminal
```bash
(
cd "$PROJECT"
"$RIVE_BIN" . --screenshot=out.png --data=on=true --data=homeScore=3 --advance=2s
)
```
`--data` sets one view-model property by path. Repeat `--data` for each property. The scorebug starts hidden, so the example sets `on` to `true`. `--advance` runs the state machine for that time before the screenshot. Change the RML, run the command again, and open `out.png`. `--data` cannot set an image property. To preview an image slot, embed a sample image in the project as the placeholder. See [Images, preview and evaluate](/rive-graphics/images-and-preview/#embed-a-placeholder-for-every-bound-image). ## Keep LIGR configuration separate [Section titled “Keep LIGR configuration separate”](#keep-ligr-configuration-separate) RML defines authored properties and animation behavior. The separate [LIGR configuration](/examples/rive-scorebug/ligr-configuration.json) connects those properties to LIGR data. `hide` is bound but never declared in `controlVariables`. It is reserved: the platform injects it on every show and hide command, so your own list never carries it. Bind the visibility of the artboard to it, or show and hide answer `200` and change nothing. LIGR configuration
```json
{
"selection": { "artboardIndex": 0, "stateMachineIndex": 0 },
"configuration": {
"renderer": "webgl2",
"controlVariables": [{ "id": "stage", "name": "stage", "type": "string", "defaultValue": "FINAL" }],
"bindings": [
{ "id": "Main.on", "expression": "!$v.hide.value" },
{ "id": "Main.homeCode", "expression": "$d.1.abbreviation" },
{ "id": "Main.awayCode", "expression": "$d.2.abbreviation" },
{ "id": "Main.homeScore", "expression": "$d.1.score" },
{ "id": "Main.awayScore", "expression": "$d.2.score" },
{ "id": "Main.clock", "expression": "$d.clock" },
{ "id": "Main.period", "expression": "$d.periodAbbrv" },
{ "id": "Main.competition", "expression": "$d.competitionName" },
{ "id": "Main.stage", "expression": "$v.stage.value" }
]
}
}
```
Do not put LIGR sporting expressions into RML property definitions. REST validates each configured binding against the selected inspected runtime. The [REST tutorial](/rive-graphics/import-through-rest/) runs the upload-to-control-room sequence in your selected environment. It creates a dedicated theme and carries returned identifiers through every later request.
# Source files and privacy
> Choose source retention before upload, understand temporary processing, and download exact originals by version.
Source retention is optional for every import. It defaults off and never affects publication eligibility. For `.rev`, choose **Keep editable source in LIGR** before creating the upload. For ZIP, choose **Keep original archive in LIGR**. REST clients must send `retainSource: false` when the user leaves it unchecked. Omitting, sending `false`, or sending `null` disables retention. Raw `.riv` uploads reject `retainSource: true` with `RUNTIME_HAS_NO_EDITABLE_SOURCE`. Their immutable runtime revisions already preserve the uploaded bytes. ## Playback images and source retention [Section titled “Playback images and source retention”](#playback-images-and-source-retention) `retainSource: false` disables storage of the original editor file or complete upload archive after processing. It does not remove supplied images required for playback. LIGR stores those images with the runtime revision. Required playback files remain available after save, reload, and publication, independently of the retention choice. Missing external images remain explicitly unresolved in a draft. Known dimensions do not mean that image bytes are available. Use the builder’s image uploads or bindings to supply the missing images. ## Temporary processing [Section titled “Temporary processing”](#temporary-processing) LIGR receives editor source temporarily when it must compile that source. Users who do not want source uploaded can compile externally and upload only `.riv`. An import expires after 24 hours. Processing attempts have a 15-minute deadline. Abandoned cleanup targets approximately 24 hours, plus manager scheduling and queue delivery time. Cleanup retries after storage failures. Late upload events reopen cleanup after a previous pass. Cancelled and unattached candidates enter cleanup even when retention was selected. Do not treat the upload URL expiry as deletion proof. The upload URL lasts 15 minutes, while cleanup follows the job lifecycle above. ## Converter limits [Section titled “Converter limits”](#converter-limits) The converter accepts script-free sources and uses bundled dependencies. It does not transmit source to Rive for signing. Scripted source conversion returns `SCRIPT_SIGNING_UNSUPPORTED`. Import configuration permits up to 20,000 mapped bindings and 4 MiB of serialized data, including expanded list rows. Existing signed `.riv` runtimes remain supported. Check the selected environment’s import capabilities before uploading source. ## Retained originals [Section titled “Retained originals”](#retained-originals) Retention stores the exact uploaded `.rev` file or complete ZIP archive. LIGR never regenerates an editor file for an original download. It does not promise a byte-identical `.rev` to RML to `.rev` round trip. A retained runtime-and-assets ZIP is stored as an original archive. It is not editable source. Request metadata before requesting a file:
```http
GET /rest/v2/themes/{themeId}/rive-graphics/{graphicId}/files/metadata?version=1&assetId={assetId}
```
Use `version=working` for the current draft. Use a positive number for one exact published version. Send `assetId` when the graphic has multiple matching assets. `originalAvailable: false` means that exact revision has no retained original. LIGR never falls back to another version’s source. The builder settings menu offers **Download runtime file (.riv)** and a separate original download when that original is available. A runtime download returns the exact `.riv` bytes for the selected version. It does not embed separately hosted images or reconstruct a ZIP. Keep the version’s configuration and playback images when moving that runtime to another renderer. To tell two versions apart, compare the `sha256` field of each download. Two versions with the same `.riv` bytes have the same `sha256`.
```http
GET /rest/v2/themes/{themeId}/rive-graphics/{graphicId}/files/original?version=1&assetId={assetId}
```
The response signs the exact owner-authorized object for five minutes. The complete original ZIP is the download; there is no archive-member extraction endpoint. ## Themes that require editable source [Section titled “Themes that require editable source”](#themes-that-require-editable-source) Source stays optional by default. A theme owner can turn on **Require editable source for Rive graphics** in the theme settings. On such a theme, every import must carry editable source: a `.rev` file or a complete project ZIP. Runtime-only uploads (`.riv`, or a ZIP whose selected entry is a runtime) are refused with `SOURCE_REQUIRED`. The source is always kept, so `retainSource` is treated as `true` and the builder locks the retention choice on. `GET /v2/themes/{themeId}/rive-graphics/capabilities` reports `sourceRequired: true` for these themes. Turning the setting off later changes nothing already stored; imports made under it keep their source.
# Theme variables and data schemas
> Create theme variables and data source templates over REST, and select a data schema on a graphic.
A theme carries two resources every graphic in it can use. A theme variable holds one value an operator sets per competition, such as a colour or a sponsor name. A data source template names a data schema, such as a ladder, that an operator pushes rows into per competition. All routes sit under `/rest/v2/themes/{themeId}`. Write routes need `themes:write`. Read routes need `themes:read`. A theme another organization owns answers `404`, the same answer as a theme that does not exist. ## Theme variables [Section titled “Theme variables”](#theme-variables) Terminal
```bash
V="themes/$THEME_ID/variables"
api GET "$V"
api POST "$V" --data '{"name":"accent","type":"string","defaultValue":"#0055ff","isStyle":true}'
api POST "$V" --data '{"name":"stage","type":"enum","enumOptions":["GROUP","FINAL"],"defaultValue":"GROUP"}'
api PATCH "$V/stage" --data '{"defaultValue":"FINAL"}'
api DELETE "$V/stage"
```
Types are `string`, `number`, `boolean` and `enum`. An `enum` variable must list at least one option in `enumOptions`, and its default must be one of those options. A `number` default must parse as a number. A `boolean` default must be `"true"` or `"false"`. The id takes the value of the name, so `PATCH` and `DELETE` address the variable by its name. The name never changes. To rename a variable, delete it and create it again. Set `isStyle` for a colour, a font or another style value; the dashboard groups style variables together. `GET` answers `{ "updatedAt": "...", "variables": [...] }`. `updatedAt` is the theme’s own `updatedAt`, not one variable’s. `POST` and `PATCH` answer the variable with `updatedAt` alongside it. `DELETE` answers `200` with `{ "updatedAt": "..." }`. Send that `updatedAt` back as `ifUnchangedSince` on the next write; read [Optimistic concurrency](#optimistic-concurrency) below. A published theme version freezes the variables. Publish a new theme version to release a change to the overlays. ## Data schemas [Section titled “Data schemas”](#data-schemas) A data source template describes the rows an operator pushes. Send a CSV sample as `sample`, or send XLSX bytes as `sampleBase64`. The server parses the sample, infers the JSON schema, and stores the first rows as a preview. Terminal
```bash
D="themes/$THEME_ID/data-source-templates"
api GET "$D"
api POST "$D" --data '{"alias":"standings","sourceType":"csv","shape":"rows","sample":"team,points\nEagles,42\nHawks,38\n"}'
api PATCH "$D/$TEMPLATE_ID" --data '{"description":"League ladder"}'
api DELETE "$D/$TEMPLATE_ID"
```
The alias uses lowercase letters, digits and underscores, and it starts with a letter. It is unique in the theme. Shapes are `cell` for one value, `kv` for a key and value list, and `rows` for a table. `config` carries parse options, for example `{"headerRow":1,"dataStartRow":2}` for a `rows` sheet. Select the schema on a graphic with `PUT /v2/themes/{themeId}/rive-graphics/{graphicId}/schemas/{alias}`. Read [Edit through REST](/rive-graphics/edit-through-rest/) for the graphic operations. Delete is refused with `409 DATA_SCHEMA_IN_USE` while a graphic selects the alias. The `details` list names each graphic. Deselect the schema on each graphic, then delete the template. ## Pushing data [Section titled “Pushing data”](#pushing-data) An operator pushes rows per competition, not per theme: Terminal
```bash
api POST "data-sources/$COMPETITION_THEME_SETTING_ID/standings" \
--data '{"csv":"team,points\nEagles,42\nHawks,38\n"}'
```
That route needs `data-sources:write`. Read [External data sources](/control-room/data-sources/) for the push formats. ## Optimistic concurrency [Section titled “Optimistic concurrency”](#optimistic-concurrency) Send `ifUnchangedSince` with the `updatedAt` you last read. A theme or a template that changed since then answers `409 TARGET_CHANGED`. Read the resource again, apply your change to the new state, and send the request again. ## Command line [Section titled “Command line”](#command-line) The `graphics-cli` package wraps both resources so you can script them without curl: Terminal
```bash
npx ligr-graphic rive var list --theme $THEME_ID
npx ligr-graphic rive var add accent --type string --default '#0055ff' --style
npx ligr-graphic rive var set stage --default FINAL
npx ligr-graphic rive var rm stage
npx ligr-graphic rive data-schema list --theme $THEME_ID
npx ligr-graphic rive data-schema create standings --type csv --shape rows --sample standings.csv
npx ligr-graphic rive data-schema set $TEMPLATE_ID --description 'League ladder'
npx ligr-graphic rive data-schema rm $TEMPLATE_ID
```
Pass `--theme `, or run the command from a project folder that holds a `rive.json` with a `themeId`. Neither group needs `--graphic` or takes a lock. Add `--json` to print the raw resource instead of a summary line.