HTTP API¶
In browser mode, CroStream's whole UI runs on a small
JSON API: every button you click in the browser is a POST to
/api/<Method>, and everything that updates live arrives over one event
stream. Your own tools can use the same API: run a macro from a Stream
Deck, show the activity log on another screen, bump a counter from a
script, or watch chat from a bot. This page explains the rules, documents
the methods that are useful for automation, and lists the rest.
The API exists only in browser mode
The desktop app talks to its window directly and doesn't serve
/api/. Run crostream serve (see Browser mode) to
use the API. The desktop app and crostream serve can't run at the
same time on one computer.
Before you start: it has no login¶
The API is unauthenticated. Anyone who can reach the address
crostream serve listens on can do anything the UI can: post to chat,
ban viewers, run ads, start a raid, switch OBS scenes, change or delete
your setup. That's a deliberate choice for a tool that runs on your own
machine or home network, and it puts the safety in your hands:
- Keep the default address (
127.0.0.1:8080) unless you need other machines to reach it. Then only programs on the same computer can. - Only listen on a network you trust, such as your home LAN. Never forward the port on your router, and never listen on a public address.
- To reach it from elsewhere, use an SSH tunnel
(
ssh -L 8080:127.0.0.1:8080 <host>) or a VPN rather than opening it up. - Firewall the port on machines that move between networks (a laptop on café Wi-Fi).
- Put a login in front with a reverse proxy if you must expose it more widely; see Reverse proxies.
CroStream does protect you from other web pages and from DNS tricks:
| Check | What it stops | When it fails |
|---|---|---|
| Host allow-list | DNS rebinding: a web page pointing its own domain at your CroStream. Every request's Host header must be the listen address or a name you allowed with -host. |
421 {"error":"unexpected host"} |
| Origin check | Other web sites you visit sending requests to CroStream from your browser. A POST must carry no Origin header, or exactly http://<the Host header> (or https://<the Host header> through an HTTPS reverse proxy on this machine). |
403 {"error":"forbidden origin"} |
| JSON only | Plain HTML forms from other sites, which can't send application/json. |
415 |
Tools like curl, Stream Deck plugins and scripts send no Origin
header, so they pass. Browsers always send one, so a web page on another
site can't drive CroStream.
Calling a method¶
Every method is a POST to /api/<Method> with
Content-Type: application/json and a body that is a JSON array of the
method's arguments, in order. A method without arguments takes [].
A successful call answers 200 with the result under "result"
(null for methods that return nothing):
{"result": [{"id": "twitch", "status": {"state": "connected", "accounts": [{"role": "broadcaster", "login": "crothers"}]}}, …]}
A failed call answers with an error status and a message under "error":
| Status | Meaning |
|---|---|
400 |
The body isn't a JSON array, has the wrong number of arguments, or an argument has the wrong type: {"error":"bad arguments: want 1, got 0"} |
403 |
Forbidden origin (see above) |
404 |
Unknown method |
405 |
Not a POST |
413 |
The body is too large: 64 KB for most methods, more for imports and saves |
415 |
The content type isn't application/json |
421 |
Unexpected host (see above) |
422 |
The method ran and failed, with the reason: {"error":"no connected provider for scene.set"} |
503 |
Too many event streams are open |
Arguments are decoded strictly: objects may not have fields the method
doesn't know, and null is only accepted where an argument is a map
(meaning "empty").
The event stream¶
GET /api/events is a Server-Sent Events
stream of everything that changes. It starts with a status event
holding every integration's status, then sends events as they happen, and
a : ping comment every 15 seconds to keep the connection open.
event: status
data: [{"id":"twitch","status":{"state":"connected",…}},{"id":"obs",…}]
event: feed
data: {"seq":412,"time":"2026-10-11T19:02:11Z","kind":"triggered","source":"twitch","rule":"!lurk","user":"PixelPanda",…}
| Event | Data | Sent when |
|---|---|---|
status |
Every integration's status: [{id, status: {state, detail, error, url, accounts}}] |
An integration connects, disconnects, fails or changes |
feed |
One activity line (the same shape as GetFeed) |
Every line of the activity feed: runs, step results, events, notices |
chat |
One chat message: {id, time, userId, login, name, role, color, badges, text, emotes, first, deleted} |
Every chat message |
auth |
The login warnings now showing: [{integration, name, login, kind, level, expiresAt, reason, key, acknowledged}] |
A login is about to expire, failed to renew, or expired |
overlay:live |
{overlay, state} (an overlay's live overrides) or {data} (changed overlay data keys) |
Every overlay change and data update |
At most 16 streams can be open at once (the UI uses one per browser tab).
A client that reads too slowly is dropped and should reconnect; browsers'
EventSource and most SSE libraries do that by themselves.
Methods for automation¶
Macros, triggers and queues are identified by IDs such as
seq-01m4nr3cqyykwt94nac1zkhpm6 (macros), bind-… (triggers) and
queue-…. IDs never change when you rename something, so they're safe to
put in a button. Look them up once with the list methods below, or in the
settings file.
All examples use http://127.0.0.1:8080; replace it with your address.
To keep them short, they assume this helper:
cs() { curl -s -X POST "http://127.0.0.1:8080/api/$1" -H 'Content-Type: application/json' -d "${2:-[]}"; }
GetStatus¶
GetStatus() returns every integration's status, as the status event
does. state is connected, connecting, disconnected, awaiting
(waiting for you to log in), disabled or error.
GetSequences¶
GetSequences() lists your macros: [{id, name, steps, error}], where
steps are short summaries and error says why a macro can't run.
seq-01m4nqwwap513e991pfg4hfsdx clip
seq-01m4nr3cqyykwt94nac1zkhpm6 Airhorn
seq-01m4nr3cqa2tva4n3aypmpxr1r BRB
GetSequenceDefs() returns the full definitions instead, as stored in the
settings file.
RunSequence¶
RunSequence(id) runs a macro now, as if you clicked Run on it. It
runs as you (the broadcaster, when Twitch is logged in), with no trigger
involved: no roles, cooldowns or redemption to settle. Only {user},
{user_role} and {rule} are set.
The call returns as soon as the run starts; the outcome appears in the
activity feed (the feed event, or GetLog with the run's
ID). It fails with 422 when the macro doesn't exist or can't run.
GetBindings and TestBinding¶
GetBindings() lists your triggers: [{id, name, source, trigger,
sequenceId, sequenceName, disabled, error, warning}], where trigger is
a readable description such as Chat command !lurk · everyone · 30s per user.
TestBinding(id) does what Test does on the Triggers page: runs the
trigger's macro with a sample event, ignoring roles and cooldowns and
never settling a redemption. Like a real run it returns at once.
RunAction and RunActionOn¶
RunAction(action, params) runs one action right away, without a macro,
like the Actions page does. RunActionOn(provider, action, params)
runs it on one integration only (for actions more than one provides, such
as clip on obs or twitch). Unlike RunSequence, these wait for the
action to finish and fail with 422 and its message when it fails.
# Switch OBS to the BRB scene
cs RunAction '["scene.set", {"scene": "BRB"}]'
# Say something in chat
cs RunAction '["chat.send", {"message": "Back in 5! Stretch break."}]'
# Add a death
cs RunAction '["tools.counter", {"name": "deaths", "change": "add", "amount": 1}]'
# Start a 5 minute countdown named "break"
cs RunAction '["overlay.timer", {"timer": "break", "do": "start", "amount": "5m"}]'
# Save a replay clip only (not a Twitch clip)
cs RunActionOn '["obs", "clip", {}]'
Parameters are the action's own, with the names the
settings file uses. Values
are text, numbers or booleans as the action defines them; {variables}
in text are left as typed, since there's no event to fill them from.
GetCatalog() lists every action with its parameters:
chat.send: message
scene.set: scene
tools.counter: name, change, amount
twitch.timeout: user, duration, reason
…
See Actions for what each one does.
GetFeed¶
GetFeed() returns the recent activity feed, oldest first: the lines the
Activity page shows live, the same shape as the feed event.
{
"seq": 2,
"time": "2026-10-11T15:09:15.066Z",
"kind": "action_ok",
"source": "tools",
"rule": "quick action",
"sequence": "quick action",
"step": "action",
"provider": "tools",
"action": "tools.counter",
"detail": "deaths = 1",
"run": "run-01m4nr5kdtwp72re2xe48ek5b0"
}
kind is triggered, action_ok, action_error, finished,
notice, event and a few more; level (info, warn, error) is set
on notices; run groups the lines of one run.
GetLog¶
GetLog(query) searches the stored activity log (the
History page's log), newest first. Every field
of the query is optional:
| Field | Meaning |
|---|---|
kinds |
Line kinds, such as ["action_error"] |
levels |
error, warn, info |
sources |
Integration IDs, such as ["obs"] |
text |
Matches the detail, message, trigger, macro, action, user, step or provider, ignoring case |
rule, run, event |
Exact trigger name, run ID or event ID |
since, until |
RFC 3339 times; since inclusive, until exclusive |
beforeId, afterId |
Page back, or poll forward, by line id |
limit |
Default 200, at most 1,000 |
# The last 5 failures
cs GetLog '[{"levels": ["error"], "limit": 5}]' | jq -r '.result[] | "\(.time) \(.rule): \(.message // .detail)"'
# Everything one run did
cs GetLog '[{"run": "run-01m4nr5kdtwp72re2xe48ek5b0"}]'
To follow the log from a script, remember the highest id you've seen
and ask for {"afterId": <id>} every few seconds, or listen to the
feed event.
GetChatFeed¶
GetChatFeed() returns the last 200 chat messages, oldest first, in the
shape of the chat event. GetChatLog(query) searches stored chat.
OverlayData, OverlayList and OverlayResetLive¶
OverlayData() returns every overlay data key and its
value: chat, events, counters, timers, leaderboards and so on.
OverlayList() lists your overlays (id, name, size, …);
OverlayLive(id) returns what macros changed on one (shown or hidden
elements, replaced texts); OverlayResetLive(id) drops those changes so
the design shows again.
GetIntegrationState¶
GetIntegrationState(id) returns an integration's live state, or null.
For queue it lists your queues with what's waiting:
obs gives the current scene, the scene list and the streaming and
recording state; steam the game you're playing and your library;
discord your status.
Invoke¶
Invoke(id, command, args) runs an integration command, the buttons on
its page: connect and disconnect for obs; login, logout and
cancel_login for twitch. args is an object of strings, usually {}.
login doesn't open a browser in browser mode: the status (the status
event, or GetStatus) switches to awaiting with the login page in
url. See Twitch login from another machine.
ReloadConfig¶
ReloadConfig() re-reads the settings file after you
edit it by hand, like Reload on the Triggers and Macros pages.
Viewers and points¶
GetViewers(query) pages through the viewer database
({"search": "lunar", "limit": 10}; sort, following, subscriber,
activeDays and offset also work). Each viewer has an id (their
Twitch user ID), login, name, points, watchSeconds, messages,
bits and more. AdjustViewerPoints(id, change) changes a balance, as
the Viewers page does; mode is add, subtract or set, and a
balance never goes below zero:
Recipes¶
An incoming webhook is safer for buttons
To run a macro from a button on another device, an incoming webhook is usually a better fit than this API: it has its own secret, runs only the macros you attach to it, and can't change your setup. The recipes below use the API because it can run any action.
Bitfocus Companion¶
Companion's Generic: HTTP Requests connection can call the API from any Stream Deck, X-keys or other control surface it supports.
- Add a Generic: HTTP Requests connection. Set its base URL to your
CroStream address, such as
http://192.168.1.50:8080. - Create a button. Add a POST action with:
- URL:
/api/RunSequence - Body:
["seq-01m4nr3cqyykwt94nac1zkhpm6"] - Body content type:
application/json
- URL:
- Press it. The macro runs, and the Activity page shows the run.
Use /api/RunAction with a body such as
["scene.set", {"scene": "BRB"}] for one-off actions. If Companion runs
on another computer, CroStream must listen on an address it can reach
(see Browser mode).
Elgato Stream Deck¶
Stream Deck's built-in Website action only opens pages (a GET), so
it can't call the API. Use a plugin that sends HTTP requests with a body,
such as a "web request" or "API" plugin from the Stream Deck store, and
give it:
| Setting | Value |
|---|---|
| Method | POST |
| URL | http://127.0.0.1:8080/api/RunSequence |
| Header | Content-Type: application/json |
| Body | ["seq-01m4nr3cqyykwt94nac1zkhpm6"] |
Pinned macros on the Dashboard and the pop-out Controls window are a no-setup alternative on a second screen or tablet.
A script that reacts to chat¶
Any language with an SSE client works. In Python, with only the standard library:
import json
import urllib.request
BASE = "http://127.0.0.1:8080"
def call(method, *args):
req = urllib.request.Request(
f"{BASE}/api/{method}",
data=json.dumps(list(args)).encode(),
headers={"Content-Type": "application/json"},
)
return json.load(urllib.request.urlopen(req))["result"]
event = None
with urllib.request.urlopen(f"{BASE}/api/events") as stream:
for raw in stream:
line = raw.decode().rstrip("\n")
if line.startswith("event: "):
event = line[7:]
elif line.startswith("data: ") and event == "chat":
msg = json.loads(line[6:])
if msg["text"].strip().lower() == "!death":
call("RunAction", "tools.counter", {"name": "deaths", "change": "add", "amount": 1})
For most chat commands a trigger is simpler; use the API when your logic lives outside CroStream.
All methods¶
The browser UI uses every method below. Signatures are
Method(argument types) → result.
Status, integrations and actions
| Method | Arguments | Returns |
|---|---|---|
GetManifests |
none | Every integration with its settings, commands, trigger and action types |
GetStatus |
none | Every integration's status |
GetSystem |
none | {os, arch} of the machine CroStream runs on |
GetSettings |
id | An integration's settings (secrets as {"set": true}) |
SaveSettings |
id, values | Saves an integration's settings |
Invoke |
id, command, args | Runs an integration command |
GetOptions |
id, source | Choices for a field, such as OBS scenes |
GetIntegrationState |
id | An integration's live state |
GetCatalog |
none | Every action with its providers and parameters |
GetFacts |
none | The live values macro conditions can check |
RunAction |
action, params | Runs one action |
RunActionOn |
provider, action, params | Runs one action on one provider |
ConfigDir |
none | The config folder's path |
OpenConfigDir |
none | Opens the config folder on the CroStream machine, where possible |
Macros, triggers, queues and the settings file
| Method | Arguments | Returns |
|---|---|---|
GetSequences |
none | Macros, summarized |
GetSequenceDefs |
none | Macros, in full |
SaveSequence |
macro | Creates (empty id) or updates a macro |
DeleteSequence |
id | Deletes a macro no trigger uses |
RunSequence |
id | Runs a macro |
GetBindings |
none | Triggers, summarized |
GetBindingDefs |
none | Triggers, in full |
SaveBinding |
trigger | Creates (empty id) or updates a trigger |
DeleteBinding |
id | Deletes a trigger |
TestBinding |
id | Runs a trigger's macro with a sample event |
GetQueueDefs |
none | Queues |
SaveQueue |
queue | Creates (empty id) or updates a queue |
DeleteQueue |
id | Deletes a queue nothing uses |
ReloadConfig |
none | Re-reads the settings file |
GetBackups |
none | Automatic backups of the settings file |
RestoreBackup |
name | Restores one (backing up the current file first) |
ExportBundle |
selection | The text of an export file |
PreviewImport |
text | What importing it would do |
ImportBundle |
text, mode (copy or replace) |
Imports it |
GetDashboardSettings |
none | The pinned macros |
SaveDashboardSettings |
settings | Saves them |
Activity, history and chat
| Method | Arguments | Returns |
|---|---|---|
GetFeed |
none | The recent activity feed |
GetChatFeed |
none | The last 200 chat messages |
GetLog |
query | Stored log lines |
GetLogStats |
since, until | Totals for a period |
ExportLog |
query, format | The log as text |
GetActivity |
query | The Activity page's items |
GetSeries |
query | Chart data |
GetHistory |
query | Stored events |
GetHistoryStats |
since, until | Event totals |
GetHistoryInfo |
none | Whether history is on, and its size |
SaveHistorySettings |
settings | Turns history on or off, sets how long it's kept |
ClearHistory |
none | Deletes all stored history |
GetChatLog |
query | Stored chat |
ExportChat |
query, format | Chat as text |
GetAuthAlerts |
none | The login warnings showing now |
AcknowledgeAuthAlert |
key | Clicks through a login warning |
DismissAuthBanner |
key | Hides an early warning until midnight |
Webhooks
| Method | Arguments | Returns |
|---|---|---|
GetWebhookSettings |
none | Who may reach incoming webhooks (exposure) and the port |
SaveWebhookSettings |
settings | Saves them; the listener restarts |
GetWebhookURLs |
none | The base addresses incoming webhooks can be reached at |
GetInboundHooks |
none | Incoming webhooks |
SaveInboundHook |
webhook | Creates (empty id) or updates one; makes the secrets it needs |
DeleteInboundHook |
id | Deletes one no trigger uses |
GetHookSecret |
id | Its token and signing key |
RegenerateHookSecret |
id | New ones; the old stop working |
SetHookSecret |
id, kind (token or hmac), value |
Sets one yourself |
GetHookDeliveries |
id | Its last 20 calls, newest first |
PreviewHookMapping |
format, body, rules | What the rules make of the body, and its shape |
SendHookTest |
id, body, content type | Delivers a signed test call through the real pipeline |
GetDestinations |
none | Outgoing destinations |
SaveDestination |
destination | Creates (empty id) or updates one |
DeleteDestination |
id | Deletes one no macro uses |
GetDestinationSecretState |
id | {"set": true} or false: secrets can't be read back |
SetDestinationSecret |
id, secret | Saves it; an empty one removes it |
TestDestination |
id, method, path, body type, body | Sends one request and returns the answer |
PreviewJSONBuild |
fields, variables | The JSON a Send a webhook body builds |
The API has no login, so GetHookSecret returns tokens to anyone who can
reach it. Keep it on your own PC or network, as above. Webhooks
themselves are not served by this API but by their own listener; see
Webhooks.
Crash reporting
| Method | Arguments | Returns |
|---|---|---|
GetDiagnostics |
none | The crash reporting state: the saved settings (reports, tracing, replay), whether it is available, forced (development build) or killed_by_env, whether it is active, and who reports are sent as (user, or null while off) |
SaveDiagnostics |
settings | Saves the three choices, applies them at once and returns the new state |
In browser mode the page's own reports go to POST /sentry/envelope,
which CroStream cleans and forwards; it isn't a method to call by hand.
Viewers
| Method | Arguments | Returns |
|---|---|---|
GetViewers |
query | A page of viewers |
GetViewer |
id | One viewer in detail |
GetViewerActivity |
id, query | Their activity |
GetViewerChat |
id, query | Their chat |
GetViewerLedger |
id, query | Their points history |
AdjustViewerPoints |
id, change | Changes their points |
UpdateViewer |
id, patch | Changes notes, ignore and similar |
DeleteViewer |
id, also history | Deletes a viewer |
GetViewersInfo |
none | Totals and settings |
SaveViewerSettings |
settings | Saves them |
ResetViewerPoints |
confirmation text | Sets every balance to zero |
ImportViewers |
import | Imports points from another bot |
ExportViewers |
format | Exports the database |
SyncFollowers |
none | Fetches your followers from Twitch |
Overlays and widgets
| Method | Arguments | Returns |
|---|---|---|
OverlayList |
none | Overlays |
OverlayGet |
id | One overlay |
OverlayCreate |
name, width, height | A new overlay |
OverlayTemplates |
none | Overlay templates |
OverlayCreateFromTemplate |
template, name, width, height | A new overlay from a template |
OverlaySave |
overlay | Saves it (refuses a stale rev) |
OverlayDuplicate |
id, name | A copy |
OverlayDelete |
id | Deletes it |
OverlayWidgets |
none | Every kind of element |
OverlayLive |
id | Its live overrides |
OverlayData |
none | All overlay data |
OverlayResetLive |
id | Drops its live overrides |
OverlayTextPresets, OverlaySaveTextPreset, OverlayDeleteTextPreset |
Text effect presets | |
OverlayFramePresets, OverlaySaveFramePreset, OverlayDeleteFramePreset |
Frame presets | |
WidgetLibrary |
none | Built-in and custom widgets |
WidgetGet |
id | A custom widget |
WidgetCreate |
name | A new blank widget |
WidgetClone |
source (builtin:<kind> or a widget ID), name |
A copy |
WidgetSave |
widget | Saves it (refuses a stale version) |
WidgetDelete |
id | Moves it to the trash |
WidgetHistory |
id | Its saved versions |
WidgetRestore |
id, version | Restores one |
WidgetExport |
id | Its .slwidget file contents |
WidgetImport |
file | Adds it |
WidgetUsage |
id | The overlays that place it |
Media library and fonts
| Method | Arguments | Returns |
|---|---|---|
LibraryList |
query | Items in a folder or search |
LibraryTree |
none | The folder tree |
LibraryItem |
id | One item |
LibraryStats |
none | Totals |
LibraryCreateFolder, LibraryRenameFolder, LibraryMoveFolder |
Folders | |
LibraryRenameItem, LibrarySetNotes, LibraryMoveItems |
Items | |
LibraryTags, LibrarySaveTag, LibraryDeleteTag, LibraryTagItems |
Tags | |
LibraryTrash, LibraryRestore, LibraryDeleteForever, LibraryEmptyTrash |
The trash | |
LibrarySetProbe |
id, probe | Stores a video's size, length and poster |
FontCatalog, FontInstall |
The free font catalog | |
SystemFonts |
rescan | Fonts installed on the machine |
LibraryFonts, LibraryFontsFolder |
none | Fonts in the library |
Files are uploaded with a multipart POST /media/upload?folder=<folder id>
(one file field per file), under the same Origin rule. Reading files
from the CroStream machine's disk is desktop-only and not part of the API.
Methods not listed here (file dialogs, the updater, tray and window settings, pop-out windows) belong to the desktop app and aren't available over HTTP.
Related¶
- Browser mode: running
crostream serveand its options. - Headless setup: a streaming PC with no screen, and reverse proxies.
- Overlay data reference: the data
OverlayDatareturns. - The settings file: the shapes of macros, triggers and queues.