Skip to content

Push and publish

Every write on this page needs a write key. Set LIGR_API_KEY in your shell and replace 203 with your theme id.

The CLI sends requests to production by default. For another environment, set LIGR_API_URL to its full REST base, including /rest/v2, for example https://<api-origin>/rest/v2. You can also pass --base-url. The curl tutorials in Import through REST and Set up through REST read the same LIGR_API_URL. npx ligr-graphic theme list or GET /v2/themes prints the themes your organization owns.

ligr-graphic runs the calls below for you. See Quick start for the scaffold.

  1. Create the theme. Once per theme. The command prints the theme id. In a folder that holds graphic.json, it writes the theme id into ligr.json. --variables <file.json> adds theme variables from a JSON array of { id, name, type, defaultValue }.

    Terminal
    npx ligr-graphic theme create --name "My Theme" --sports football

    npx ligr-graphic theme delete --theme 203 --yes removes a theme you own and every graphic in it. A theme a competition still uses is refused. Remove the theme instance from the competition first.

  2. Create the graphic. Once per graphic. The command writes ligr.json with the theme id and the graphic id.

    Terminal
    npx ligr-graphic create --theme 203 --name "Scorebug" --sports football
  3. Push the bundle. The command builds, validates, uploads only the files whose hash changed, and records the bundle with graphic.json as the manifest.

    Terminal
    npx ligr-graphic push

    npx ligr-graphic validate runs the same checks without a push. --skip-build pushes the folder as it is.

  4. Publish the graphic, then the theme. The command publishes a graphic version. With --theme-version it also publishes a theme version pinned to it.

    Terminal
    npx ligr-graphic publish --theme-version

    Each publish creates a new graphic version, even when nothing changed since the last one.

    npx ligr-graphic status prints the working version, the published versions, the assets and the lock.

  5. Inspect and activate the published theme version. Replace 12 with the version printed above.

    Terminal
    npx ligr-graphic theme inspect --theme 203 --version 12
    npx ligr-graphic theme activate --theme 203 --version 12

    These commands select an existing snapshot. They do not publish another version or change its graphic selections.

  6. Publish one theme version after several graphics. Publish each graphic without --theme-version. Then publish one theme version that pins the latest published version of every graphic.

    Terminal
    npx ligr-graphic theme publish --theme 203 --dry-run
    npx ligr-graphic theme publish --theme 203

    --dry-run prints the version number and the graphic versions, and writes nothing. The command prints the new version, each pinned graphic version, and whether the version is active. Add --activate to set it active. A graphic with no published version is not in the theme version.

Send the key in Authorization: Bearer <api key>. The endpoints are under Code graphics and Themes in the REST reference. Create the theme and graphic once, then upload and publish each revision.

  1. Create the theme. Once per theme. Keep the id from the answer. GET /v2/themes lists the themes you own, and DELETE /v2/themes/{themeId} removes one that no competition uses.

    Terminal
    curl -X POST 'https://api.ligr.live/rest/v2/themes' \
    -H "Authorization: Bearer $LIGR_API_KEY" \
    -H 'Content-Type: application/json' \
    -d '{ "name": "My Theme", "sports": ["football"],
    "variables": [{ "id": "accent", "name": "accent", "type": "string", "defaultValue": "#ff0000" }] }'
    201 Created
    { "id": 203, "name": "My Theme", "activeVersion": null, "versions": [], "graphics": [] }

    The sixth theme returns 409 THEME_LIMIT_REACHED.

  2. Create the graphic. Once per graphic. Keep the graphicId from the answer.

    Terminal
    curl -X POST 'https://api.ligr.live/rest/v2/themes/203/code-graphics' \
    -H "Authorization: Bearer $LIGR_API_KEY" \
    -H 'Content-Type: application/json' \
    -d '{ "name": "Scorebug", "sports": ["football"] }'
    201 Created
    {
    "graphicId": "e5515527-238e-44cb-9567-b65902218d47",
    "id": 1761,
    "name": "Scorebug",
    "type": "code",
    "version": 0,
    "sports": ["football"],
    "publishedVersions": [],
    "lock": null
    }

    The answer holds two identifiers. Every later call in this guide takes graphicId, the UUID, in the path. id is the internal row number; it never goes in a path. A path with id in it answers 404.

    sports defaults to the sports of the theme. Leave out stateMachine to get the default manifest. You send the real one with the bundle.

  3. Request an upload URL for each file. One call opens one upload session.

    Terminal
    curl -X POST "https://api.ligr.live/rest/v2/themes/203/code-graphics/$GRAPHIC_ID/uploads" \
    -H "Authorization: Bearer $LIGR_API_KEY" \
    -H 'Content-Type: application/json' \
    -d '{ "files": [
    { "name": "index.html", "contentType": "text/html" },
    { "name": "assets/app.js", "contentType": "application/javascript" }
    ] }'
    200 OK
    {
    "uploadSessionId": "823d14c6-ef4e-4d72-932e-20d8ed422e3c",
    "uploads": [
    { "name": "index.html", "url": "https://…?X-Amz-Signature=…", "method": "PUT", "expiresInSeconds": 600 },
    { "name": "assets/app.js", "url": "https://…?X-Amz-Signature=…", "method": "PUT", "expiresInSeconds": 600 }
    ]
    }

    Keep the uploadSessionId. Each URL is valid for 10 minutes. One request signs at most 200 files. For more, send the same uploadSessionId in the next request. The answer keeps that id, and every URL writes into the same session. The CLI does this for you.

  4. Send each file to its URL. Use the content type you declared.

    Terminal
    curl -X PUT "$UPLOAD_URL" \
    -H 'Content-Type: text/html' \
    --data-binary @dist/index.html
  5. Record the bundle. Name every file the graphic keeps, with the manifest.

    Terminal
    curl -X PUT "https://api.ligr.live/rest/v2/themes/203/code-graphics/$GRAPHIC_ID/bundle" \
    -H "Authorization: Bearer $LIGR_API_KEY" \
    -H 'Content-Type: application/json' \
    -d '{
    "uploadSessionId": "823d14c6-ef4e-4d72-932e-20d8ed422e3c",
    "files": [
    { "name": "index.html", "mime": "text/html", "size": 1240, "hash": "sha256:6f1c…" },
    { "name": "assets/app.js", "mime": "application/javascript", "size": 8300, "hash": "sha256:9b02…" }
    ],
    "stateMachine": { "schemaVersion": 1, "runtime": { "engine": "iframe-html", "entryFile": "index.html", "width": 1920, "height": 1080, "exitDurationMs": 800 }, "controlVariables": [], "userExpressions": [] }
    }'

    The answer is the working copy of the graphic, with its assets and its publishedVersions.

  6. Publish the graphic version.

    Terminal
    curl -X POST "https://api.ligr.live/rest/v2/themes/203/code-graphics/$GRAPHIC_ID/versions" \
    -H "Authorization: Bearer $LIGR_API_KEY"
    201 Created
    { "version": 3 }
  7. Publish a theme version that holds it.

    Terminal
    curl -X POST 'https://api.ligr.live/rest/v2/themes/203/versions' \
    -H "Authorization: Bearer $LIGR_API_KEY" \
    -H 'Content-Type: application/json' \
    -d '{ "activate": false }'
    201 Created
    { "version": 12, "activeVersion": 11 }

    Inspect GET /v2/themes/203/versions/12, then select that exact snapshot:

    Terminal
    curl -X PUT 'https://api.ligr.live/rest/v2/themes/203/active-version' \
    -H "Authorization: Bearer $LIGR_API_KEY" \
    -H 'Content-Type: application/json' \
    -d '{ "version": 12 }'

    Reload the preview overlay to load the selected version. See Publish a theme version for pinning and rollback.

The first POST …/uploads call opens an upload session. Further batches can extend that session by sending its uploadSessionId. Every URL writes into that session, never directly into the graphic. A completed session cannot be reused.

PUT …/bundle moves the files of the one session you name into the graphic. It applies one rule per file in files:

The file isResult
In the named sessionThe request copies it into the working copy
Not in the session, but the graphic already holds itThe request keeps it. Skip an unchanged file: compare its hash to the one in GET …/code-graphics/{graphicId}
In neither placeThe request fails with CODE_GRAPHIC_INVALID and names the file in details[]

A file you leave out of files is deleted. Leave out uploadSessionId only when every named file is already in the graphic.

The bundle request clears the session it names when it succeeds. It leaves every other session alone. A publish never touches an upload session, so an upload URL you hold stays valid across a publish.

files is the complete file set: name, mime, size in bytes and hash for every file the graphic keeps. The API checks every size against the limits before it writes anything. The hash is yours: the API stores it and returns it, so a later push can compare and skip an unchanged file. sha256:<hex> is the usual form.

stateMachine is optional. Leave it out to keep the stored manifest. When you send it, the API validates it against the file set of this request, so runtime.entryFile must be in files. See The manifest.

A publish turns the working copy into an immutable version and opens the next working copy. The version in the answer is the number a theme version pins. GET …/code-graphics/{graphicId} lists them in publishedVersions.

A theme version is a frozen list of graphic versions. POST /v2/themes/{themeId}/versions pins every graphic you list at the version you give, and every graphic you do not list at its latest published version. A graphic with no published version is left out. Overlays load the active theme version when their page loads. Use GET …/versions/{version} to inspect a frozen selection and PUT …/active-version to activate or roll back to it.

The dashboard editor takes a lock on a graphic while a person edits it. The lock stays for up to 60 seconds after the editor closes.

RequestLock behaviour
POST …/uploadsRefuses to sign while another editor holds the lock. Does not take the lock, so a slow upload never blocks an editor
PUT …/bundleHolds the lock for the request
POST …/versionsHolds the lock for the request

A request that meets a lock answers 409 with code: "LOCKED" and holder.userName. Wait, then retry. Do not delete the graphic and create it again. The API takes the lock as API key <name>, and the same key can retake its own stale lock.

StatusCodeCause
400CODE_GRAPHIC_INVALIDThe manifest breaks a rule, or a file in files is in neither the session nor the graphic. details[] names each problem
400BUNDLE_TOO_LARGEA file is over 5 MiB, or the bundle is over 10 MiB. details[] names the files
400INVALID_ASSET_PATHA file name holds .., an empty segment, a control character, or one of ?, #, %
400GRAPHIC_TYPE_MISMATCHThe graphic is a Rive graphic, not a code graphic
400—The body fails schema validation. The answer has details per field
403INSUFFICIENT_SCOPEA read key on a write route
404—The theme or the graphic does not exist, or your organization does not own it
409LOCKEDAnother editor holds the lock
409GRAPHIC_ASSET_MISSINGA published asset or thumbnail is absent from storage. Upload the missing file, then publish again
409GRAPHIC_ASSET_INVALIDAn asset cannot be read, or its size or SHA-256 differs. Replace the file, then publish again

See Errors for the full map.

An upload batch is one request: POST …/uploads signs every file in one call. A full push of a graphic is four requests. See Rate limits.