API & automation
A thin HTTP surface to drive a live show from a Stream Deck, a script, or anything that can make a web request: take a guest to an output, clear it, read the live roster, start/stop recording. It carries no new authority — it does exactly what the in-browser control panel does, gated by the same room-scoped credential. Base URL: https://castalong.me
Authentication
Every control call is bound to one room and accepts one of two credentials:
| Credential | Where | Scope |
|---|---|---|
| Producer token recommended for Stream Deck | Dashboard → a room → Producer link (the ptoken in it). Rotate any time. | That one room: route, admit, mute, recording. Nothing else. |
Account API key (ck_…) | Dashboard → Advanced setup. | All your rooms. |
Pass it any of these ways (pick one):
?token=pt_xxxxxxxx # producer token (or ck_ key) as a query param ?ptoken=pt_xxxxxxxx # explicit producer token ?key=ck_xxxxxxxx # explicit account key Authorization: Bearer pt_xxx # header (ck_ -> key, otherwise producer token)
The producer token is a capability: anyone holding it can run that room's show. Treat it like a password, share it only with your producer, and rotate it from the dashboard if it leaks. It can never touch another room or your account.
Rate limits
Control calls are rate-limited per room: ~10 requests/second sustained, burst of 20. Over that you get 429 slow down. This stops a stuck Stream Deck button or a macro loop from thrashing the show. Read calls and recording start/stop are well within this for any human-driven workflow.
Errors
Errors are plain-text bodies with a standard HTTP status. Success responses are JSON.
| Status | Meaning |
|---|---|
200 | OK — JSON body. |
400 | Bad/missing parameter (e.g. no bus, bad track). |
403 forbidden | Credential missing or not valid for this room. |
404 | No such room / recording. |
405 | Wrong method. |
429 slow down | Per-room rate limit hit; retry shortly. |
502 | The media relay was unreachable; the action didn't apply. |
GET POST /api/route
Take a guest onto an output (bus), or clear it. One request = one live switch, so a Stream Deck button maps straight to it.
| Param | |
|---|---|
room | Room slug (required). |
bus | Output name, e.g. program, facecam, game (required). |
guest | Guest display name to take. Omit (or clear=1) to clear the bus. |
track | auto (default), camera, or screen. |
transition | cut (default) or fade. |
ms | Fade duration in ms (0–10000). |
# take Alice's camera to program with a 300ms fade curl "https://castalong.me/api/route?room=my-show-ab12&bus=program&guest=Alice&track=camera&transition=fade&ms=300&token=pt_xxxxxxxx" # clear the program bus curl "https://castalong.me/api/route?room=my-show-ab12&bus=program&clear=1&token=pt_xxxxxxxx"
{ "ok": true, "bus": "program", "guest": "Alice", "track": "camera" }
GET /api/room
The live guest roster — use it to discover exact guest names for your buttons, and which tracks each guest is publishing.
curl "https://castalong.me/api/room?room=my-show-ab12&token=pt_xxxxxxxx"
{
"room": "my-show-ab12",
"guests": [
{ "name": "Alice", "identity": "alice-3f9a", "camera": true, "screen": false, "audio": true },
{ "name": "Bob", "identity": "bob-7c21", "camera": true, "screen": true, "audio": true }
]
}
POST /admit · POST /mute
/admit lets a waiting-room guest into the show (room, identity + credential). /mute stream-mutes a guest's audio in your mix without muting them for other guests (room, sid, muted=1|0 + credential). Both take the same room-scoped credential as above.
GET POST /api/lock · /api/kick
Live room access control, same room-scoped credential.
/api/lock?room=<slug>&locked=1&token=pt_…— lock the room so no new guests can join (locked=0to reopen). Connected guests stay./api/kick?room=<slug>&identity=<id>&token=pt_…— remove a guest (use /api/room to get theiridentity). They can rejoin unless the room is locked.
Discovery & recording sub-endpoints
| Endpoint | |
|---|---|
GET /token?room=&name=[&pin=] | Mint a guest join token (used by the join page). |
GET /roominfo?room= | Public: whether the room needs a PIN / has a waiting room. |
GET /myrooms?key=ck_… | Your rooms [{slug,name}] for the account key. |
POST /recordings/marker?id=<recID>&label= | Drop a chapter marker (credential). Ships in the bundle as chapters.txt. |
GET /recordings/bundle?id=<recID> | Zip of all per-guest files (session). |
GET /recordings/wav?id=<fileID> | One guest's audio as a WAV. |
Recording
POST /recordings/start · POST /recordings/stop · GET /recordings
Start records every guest to their own file (server-side ISO capture). start takes room + credential and returns a recording id; stop takes id; GET /recordings (dashboard session) lists your recordings and per-guest files, which you download from /recordings/file?id=<fileID>.
curl -X POST "https://castalong.me/recordings/start" -d "room=my-show-ab12&token=pt_xxxxxxxx"
# -> { "ok": true, "id": "rec_..." }
curl -X POST "https://castalong.me/recordings/stop" -d "id=rec_...&token=pt_xxxxxxxx"
GET /config/{token}
The per-machine install profile the OBS plugin reads (server, room, sources, control + graphics URLs, seat license). You paste a Config URL once into the dock; you won't usually call this yourself. Each call updates the install's last-seen (the seat meter).
GET /plugin/latest
Public update manifest the in-OBS updater polls. ?os=win64|linux64. Returns the newest version, a signed download URL, its sha256 and the signing key id. Every release is Ed25519-signed; the plugin verifies it before installing.
{ "version": "0.2.23", "os": "win64", "url": "https://castalong.me/plugin/…win64.zip",
"sha256": "…", "sig": "…", "kid": "k1-primary-2026-06" }
Webhooks
Register outbound webhooks under Dashboard → Integrations. Castalong POSTs JSON to your URL when these events happen:
| Event | When |
|---|---|
guest.join | a guest is issued a join token (data.room, name, identity) |
recording.started | recording begins (data.recording, room) |
recording.finished | files are ready (data.files[] with download URLs, bundle) |
recording.exported | cloud export to your bucket completed |
POST your-url
Content-Type: application/json
X-Castalong-Event: recording.finished
X-Castalong-Signature: sha256=<hex hmac of the raw body>
{ "event": "recording.finished", "at": 1718390000,
"data": { "recording": "rec_…", "room": "My Show",
"files": [{ "label": "Alice", "url": "https://castalong.me/recordings/file?id=12", "size": 8123456 }],
"bundle": "https://castalong.me/recordings/bundle?id=rec_…" } }
Verify each delivery: HMAC-SHA256 the raw request body with your webhook's signing secret and constant-time compare to the hex in X-Castalong-Signature (after the sha256= prefix). For live on-air state, still poll /api/room. Billing webhooks (provider → us) remain at POST /webhooks/billing.
Stream Deck setup
- Dashboard → your room → copy the Producer link; note the
ptokenvalue in it and your room slug. - Add a Website button (System → Website). It does a GET, which is all
/api/routeneeds. - URL:
https://castalong.me/api/route?room=<slug>&bus=program&guest=Alice&token=pt_xxxx. Tick "Access in background" so it fires without opening a browser. - One button per (guest, bus) you switch between — a "clear program" button uses
&clear=1. Use/api/roomfirst to get exact guest names.
Prefer not to build buttons by hand? Dashboard → your room → Stream Deck profile → Download gives you a ready-made .streamDeckProfile (clear-output, lock, record buttons, pre-linked to that room) to import into the Stream Deck app. If your Stream Deck version doesn't accept the import, the manual Website-button recipe above uses the exact same URLs.
Copy-paste scripts
Bash — take a guest, then clear after 5s
ROOM=my-show-ab12; TOK=pt_xxxxxxxx curl -s "https://castalong.me/api/route?room=$ROOM&bus=program&guest=Alice&token=$TOK" sleep 5 curl -s "https://castalong.me/api/route?room=$ROOM&bus=program&clear=1&token=$TOK"
PowerShell — list guests
$room="my-show-ab12"; $tok="pt_xxxxxxxx" Invoke-RestMethod "https://castalong.me/api/room?room=$room&token=$tok" | % guests
Python — rotate two cameras
import requests, time
ROOM, TOK = "my-show-ab12", "pt_xxxxxxxx"
def take(guest): requests.get("https://castalong.me/api/route",
params={"room":ROOM,"bus":"program","guest":guest,"transition":"fade","ms":300,"token":TOK})
for g in ["Alice","Bob"]*5:
take(g); time.sleep(4)