Skip to content

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 [].

curl -s -X POST http://127.0.0.1:8080/api/GetStatus \
  -H 'Content-Type: application/json' -d '[]'

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.

curl -sN http://127.0.0.1:8080/api/events
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.

cs GetStatus | jq -r '.result[] | "\(.id): \(.status.state)"'
twitch: connected
obs: connected
steam: disabled
…

GetSequences

GetSequences() lists your macros: [{id, name, steps, error}], where steps are short summaries and error says why a macro can't run.

cs GetSequences | jq -r '.result[] | "\(.id)  \(.name)"'
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.

cs RunSequence '["seq-01m4nr3cqyykwt94nac1zkhpm6"]'
{"result": null}

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.

cs TestBinding '["bind-01m4nr3chxcwr2jb4t4paxrjbc"]'

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:

cs GetCatalog | jq -r '.result[] | "\(.action): \([.params[]?.key] | join(", "))"'
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.

cs OverlayData | jq '.result["leaders.bits.stream"]'

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:

cs GetIntegrationState '["queue"]' | jq '.result[] | {name, waiting, paused}'
{"name": "Song requests", "waiting": 3, "paused": false}

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 {}.

cs Invoke '["obs", "connect", {}]'

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.

cs ReloadConfig

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:

cs AdjustViewerPoints '["1004", {"mode": "add", "amount": 500, "reason": "Won the stream quiz"}]'

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.

  1. Add a Generic: HTTP Requests connection. Set its base URL to your CroStream address, such as http://192.168.1.50:8080.
  2. Create a button. Add a POST action with:
    • URL: /api/RunSequence
    • Body: ["seq-01m4nr3cqyykwt94nac1zkhpm6"]
    • Body content type: application/json
  3. 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.