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| Parameter | Type | Required | Default | Notes |
|---|---|---|---|---|
cards list --tag TAG | string | no | none | Match a catalogue tag. |
cards list --category CATEGORY | string | no | none | Match a catalogue category. With --tag, a card must match both. |
cards show <id> | string | yes | none | The catalogue card id from cards list, such as gauge. |
card create --type ID | string | yes | none | The catalogue card id from cards list. |
card create --major N | integer | no | current major | Pin a catalogue major version. |
card create --file FILE | path | yes | none | The static params as a JSON object of at most 1 MiB, or - for stdin. |
<id> | string | yes | none | The crd_ card id taken by card show, card params, card delete and card data. |
card params --file FILE | path | yes | none | The new static params, read as for card create. They replace the old ones at once. |
card data put --file FILE | path | yes | none | The data document as a JSON object, or - for stdin. It replaces the whole document. |
card data put --ttl DURATION | string | no | none | Blank 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 INSTANT | string | no | none | Blank 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 REVISION | string | no | none | Write 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> | string | yes | none | A catalogue card id, or a crd_ card id to draw that instance with its own params and data. |
card preview --output FILE | path | yes | none | Write the PNG here. Parent directories are created. |
card preview --params FILE | path | no | none | Static properties as a JSON object. |
card preview --data FILE | path | no | none | The dynamic document as a JSON object. |
card preview --feed FILE | path | no | none | The host feed document as a JSON object. A feed the card needs and you leave out is reported. |
card preview --width PX / --height PX | integer | no | 1920 / 1080 | Image size, 1 to 8192. |
card preview --layout LAYOUT | string | no | full | full, free, center, wide, header, footer, corner, chip or strip: the layout to draw. |
card preview --anchor ANCHOR | string | no | none | The corner of a corner or chip layout, or the edge of a strip. |
card preview --now ISO | string | no | the current time | Freeze the clock at this ISO 8601 instant. |
card preview --timezone ZONE | string | no | this machine's | Draw 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_IDCreating 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 VENDORaccepts a vendor's own signature, with--verify-secret-file FILE(or-for stdin, never an argument) holding the secret the vendor shows. Forgeneric_hmac,--verify-header NAMEnames the header that carries the signature,--verify-encodingishex(the default) orbase64,--verify-timestamp-header NAMEnames a header that carries the Unix time, and--verify-tolerance SECONDSis 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 ownsrkw_secret, which keeps working beside it. The vendor secret is stored sealed and shown only assetand ahintof its last four characters. Onkv webhook update,--verifywithout--verify-secret-filekeeps the stored secret while the vendor stays the same, and--clear-verifyremoves the verification and its secret.--verifyand--signingexclude each other.--handshake VENDORismicrosoft_graph,monday,trelloorazure_event_grid, and--clear-handshakeremoves it. Slack and Asana answer their own handshakes through--verify.
--verify | Header | What is signed | Timestamp |
|---|---|---|---|
github | X-Hub-Signature-256 | HMAC-SHA256 of the body | None |
stripe | Stripe-Signature | HMAC-SHA256 of the timestamp, a dot and the body; any v1 may match | 5 minutes |
shopify | X-Shopify-Hmac-Sha256 | HMAC-SHA256 of the body, in base64 | None |
slack | X-Slack-Signature | HMAC-SHA256 of v0:, the timestamp, a colon and the body | X-Slack-Request-Timestamp, 5 minutes |
linear | Linear-Signature | HMAC-SHA256 of the body | webhookTimestamp in the body, 1 minute |
vercel | x-vercel-signature | HMAC-SHA1 of the body | None |
asana | X-Hook-Signature | HMAC-SHA256 of the body, keyed by the handshake's X-Hook-Secret | None |
generic_hmac | The --verify-header you name | HMAC-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.
| Vendor | screenRIG answers | Later posts |
|---|---|---|
Slack (--verify slack) | The signed url_verification with its challenge; a person testing with curl can send the webhook's secret instead | Signed 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_invalid | X-Hook-Signature |
Microsoft Graph (--handshake microsoft_graph) | The decoded ?validationToken= as text/plain | Carry the webhook's secret |
monday.com (--handshake monday) | The challenge it posts, echoed as JSON | Carry the webhook's secret |
Trello (--handshake trello) | A HEAD check answers 200 with no body, for this webhook only | Carry 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].
| Vendor | In the vendor's settings |
|---|---|
| GitHub | Settings, 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 |
| Stripe | Workbench, 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 |
| Shopify | Subscribe with the url in JSON format. The secret is the app's client secret |
| Slack | The 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}} |
| Linear | In 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 |
| Vercel | Team settings, Webhooks: create one with the url. Vercel shows the secret once |
| Asana | Create 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 Graph | Create the subscription with notificationUrl set to the url |
| monday.com | Run create_webhook with the url |
| Trello | POST /1/webhooks with callbackURL set to the url |
| Azure Event Grid | Create 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:
| Reason | Meaning |
|---|---|
signature_missing | A vendor verification is set and the post carried neither the secret nor the vendor's header |
signature_invalid | The header is malformed or does not match the body |
timestamp_missing | The signature needs a timestamp the post did not carry |
timestamp_stale | The signature is valid and its timestamp is outside the window |
secret_pending | An Asana verification is still waiting for its handshake |
handshake_invalid | The 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]| Parameter | Type | Required | Default | Notes |
|---|---|---|---|---|
<webhook_id> | string | yes | none | The kvw_ webhook id. |
--card CARD_ID | string | yes | none | The crd_ card id. |
--description TEXT | string | no | none | What sends to it, at most 200 bytes. It must not hold a credential. |
--map FILE | path | no | none | Replace the mapping with a JSON object read from a file, or - for stdin. Not with --clear-map. |
--clear-map | boolean | no | off | Remove the mapping: the posted object becomes the document. |
--key-template TEMPLATE | string | no | none | Set the key template that keys the documents, such as room-{screen.field.room}. Not with --clear-key-template. |
--clear-key-template | boolean | no | off | Return to writing the card's own document. |
--enable / --disable | boolean | no | off | --disable answers every post like a wrong secret until --enable. |
--signing / --no-signing | boolean | no | off | --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 VENDOR | string | no | none | Replace 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 FILE | path | no | the stored secret | Read the vendor's secret from a file, or - for stdin (never an argument). asana takes none. Only one option reads stdin. |
--verify-header NAME | string | with generic_hmac | none | The header that carries the signature, 1 to 100 letters, digits, - or _. |
--verify-encoding ENCODING | string | no | hex | With generic_hmac: hex or base64; it may begin with sha256=. |
--verify-timestamp-header NAME | string | no | none | With generic_hmac: a header that carries the Unix time in seconds, or in milliseconds as 13 digits. |
--verify-tolerance SECONDS | integer | no | 300 | With --verify-timestamp-header: how far the timestamp may sit from the server's clock, 1 to 86400. |
--clear-verify | boolean | no | off | Stop 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 VENDOR | string | no | none | Set the handshake the URL answers: microsoft_graph, monday, trello or azure_event_grid. Not with --clear-handshake. |
--clear-handshake | boolean | no | off | Stop 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-verifyThe 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]| Parameter | Type | Required | Default | Notes |
|---|---|---|---|---|
<webhook_id> | string | yes | none | The kvw_ webhook id. |
--card CARD_ID | string | yes | none | The crd_ card id. |
--limit N | number | no | 100 | How many deliveries to return, 1 to 100. |
screenrig kv webhook deliveries kvw_01EXAMPLEWEBHOOK00000 --card crd_01EXAMPLEBOARD0000000000Each 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.