Skip to content

Import through REST

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.

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

Complete Local RML scorebug project in the same terminal first. That guide downloads the public ZIP and configuration, then exports their absolute paths.

Terminal
: "${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}')

Choose source retention now. This example sends explicit false. Supplied playback images are preserved even when the original archive is not retained.

Terminal
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
api POST "themes/$THEME_ID/rive-graphics/imports/$JOB_ID/prepare" \
--data "$(jq -n --arg versionId "$VERSION_ID" '{versionId: $versionId}')" | jq
Terminal
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
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”

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

Terminal
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

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

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

Terminal
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. Use this theme and room when that guide requests themeId and defaultControlRoomId.

Terminal
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: 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 before retaining or downloading originals.