Castalong

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:

CredentialWhereScope
Producer token recommended for Stream DeckDashboard → 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.

StatusMeaning
200OK — JSON body.
400Bad/missing parameter (e.g. no bus, bad track).
403 forbiddenCredential missing or not valid for this room.
404No such room / recording.
405Wrong method.
429 slow downPer-room rate limit hit; retry shortly.
502The 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
roomRoom slug (required).
busOutput name, e.g. program, facecam, game (required).
guestGuest display name to take. Omit (or clear=1) to clear the bus.
trackauto (default), camera, or screen.
transitioncut (default) or fade.
msFade 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=0 to reopen). Connected guests stay.
  • /api/kick?room=<slug>&identity=<id>&token=pt_… — remove a guest (use /api/room to get their identity). 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:

EventWhen
guest.joina guest is issued a join token (data.room, name, identity)
recording.startedrecording begins (data.recording, room)
recording.finishedfiles are ready (data.files[] with download URLs, bundle)
recording.exportedcloud 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

  1. Dashboard → your room → copy the Producer link; note the ptoken value in it and your room slug.
  2. Add a Website button (System → Website). It does a GET, which is all /api/route needs.
  3. 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.
  4. One button per (guest, bus) you switch between — a "clear program" button uses &clear=1. Use /api/room first 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)