Documentation

Cards CLI commands

Find card types, create a card, set its parameters and live data, preview it and update its data from outside systems through signed webhooks.

Every command here runs as described in the CLI reference: global flags, JSON output, exit codes, revisions and retries.

Cards

Discover card types, create an instance, and update its live data. A playlist places the returned card ID.

screenrig cards list --category ops
screenrig cards show gauge
screenrig card create --type gauge --file gauge-params.json
screenrig card list
screenrig card show crd_01EXAMPLEGAUGE000000000
screenrig card params crd_01EXAMPLEGAUGE000000000 --file gauge-params.json
screenrig card data get crd_01EXAMPLEGAUGE000000000
screenrig card data put crd_01EXAMPLEGAUGE000000000 --file gauge-data.json --ttl 30m --expect-rev 1
screenrig card preview crd_01EXAMPLEGAUGE000000000 --output gauge.png
screenrig card delete crd_01EXAMPLEGAUGE000000000
ParameterTypeRequiredDefaultNotes
cards list --tag TAGstringnononeMatch a catalogue tag.
cards list --category CATEGORYstringnononeMatch a catalogue category. With --tag, a card must match both.
cards show <id>stringyesnoneThe catalogue card id from cards list, such as gauge.
card create --type IDstringyesnoneThe catalogue card id from cards list.
card create --major Nintegernocurrent majorPin a catalogue major version.
card create --file FILEpathyesnoneThe static params as a JSON object of at most 1 MiB, or - for stdin.
<id>stringyesnoneThe crd_ card id taken by card show, card params, card delete and card data.
card params --file FILEpathyesnoneThe new static params, read as for card create. They replace the old ones at once.
card data put --file FILEpathyesnoneThe data document as a JSON object, or - for stdin. It replaces the whole document.
card data put --ttl DURATIONstringnononeBlank the data after this long, offline screens included: 30m, 12h or 1d, from 1s to 365d. Not with --expires-at.
card data put --expires-at INSTANTstringnononeBlank the data at this ISO 8601 timestamp, which carries a time zone. A write with neither option clears the old expiry.
card data put --expect-rev REVISIONstringnononeWrite only if the data is still at this revision, from card data get. A stale value is revision_conflict (exit 6).
card preview <type|id>stringyesnoneA catalogue card id, or a crd_ card id to draw that instance with its own params and data.
card preview --output FILEpathyesnoneWrite the PNG here. Parent directories are created.
card preview --params FILEpathnononeStatic properties as a JSON object.
card preview --data FILEpathnononeThe dynamic document as a JSON object.
card preview --feed FILEpathnononeThe host feed document as a JSON object. A feed the card needs and you leave out is reported.
card preview --width PX / --height PXintegerno1920 / 1080Image size, 1 to 8192.
card preview --layout LAYOUTstringnofullfull, free, center, wide, header, footer, corner, chip or strip: the layout to draw.
card preview --anchor ANCHORstringnononeThe corner of a corner or chip layout, or the edge of a strip.
card preview --now ISOstringnothe current timeFreeze the clock at this ISO 8601 instant.
card preview --timezone ZONEstringnothis machine'sDraw in this IANA time zone, such as America/Vancouver.

cards show prints the card's params_schema and data_schema, its static and dynamic properties, tier, layouts and feed, and an example to copy into a playlist page. card create returns the instance's crd_ id, and an identical rerun after a lost answer reuses the saved idempotency key.

card data put replaces the document at once, without republishing the playlist. The card's own validation refuses a document it cannot show (card_data_invalid), and an expiry blanks the data on every screen when nothing renews it. card delete removes the instance and its data, and answers resource_conflict (exit 5) while a playlist places it.

card preview draws the card in a local Chromium or Chrome and writes a PNG; it publishes nothing. Set SCREENRIG_BROWSER to the browser's path when none of the usual ones is installed. data.file is the path written and data.unrendered lists what the preview could not draw, such as a host feed you did not supply. Open the image before you publish. Built-in cards lists each card's layouts, properties and feeds.

Card webhooks

A card webhook is a URL that writes the data document of one card, so an outside system such as a CMS, a point of sale or a script pushes data to a card with one HTTP POST. kv webhook create, list, rotate-secret and delete take --card CARD_ID in place of --app-id, never both; update and deliveries below work on cards only. A card has at most 20 webhooks.

kv webhook create --card CARD_ID [--description TEXT] [--map FILE|-] [--key-template TEMPLATE] [--signing]
                  [--verify VENDOR [--verify-secret-file FILE|-] [--verify-header NAME] [--verify-encoding hex|base64]
                   [--verify-timestamp-header NAME] [--verify-tolerance SECONDS]] [--handshake VENDOR]
kv webhook list --card CARD_ID
kv webhook rotate-secret <webhook_id> --card CARD_ID [--signing]
kv webhook delete <webhook_id> --card CARD_ID

Creating a card webhook takes no key. --map FILE (or - for stdin) is the mapping, --key-template writes keyed documents, --signing turns signing on, and --verify and --handshake accept a vendor's own posts (Vendor signatures and handshakes). The answer carries data.secret and, when the webhook signs, data.signing_secret, each shown once.

The sender authenticates as for an application webhook: Authorization: Bearer <secret> with the srkw_… secret, or HTTP Basic with the webhook id as the username, so https://kvw_ID:srkw_…@api.screenrig.ai/… suits a sender that takes only a URL. The body, at most 1 MiB, is JSON or application/x-www-form-urlencoded; a repeated form name becomes an array. Without a mapping the posted object is the card's data document.

A mapping is a JSON template that builds the document from the body. A string with placeholders such as {{body.alert.title}} or {{body.items[0].name}} is text, and a string that is exactly one placeholder keeps the value's type. A path starts with body or query (the URL's query parameters), and a path that holds nothing is empty in text and leaves a lone value out. $each maps an array with item and index in scope, $group with $by groups in first-seen order, $number reads 1,234.50, $3.50, 3,50 € or 12% (add "$decimal": "," for a comma mark), $bool reads true, yes, y, 1 and x, and $text with $max cuts text. The template is checked when saved: invalid_request names the place that failed.

{
  "title": "{{body.alert.title}}",
  "body": "{{body.alert.message}}",
  "items": { "$each": "{{body.items}}", "$limit": 20, "$template": { "text": "{{item.name}}" } }
}
curl https://api.screenrig.ai/api/cards/crd_01EXAMPLEBOARD0000000000/kv-webhooks/kvw_01EXAMPLEWEBHOOK00000/write \
  -H "Authorization: Bearer $CARD_WEBHOOK_SECRET" \
  -H "Content-Type: application/json" \
  --data '{"alert":{"title":"Fire drill","message":"Leave by the east door"},"items":[{"name":"Floor 2"}]}'

--key-template TEMPLATE writes keyed documents in place of the card's own document. The template is a card data key with {screen.field.NAME} tokens, such as room-{screen.field.room}, and the mapped object (or the posted body, without a mapping) holds only a documents array of {fields, document} objects: each fields object fills the template and each document is written under the key it makes. A page item that gives the same template as its data shows each screen the document of its own fields. Every document is checked before any is written, and a repeated key is refused. A storage failure can leave part of a batch written: retry the identical body with the same Idempotency-Key. Keys a post omits keep their data, and a webhook sets no expiry. Keyed documents shows a mapping.

Every post is checked as the card's own data: card_data_invalid names the path and nothing is written. A document equal to the card's current one answers unchanged and writes nothing, so no screen is woken. An accepted post does not count toward the agent request cap, a webhook takes at most 120 posts a minute, and an Idempotency-Key header deduplicates a sender's retries for 24 hours.

A wrong address, a wrong or missing secret, a disabled webhook, a missing or wrong signature and every GET answer the same 404, so a link preview or a guess learns nothing. kv webhook deliveries says why.

With --signing, every post must also carry ScreenRig-Signature: t=<unix seconds>,v1=<hex>, where v1 is the HMAC-SHA256 of t, a dot and the raw body under the whsec_… signing secret, and t must be within five minutes of the server's clock. It is the format of screenRIG's own webhook signatures.

Vendor signatures and handshakes

Vendors sign their posts their own way, and some cannot send your secret at all. A card webhook can verify the vendor's signature in place of the secret and answer the vendor's setup handshake, so GitHub, Stripe, Shopify, Slack, Linear, Vercel, Asana, Microsoft Graph, monday.com, Trello and Azure Event Grid post straight to it with no relay in between. Any other sender that signs with HMAC works too. Set them with --verify and --handshake on kv webhook create, and change them with kv webhook update.

  • --verify VENDOR accepts a vendor's own signature, with --verify-secret-file FILE (or - for stdin, never an argument) holding the secret the vendor shows. For generic_hmac, --verify-header NAME names the header that carries the signature, --verify-encoding is hex (the default) or base64, --verify-timestamp-header NAME names a header that carries the Unix time, and --verify-tolerance SECONDS is how far it may sit from the server's clock (300 by default). A post that carries the vendor's valid signature is accepted without the webhook's own srkw_ secret, which keeps working beside it. The vendor secret is stored sealed and shown only as set and a hint of its last four characters. On kv webhook update, --verify without --verify-secret-file keeps the stored secret while the vendor stays the same, and --clear-verify removes the verification and its secret. --verify and --signing exclude each other.
  • --handshake VENDOR is microsoft_graph, monday, trello or azure_event_grid, and --clear-handshake removes it. Slack and Asana answer their own handshakes through --verify.
--verifyHeaderWhat is signedTimestamp
githubX-Hub-Signature-256HMAC-SHA256 of the bodyNone
stripeStripe-SignatureHMAC-SHA256 of the timestamp, a dot and the body; any v1 may match5 minutes
shopifyX-Shopify-Hmac-Sha256HMAC-SHA256 of the body, in base64None
slackX-Slack-SignatureHMAC-SHA256 of v0:, the timestamp, a colon and the bodyX-Slack-Request-Timestamp, 5 minutes
linearLinear-SignatureHMAC-SHA256 of the bodywebhookTimestamp in the body, 1 minute
vercelx-vercel-signatureHMAC-SHA1 of the bodyNone
asanaX-Hook-SignatureHMAC-SHA256 of the body, keyed by the handshake's X-Hook-SecretNone
generic_hmacThe --verify-header you nameHMAC-SHA256 of the body, or with a --verify-timestamp-header of the timestamp, a dot and the body--verify-tolerance

The key of every HMAC is the secret's text as the vendor shows it (Stripe's includes whsec_). A signature is checked on the body exactly as it arrived, in constant time, before the post takes any write budget, and it is judged before its timestamp: timestamp_stale always means the secret was right. A body may be JSON, a form or multipart/form-data, where a text field is body.FIELD and files are counted, not read.

VendorscreenRIG answersLater posts
Slack (--verify slack)The signed url_verification with its challenge; a person testing with curl can send the webhook's secret insteadSigned with the signing secret
Asana (--verify asana)The handshake's X-Hook-Secret, which it keeps sealed as the key of every X-Hook-Signature. The same secret again is a retry; a different one is refused as handshake_invalidX-Hook-Signature
Microsoft Graph (--handshake microsoft_graph)The decoded ?validationToken= as text/plainCarry the webhook's secret
monday.com (--handshake monday)The challenge it posts, echoed as JSONCarry the webhook's secret
Trello (--handshake trello)A HEAD check answers 200 with no body, for this webhook onlyCarry the webhook's secret
Azure Event Grid (--handshake azure_event_grid){"validationResponse": "CODE"}Carry the webhook's secret

A handshake is answered only for a webhook set up for that vendor, before any credential is asked for, to a request that is that handshake, and never by a disabled webhook. It writes nothing, is never billed and spends no rate budget. Microsoft Graph, monday.com, Trello and Azure Event Grid send no header screenRIG verifies, so their deliveries are accepted only with the webhook's secret. Azure Event Grid takes it as a static delivery header, Authorization: Bearer srkw_…, marked secret, and its events arrive as an array, so a mapping reads body[0].

VendorIn the vendor's settings
GitHubSettings, Webhooks, Add webhook: Payload URL is the webhook's url, content type application/json, and the same secret. GitHub's ping is signed and accepted
StripeWorkbench, Webhooks: add the url and reveal its signing secret. While Stripe rolls a secret it sends a v1 for each and either is accepted; replace the stored secret afterwards with kv webhook update --verify stripe --verify-secret-file FILE
ShopifySubscribe with the url in JSON format. The secret is the app's client secret
SlackThe Events API Request URL is the url, and the secret is the app's signing secret. A slash command posts a form: {{body.text}}, {{body.user_name}}
LinearIn the API settings create a webhook with the url and copy its signing secret. The body's webhookTimestamp must be within a minute of the server's clock
VercelTeam settings, Webhooks: create one with the url. Vercel shows the secret once
AsanaCreate the webhook with --verify asana: it has no secret yet and refuses events as secret_pending. Then create the Asana webhook with target set to the url, and the handshake sets the secret. To take a new secret, run kv webhook update with --clear-verify, then again with --verify asana
Microsoft GraphCreate the subscription with notificationUrl set to the url
monday.comRun create_webhook with the url
TrelloPOST /1/webhooks with callbackURL set to the url
Azure Event GridCreate the event subscription with the url as its webhook endpoint, and add the delivery header above
echo '{"title":"{{body.repository.full_name}}","body":"{{body.head_commit.message}}"}' |
  screenrig kv webhook create --card crd_01EXAMPLEBOARD0000000000 --description "Pushes" \
    --verify github --verify-secret-file github.secret --map -

A sender that signs with its own HMAC names the header, the encoding and, if it signs a timestamp, the timestamp header:

echo '{"title":"{{body.sensor}}","body":"{{body.state}}"}' |
  screenrig kv webhook create --card crd_01EXAMPLEBOARD0000000000 --verify generic_hmac --verify-secret-file acme.secret \
    --verify-header X-Acme-Signature --verify-encoding base64 --verify-timestamp-header X-Acme-Time --verify-tolerance 120 \
    --map -
ts=$(date +%s); body='{"sensor":"door","state":"open"}'
sig=$(printf '%s.%s' "$ts" "$body" | openssl dgst -sha256 -hmac "$SECRET" -binary | openssl base64 -A)
curl "$URL" -H "Content-Type: application/json" -H "X-Acme-Time: $ts" -H "X-Acme-Signature: $sig" -d "$body"

Every failure is the same 404 as any other refusal. kv webhook deliveries shows the reason, and a handshake shows as outcome handshake:

ReasonMeaning
signature_missingA vendor verification is set and the post carried neither the secret nor the vendor's header
signature_invalidThe header is malformed or does not match the body
timestamp_missingThe signature needs a timestamp the post did not carry
timestamp_staleThe signature is valid and its timestamp is outside the window
secret_pendingAn Asana verification is still waiting for its handshake
handshake_invalidThe request is a vendor's handshake and cannot be answered

kv webhook update#

Change a card webhook: its description, mapping, keyed documents, enabled state, signing, vendor verification or handshake.

kv webhook update <webhook_id> --card CARD_ID [--description TEXT] [--map FILE|-] [--clear-map]
                   [--key-template TEMPLATE] [--clear-key-template] [--enable|--disable] [--signing|--no-signing]
                   [--verify VENDOR [--verify-secret-file FILE|-] [--verify-header NAME] [--verify-encoding hex|base64]
                    [--verify-timestamp-header NAME] [--verify-tolerance SECONDS]] [--clear-verify]
                   [--handshake VENDOR] [--clear-handshake]
ParameterTypeRequiredDefaultNotes
<webhook_id>stringyesnoneThe kvw_ webhook id.
--card CARD_IDstringyesnoneThe crd_ card id.
--description TEXTstringnononeWhat sends to it, at most 200 bytes. It must not hold a credential.
--map FILEpathnononeReplace the mapping with a JSON object read from a file, or - for stdin. Not with --clear-map.
--clear-mapbooleannooffRemove the mapping: the posted object becomes the document.
--key-template TEMPLATEstringnononeSet the key template that keys the documents, such as room-{screen.field.room}. Not with --clear-key-template.
--clear-key-templatebooleannooffReturn to writing the card's own document.
--enable / --disablebooleannooff--disable answers every post like a wrong secret until --enable.
--signing / --no-signingbooleannooff--signing requires the signature header and prints the signing secret once; a webhook that already signs keeps its secret, which kv webhook rotate-secret --signing replaces. --no-signing stops requiring it. Not with --verify.
--verify VENDORstringnononeReplace the vendor verification: github, stripe, shopify, slack, linear, vercel, asana or generic_hmac. Without --verify-secret-file the stored secret is kept when the vendor is the same, so a header or a window can change alone; a different vendor needs its own. Not with --clear-verify.
--verify-secret-file FILEpathnothe stored secretRead the vendor's secret from a file, or - for stdin (never an argument). asana takes none. Only one option reads stdin.
--verify-header NAMEstringwith generic_hmacnoneThe header that carries the signature, 1 to 100 letters, digits, - or _.
--verify-encoding ENCODINGstringnohexWith generic_hmac: hex or base64; it may begin with sha256=.
--verify-timestamp-header NAMEstringnononeWith generic_hmac: a header that carries the Unix time in seconds, or in milliseconds as 13 digits.
--verify-tolerance SECONDSintegerno300With --verify-timestamp-header: how far the timestamp may sit from the server's clock, 1 to 86400.
--clear-verifybooleannooffStop accepting the vendor's signature and forget its secret. An Asana verification takes a new handshake only after --clear-verify and --verify asana again.
--handshake VENDORstringnononeSet the handshake the URL answers: microsoft_graph, monday, trello or azure_event_grid. Not with --clear-handshake.
--clear-handshakebooleannooffStop answering the vendor handshake.

At least one change is required.

screenrig kv webhook update kvw_01EXAMPLEWEBHOOK00000 --card crd_01EXAMPLEBOARD0000000000 --map alert-map.json
screenrig kv webhook update kvw_01EXAMPLEWEBHOOK00000 --card crd_01EXAMPLEBOARD0000000000 --verify stripe --verify-secret-file stripe.secret
screenrig kv webhook update kvw_01EXAMPLEWEBHOOK00000 --card crd_01EXAMPLEBOARD0000000000 --clear-verify

The envelope is the webhook. data.signing_secret appears once, in the answer that turned signing on. After an interrupted or ambiguous update, rerun the identical command: it reuses the saved idempotency key.

kv webhook deliveries#

Read a card webhook's delivery log, newest first, for the last seven days.

kv webhook deliveries <webhook_id> --card CARD_ID [--limit N]
ParameterTypeRequiredDefaultNotes
<webhook_id>stringyesnoneThe kvw_ webhook id.
--card CARD_IDstringyesnoneThe crd_ card id.
--limit Nnumberno100How many deliveries to return, 1 to 100.
screenrig kv webhook deliveries kvw_01EXAMPLEWEBHOOK00000 --card crd_01EXAMPLEBOARD0000000000

Each item has at, outcome (written, unchanged, refused, invalid or handshake), reason (credentials, signature for a failed ScreenRig-Signature, disabled, body, mapping, card_data_invalid, rate_limited, internal, or a vendor reason from the table above), detail, status, bytes, files (the file parts of a multipart post, which are ignored), revision and request_id. No body, header or credential is stored, and a post to a wrong address leaves no row.

Other command groups