Help Center

Advanced

API & Stream Deck

A plain-English, step-by-step guide to the control API: keys, every action, set_values, scoreboard actions, errors and Companion setup.

Anything that can send an HTTP request — an Elgato Stream Deck, Bitfocus Companion, an ATEM macro, a Python script, even a browser bookmark — can cue, take, clear and update your graphics. This page assumes you have never used an API before, so it starts from zero.

What an "API call" actually is

A request is four things: a method (almost always POST here), a URL (which channel you are talking to), headers (your key, so we know it is you), and a body (a small piece of JSON that says what to do). Press a button, a request goes out, the graphic moves. That is the whole idea.

POST   https://gfx.miri.tech/api/public/control/channel/<channel-id>/command
Header Authorization: Bearer mgfx_live_xxxxxxxx
Header Content-Type: application/json
Body   {"action":"take_next"}

JSON is just {"key":"value"}. Text goes in double quotes, numbers do not, and every pair is separated by a comma. A missing comma or a curly quote is the single most common reason a button "does nothing".

1. Create your key

  1. 1Go to Settings → API & Stream Deck → Create key.
  2. 2Name it after the machine that will use it ("Booth Stream Deck"), and scope it to one channel if that controller only ever runs one show.
  3. 3Copy the key immediately. It looks like mgfx_live_9f2c… and is shown only once — we store a hash, not the key, so it cannot be recovered.
  4. 4Paste it into your controller as the header Authorization: Bearer mgfx_live_…. If your tool cannot set that header, it can send x-api-key: mgfx_live_… instead.
Only organization owners and admins can create or revoke keys. Revoking is instant — the next request from that key fails with 401. The list shows when each key was last used, so an unused key is safe to remove.

2. Find your channel ID

Every URL below needs the channel's ID. Ask the API for it rather than hunting through the app — this one request is safe to run any time:

curl -H "Authorization: Bearer $KEY" \
  https://gfx.miri.tech/api/public/control/channels

{"channels":[{"id":"3f8c…","name":"Main","width":1920,"height":1080}]}

Then list the rundown, which gives you the item IDs, names and 1-based positions you will use in buttons:

curl -H "Authorization: Bearer $KEY" \
  https://gfx.miri.tech/api/public/control/channel/$CH/rundown

{"playlist_id":"…","items":[
  {"position":1,"item_id":"a1…","name":"Lower Third — Host"},
  {"position":2,"item_id":"b2…","name":"Score Bug"}
]}

If a channel has more than one rundown, add "playlist_id" to any command to target a different one. Otherwise the first rundown is used.

3. The commands, one by one

All of these are a POST to /api/public/control/channel/<channel-id>/command with a JSON body.

ActionWhat it doesBody
overlay.setPuts a graphic on overlay layer 1–4 (default 1). Replaces what was on that layer with a fade.{"action":"overlay.set","layer":2,"name":"Lower Third"}
overlay.toggleSame as clicking a 1–4 button: on if off, off if already on that layer.{"action":"overlay.toggle","layer":4,"position":1}
overlay.clearTakes whatever is on one layer off air.{"action":"overlay.clear","layer":2}
overlay.nextMoves a layer to the next rundown item (overlay.prev goes back).{"action":"overlay.next","layer":2}
program.clearTakes every layer off air.{"action":"program.clear"}
preview.toggleAdds/removes a graphic from Preview.{"action":"preview.toggle","position":3}
preview.clearEmpties Preview.{"action":"preview.clear"}
cue_nextMoves the cue one item down the rundown.{"action":"cue_next"}
cue_previousMoves the cue one item up.{"action":"cue_previous"}
cueCues one specific graphic. Pick it by item_id, position (1 = first) or exact name.{"action":"cue","position":3}
take (alias cue_in)Takes whatever is On Cue to air with the 500 ms fade.{"action":"take"}
take_nextCues the next item and takes it — one press.{"action":"take_next"}
take_previousCues the previous item and takes it.{"action":"take_previous"}
take_itemTakes a named graphic straight to air, wherever the cue was.{"action":"take_item","name":"Score Bug"}
clear_previewEmpties On Cue. Nothing on air changes.{"action":"clear_preview"}
clear_program (alias off)Fades the live graphic off air over 500 ms.{"action":"clear_program"}
set_valuesChanges text/data fields — see below.{"action":"set_values","values":{…}}
scoreboardRuns scoreboard actions — see below.{"action":"scoreboard","actions":[…]}
The older actions (cue, take, take_next, take_item, clear_program…) still work. Anything that "takes" acts on layer 1 unless you add "layer", and "cue" loads Preview. The state response now also includes a layers object with on_air, item_name and position for layers 1–4 — handy for lighting a button per layer.

Three ways to point at a graphic, usable on cue, take_item and the overlay actions: "item_id" (exact, never ambiguous — best for saved buttons), "position" (1-based, matches the rundown order, but shifts if someone reorders the show), and "name" (case-insensitive but must match the whole name exactly).

4. set_values — changing text live

Every template has named fields — the same names you see in the Live Data panel of the Control Room. A lower third might have title and subtitle; a countdown might have finishedText. set_values writes new text into those fields.

{
  "action": "set_values",
  "target": "cue",
  "values": { "title": "Ryan Brenneman", "subtitle": "Miri Technologies" }
}
  • target: "cue" (the default) edits the graphic sitting On Cue, so you can prepare text before taking it.
  • target: "air" edits the graphic that is already live. The change appears immediately on the output with no re-take and no fade.
  • Values merge. Sending only subtitle leaves title alone. To blank a field, send it as an empty string: "subtitle": "".
  • Every value must be text in quotes. A score of 7 is "7", not 7.

Don't know the field names? Cue the graphic once, then read the state endpoint — the preview.values object lists every field name and its current text. Those are exactly the keys set_values expects.

curl -H "Authorization: Bearer $KEY" \
  https://gfx.miri.tech/api/public/control/channel/$CH/state

{"ok":true,"on_air":true,"revision":412,
 "program":{"item_id":"a1…","item_name":"Lower Third — Host",
            "values":{"title":"Ryan Brenneman","subtitle":"Miri Technologies"}},
 "preview":{"item_id":"b2…","item_name":"Score Bug","position":2,"values":{"home":"3","away":"1"}}}

A typical "insert a guest name" button is therefore two presses, or two actions on one press: cue the lower third, set_values the name, then take.

5. scoreboard — running the game

If the channel has a scoreboard, one command can run several changes at once. actions is a list, applied in order, so "add a run and stop the clock" is a single button.

{
  "action": "scoreboard",
  "actions": [
    { "type": "score", "team": "home", "delta": 1 },
    { "type": "clock", "op": "stop" }
  ]
}

team is always "home" or "away". delta is a change (use -1 to undo a mis-press), while value sets an exact number. Clock times are in milliseconds — 12 minutes is 720000.

Action typeMeaningExample
scoreAdd to or subtract from a score.{"type":"score","team":"away","delta":2}
set-scoreSet a score exactly.{"type":"set-score","team":"home","value":14}
periodNext/previous period, quarter or inning. Resets the count or clock.{"type":"period","delta":1}
set-periodJump to a period.{"type":"set-period","value":3}
halfBaseball: top or bottom of the inning.{"type":"half","value":"bottom"}
clockGame clock. op is start, stop, toggle, reset, set (needs ms) or adjust (adds ms, may be negative).{"type":"clock","op":"set","ms":720000}
shot-clockShot clock. Same op list minus adjust.{"type":"shot-clock","op":"reset"}
countBaseball count. field is balls, strikes or outs.{"type":"count","field":"strikes","delta":1}
clear-countBalls, strikes and outs back to zero.{"type":"clear-count"}
baseToggle a runner. index 0 = first, 1 = second, 2 = third.{"type":"base","index":1}
clear-basesEmpty the diamond.{"type":"clear-bases"}
timeoutTimeouts left for a team.{"type":"timeout","team":"home","delta":-1}
foulTeam fouls.{"type":"foul","team":"away","delta":1}
shotsShots on goal.{"type":"shots","team":"home","delta":1}
setsSets won (volleyball).{"type":"sets","team":"away","delta":1}
downFootball down.{"type":"down","delta":1}
distanceYards to go.{"type":"distance","value":7}
possessionPossession arrow. null clears it.{"type":"possession","team":"home"}
statusFree text banner (HALFTIME, FINAL).{"type":"status","value":"FINAL"}
resetBack to a fresh game.{"type":"reset"}
A scoreboard fed by an external data feed cannot be driven by buttons — the command returns 409. Switch the board's control source to manual in the Control Room first.

6. What comes back

Every successful command replies with the resulting state — the same shape as the state endpoint, plus "ok": true and the action you sent. That is what makes button feedback possible: poll /state once a second and read on_air.

CodeWhat went wrongUsual fix
400Unknown action, or the body wasn't valid JSON.Check spelling and quotes.
401Key missing, mistyped or revoked.Re-check the Authorization header; create a new key.
403The key is scoped to a different channel.Use that channel's ID, or an org-wide key.
404Channel, rundown, item or scoreboard not found.Re-list the rundown; positions shift.
409Nothing cued, empty rundown, end of list, feed-driven board.Cue something first.
429More than 600 requests a minute from one key.Slow your polling down.
500Something failed on our side.Retry; the state endpoint shows the truth.

7. Bitfocus Companion, step by step

  1. 1Connections → add Generic: HTTP Requests.
  2. 2Make a button → Press actions → POST.
  3. 3URL: https://gfx.miri.tech/api/public/control/channel/<channel-id>/command
  4. 4Body type JSON, body {"action":"take_next"}.
  5. 5Custom header: Authorization = Bearer mgfx_live_…
  6. 6Duplicate the button and change only the body for cue_next, take, clear_program, and so on.
  7. 7For tally, add a second HTTP instance polling /state every second into a custom variable, then add a Boolean feedback on on_air to turn the TAKE button red while a graphic is live.

On a Stream Deck without Companion, use any HTTP-request plugin with the same method, URL, headers and body. A practical layout:

[ CUE PREV ] [ CUE NEXT ] [   TAKE   ] [ CLEAR PGM ]
[ LT HOST  ] [ SCORE BUG ] [  TICKER  ] [ COUNTDOWN ]

The bottom row each posts {"action":"cue","name":"…"}, so an operator can run the show without touching the browser.

8. Troubleshooting

  • Nothing happens, no error — your tool probably sent the body as plain text. Set the content type to JSON.
  • 401 with a key you just made — a trailing space, or the word Bearer is missing before the key.
  • Wrong graphic fires — you used position and the rundown was reordered. Switch those buttons to item_id.
  • set_values does nothing visible — you targeted cue while the graphic is on air. Use "target":"air".
  • Text appears but looks wrong — the field name is misspelled; unknown names are stored and ignored. Confirm names from preview.values.

The same reference in copy-paste form lives in docs/control-api.md in your project.