referencegenerated — do not edit

GENERATED from trax-public-api /v1 openapi.json (baked copy: scripts/specs/v1-openapi.json) by scripts/sync-generated.sh — do not edit by hand. Baked copy. Refresh with: cp ../trax-public-api/internal/openapi/openapi.json scripts/specs/v1-openapi.json && npm run sync.

TRaX Developer API — /v1 reference

This page is generated from the /v1 OpenAPI 3.0 spec. The live, machine-readable contract is served at https://api.traxstreaming.live/v1/openapi.json — point Postman, Insomnia, or a codegen tool at that URL. This page is a browsable rendering of the same spec (version 1.16.0).

The versioned public developer API for TRaX. Authenticate one of two ways: (1) a user-scoped API key — Authorization: Bearer sk_live_... (or the X-API-Key alias) — for server-to-server integrations; or (2) an interactive-login user JWT — Authorization: Bearer <oidc-jwt> — obtained from an OAuth Authorization-Code + PKCE login against the TRaX IdP, the path for native iOS/Android apps. A user JWT acts as that user with their full access; per-tenant ownership is always enforced. Every response carries an X-Request-Id. Errors share one envelope: { "error": { "code", "message", "requestId" } }. List endpoints are cursor-paginated: { "data": [ ... ], "nextCursor": "..." }. Phases 1-3 are LIVE: reads + gated go-live (Phase 1), studio/source/destination CRUD + connections (Phase 2), and chat + the media library (Phase 3) — including the full upload lifecycle, large files in presigned parts, and attaching a library asset to a studio by id. The audio mixer and the CANVAS are live too: read the whole canvas in one conditional call, move a tile, pick a preset, recall a scene, or hand the arrangement back to the auto planner. Phase 8 (#597) adds the phone go-live path: POST /v1/studios/{id}/contribute provisions a device's input and its publish credential in one idempotent call, and POST /v1/studios/{id}/monitor mints the return feed. The reads a client polls — stream-status, the source list, and a single source — support ETag / If-None-Match and answer 304, which matters on a cellular link. Mutating endpoints are gated behind PUBLIC_API_WRITE_ENABLED and return 503 write_disabled while off. See trax-dev/docs/public-api-platform-design.md.

At a glance

  • Base URL: https://api.traxstreaming.live — every path below is absolute (e.g. GET /v1/studios).
  • Authentication: Authorization: Bearer sk_live_... (or the X-API-Key header), or an interactive-login user JWT (Authorization: Bearer <oidc-jwt>, the native-app OAuth + PKCE path) — see Authentication. The meta endpoints (/v1/health, /v1/openapi.json) need no auth.
  • Errors: one envelope, { "error": { "code", "message", "requestId" } } — see Errors & status codes.
  • Lists: cursor-paginated, { "data": [ … ], "nextCursor": "…" } — see Pagination.
  • Rate limits: per-key token bucket, 429 + Retry-After — see Rate limits.

Endpoints

meta

Health + spec (unauthenticated)

GET /v1/health

Liveness probe (unauthenticated).

Auth: none (public).

Responses:

Status Description Body
200 Service is up. Health
default Error envelope. Error

GET /v1/openapi.json

This OpenAPI 3.0 document (unauthenticated).

Auth: none (public).

Responses:

Status Description Body
200 The OpenAPI spec. object
default Error envelope. Error

studios

Studios, sources, destinations, stream status

GET /v1/studios

List the caller's studios.

Requires scope studios:read.

Auth: API key required.

Parameters:

Name In Required Description
cursor query no Opaque pagination cursor from a prior response's nextCursor.
limit query no Max items per page (default 50, max 200).

Responses:

Status Description Body
200 A page of studios. StudioList
default Error envelope. Error

POST /v1/studios

Create a studio.

Requires scope studios:write. Gated behind PUBLIC_API_WRITE_ENABLED.

Auth: API key required.

Responses:

Status Description Body
201 The created studio. Studio
default Error envelope. Error

GET /v1/studios/{id}

Get one studio.

Requires scope studios:read.

Auth: API key required.

Parameters:

Name In Required Description
id path yes Studio id.

Responses:

Status Description Body
200 The studio. Studio
default Error envelope. Error

PATCH /v1/studios/{id}

Update a studio (name / auto-go-live settings).

Requires scope studios:write. Gated behind PUBLIC_API_WRITE_ENABLED. There is no studio delete on the public API.

Auth: API key required.

Parameters:

Name In Required Description
id path yes Studio id.

Responses:

Status Description Body
200 The updated studio. Studio
default Error envelope. Error

GET /v1/studios/{id}/audio

Read the studio's audio mix.

Requires scope audio:read.

The whole mix in one call: the master strip plus one strip per source. One call, not one per source, because a mixer surface draws every fader at once and asking per source would let the strips disagree with each other mid-render.

Every source row gets a strip, including sources with no audio track. The strip is per-source state that persists and replays to the encoder, so hiding rows here would make this API's idea of "the mix" narrower than the operator's own panel. Read active and type to decide what your UI draws.

A source nobody has touched in the mixer reports the RUNNING defaults — volume 1.0, unmuted, centre, trim 1.0, no groups — not an absence. Those are the values the encoder is applying to it, so reporting them is a measurement rather than a guess.

volume is a LINEAR gain, not decibels: 0.0 silent, 1.0 unity (0 dB), above 1.0 amplifies, ceiling 10.0. pan is -1.0 full left … +1.0 full right. trim is the pre-fader input gain, linear, 1.0 nominal.

MUTE IS NOT VOLUME ZERO. muted is an independent gate and the fader keeps its position underneath it, so unmuting returns to exactly the level the operator was at.

Auth: API key required.

Parameters:

Name In Required Description
id path yes Studio id.

Responses:

Status Description Body
200 The studio's mix. StudioAudio
default Error envelope. Error

PATCH /v1/studios/{id}/audio/master

Set the master level or mute.

Requires scope audio:write. Gated behind PUBLIC_API_WRITE_ENABLED.

A PATCH: send only the fields you are changing, and the rest keep their current values. Sending {"muted": true} does not move the master fader.

Muting the master silences the program for every viewer on every destination while leaving each source's own strip untouched. To silence one contributor, patch that source instead.

volume is the same LINEAR scale as a channel fader: 0.0 silent, 1.0 unity, ceiling 10.0. A value outside the range is a 400, not a clamp — a client that believed it was sending decibels should find out.

The master strip has a fader and a mute and nothing else, which is the actual shape of the master strip in the studio.

Auth: API key required.

Parameters:

Name In Required Description
id path yes Studio id.

Responses:

Status Description Body
200 The updated master strip. AudioMaster
default Error envelope. Error

PATCH /v1/studios/{id}/audio/sources/{sourceId}

Set one source's level, mute, pan, solo or trim.

Requires scope audio:write. Gated behind PUBLIC_API_WRITE_ENABLED.

A PATCH: send only the fields you are changing. {"muted": true} mutes the channel and leaves the level, pan, solo and trim exactly where they were — which is what lets a phone mute a mic without knowing, or clobbering, the mix an operator set on a laptop.

SET muted; DO NOT write volume: 0 and call it mute. Mute is an independent gate and the fader keeps its position underneath, so unmuting returns to the level the operator was at. Collapsing the two loses that position.

This reaches the same control path the TRaX studio's own mixer uses. On a live studio the change is immediate and audible, and every open studio tab re-renders the fader. On an idle studio it persists and applies at the next go-live.

Units: volume LINEAR 0.0–10.0 (1.0 = unity, 0 dB — not decibels), pan -1.0–1.0, trim LINEAR 0.1–16.0. Anything outside those ranges is a 400 rather than a silent clamp.

soloed is a mixing-desk intent flag synced across the studio's operators; it does not by itself mute the other channels.

muteGroups is read-only — it is returned by the mix read but cannot be set here, because the group MASTER button that gives membership its meaning lives in the studio's mixer panel.

A sourceId from a studio you cannot see answers 404 studio not found, identical to a source that does not exist.

Auth: API key required.

Parameters:

Name In Required Description
id path yes Studio id.
sourceId path yes Source id.

Responses:

Status Description Body
200 The updated channel strip, in full. AudioSourceStrip
default Error envelope. Error

POST /v1/studios/{id}/contribute

Put a device on a studio and get its publish credential.

Requires scopes sources:ingest AND sources:write. Gated behind PUBLIC_API_WRITE_ENABLED.

This is the one call a phone cannot go live without. It provisions — or re-finds — this device's input on the studio and hands back the credential to publish into it, in a single round trip. If it succeeds you can start pushing video; if it fails there is no stream, and nothing else on this API changes that.

It needs BOTH scopes because it does two separable things: it CREATES an input on the studio (sources:write) and it hands back a LIVE PUBLISH CREDENTIAL (sources:ingest). Those two deliberately do not imply each other in either direction, so a key holding only one would be doing half of this without having asked for that half. Tick both when you mint the key.

IDEMPOTENT per (owner, studio, deviceId) — and that is a contract, not an implementation detail you should avoid relying on. Send the same deviceId and you get the same input, the same media path and the same credential, every time. 201 the first time, 200 on every call after. A phone on a bad uplink can send this three times and end up with one input.

Retrying is SAFE, including after a timeout where you never saw the response. Nothing here rotates a credential, so a retry cannot invalidate the key the timed-out attempt already issued. That is why rotation is a separate endpoint with its own name: POST /v1/studios/{id}/sources/{sourceId}/ingest/rotate destroys the old credential deliberately, and this call never does it by accident.

Same writer as the TRaX app's own contribute flow, so an input provisioned here and one provisioned in the app are the same row with the same reconnection behaviour — there is no second provisioning path to drift from.

ingestHost and ingestMetro tell your user where they are publishing. ingestMetro is "auto" today because the nearest point of presence is chosen in DNS when your device resolves the hostname, not by this call — display it, do not branch on it.

Rate limited in the credential-handout class, roughly 5 per minute per caller with a small burst. A real client calls this once per session and again on reconnect. On 429 the response carries Retry-After; wait that long rather than guessing.

Answers 503 contribute_disabled when the deployment has the device-contribute path switched off. That is not a transient failure and it carries no Retry-After — retrying will never make it succeed.

Auth: API key required.

Parameters:

Name In Required Description
id path yes Studio id.

Responses:

Status Description Body
200 This device already had an input on this studio. Same input, same credential — your earlier call created it. Contribution
201 The input was created by this call. Contribution
default Error envelope. Error

GET /v1/studios/{id}/destinations

List a studio's destinations.

Requires scope destinations:read.

Auth: API key required.

Parameters:

Name In Required Description
id path yes Studio id.
cursor query no Opaque pagination cursor from a prior response's nextCursor.
limit query no Max items per page (default 50, max 200).

Responses:

Status Description Body
200 A page of destinations. DestinationList
default Error envelope. Error

POST /v1/studios/{id}/destinations

Add a destination to a studio.

Requires scope destinations:write. Gated behind PUBLIC_API_WRITE_ENABLED. The stream key is write-only and never returned.

Auth: API key required.

Parameters:

Name In Required Description
id path yes Studio id.

Responses:

Status Description Body
201 The created destination. Destination
default Error envelope. Error

DELETE /v1/studios/{id}/destinations/{destId}

Remove a destination.

Requires scope destinations:write. Gated behind PUBLIC_API_WRITE_ENABLED.

Auth: API key required.

Parameters:

Name In Required Description
id path yes Studio id.
destId path yes Destination id.

Responses:

Status Description Body
200 The destination was removed. Ok
default Error envelope. Error

PATCH /v1/studios/{id}/destinations/{destId}

Update a destination.

Requires scope destinations:write. Gated behind PUBLIC_API_WRITE_ENABLED.

Auth: API key required.

Parameters:

Name In Required Description
id path yes Studio id.
destId path yes Destination id.

Responses:

Status Description Body
200 The updated destination. Destination
default Error envelope. Error

GET /v1/studios/{id}/events

Stream studio state changes (Server-Sent Events).

The push channel that replaces polling. Open it once and you are told when something changes, instead of asking every few seconds whether anything did — which is what a phone on cellular should not be spending its uplink on while it is also publishing.

Requires scopes stream:read, sources:read and destinations:read — the same three permissions the three reads it carries require. No new scope: a stream that carries exactly what those GETs return needs exactly what they need.

Format. text/event-stream, one event per frame:

id: 9f2c1a4b7e3d-12
event: source
data: {"id":"...","publishing":true, ...}

Event types and their data, which are the SAME shapes the equivalent GETs return:

event data
snapshot { sources: Source[], streamStatus: StreamStatus, destinations: Destination[], chatConnectors: ChatConnector[] }
source a Source — created, changed, or its publishing flipped
streamStatus a StreamStatus
destination a Destination
chatConnector a ChatConnector — a linked platform's reconnect pill state changed
chatConnectorRemoved { platform } — a platform was unlinked; drop its connector
canvas a CanvasState — the layout mode, active preset/scene, and per-source placements all in one; replace your whole canvas
audio a StudioAudio — the master strip plus one strip per source; replace your whole mixer
sourceRemoved { id } — drop that source
destinationRemoved { id } — drop that destination

Apply an event by replacing the row it names. There is no partial-update form: a client that applies every event ends up identical to one that re-read the collections.

Snapshot first. The first event on a fresh stream is a snapshot of full current state, so opening this stream IS your initial read — you do not need to GET anything to prime yourself.

Resuming. Every event carries an opaque id. Reconnect with the standard Last-Event-ID header (or ?lastEventId=) set to the last id you processed and the server replays what you missed. The replay window is bounded, so an id the server cannot honour is NOT an error: you get a fresh snapshot and carry on. Always send your last id; never fall back to re-GETting the collections. Treat the id as opaque — do not parse or compare it.

Keepalives. On an idle studio the server sends SSE comment lines (: heartbeat <RFC3339>), which keep cellular NATs and proxies from reaping the connection. Comments are not events; a standard EventSource client ignores them for you. If you write your own client, use them to reset a dead-stream timer.

Limits. At most 5 concurrent event streams per credential (429 with Retry-After beyond that), and at most 32 concurrent watchers on one studio. The stream ENDS when your access does — deleting the studio or revoking the key terminates it rather than leaving a socket attached to something that is gone.

Auth: API key required.

Parameters:

Name In Required Description
id path yes Studio id.
Last-Event-ID header no The id of the last event you processed, to resume after a dropped connection. An id the server cannot honour yields a fresh snapshot rather than an error, so it is always safe to send.
lastEventId query no Query alias for the Last-Event-ID header, for clients that cannot set it.

Responses:

Status Description Body
200 The event stream. Stays open until the client disconnects or access ends.
default Error envelope. Error

POST /v1/studios/{id}/monitor

Get the return feed — watch the program while you publish into it.

Requires scope viewer:tokens. Gated behind PUBLIC_API_WRITE_ENABLED.

Issues a short-lived, read-only credential for the studio's PROGRAM output: what the show is actually sending, right now. It is the app's version of the operator's program monitor, and it is what tells the person holding the camera that their shot is on air.

The scope is viewer:tokens rather than a :read scope for a reason worth knowing before you mint a key. This hands back a credential that PLAYS THE PROGRAM — a media credential, not a row of JSON — and stream:read is a DEFAULT scope that every casually created key carries forever. viewer:tokens is the scope this platform already uses for minting media playback credentials, it is opt-in by name, and any key holding it can already mint a video.play token for the same studio through POST /v1/viewer-tokens. So requiring it here grants nothing your key could not already do; it just refuses to give this power away to a key that never asked for it.

SHORT-LIVED and re-mintable: 300 seconds by default, 900 maximum, clamped rather than refused. That costs less than it sounds. The media server authorises a read once, at session ESTABLISHMENT, and never re-checks — so a monitor that is already playing keeps playing past expiresAt. Mint again to (re)CONNECT, not to keep watching, and schedule from expiresAt rather than from the TTL you requested.

Take the WHEP path: whep.url with Authorization: Bearer whep.token on the signaling POST. WebRTC survives a lossy cellular uplink far better than the alternative. srt is there for clients that cannot speak WHEP and is null when a dialable URL cannot be built — the credential rides inside SRT's 512-byte streamid cap, and a URL over it is rejected by the client's own SRT library before it reaches the network, so the server declines to hand back one that cannot work.

The credential is always issued when you may hold it. Whether FRAMES flow depends on the studio being live with its program preview published — a monitor opened off-air simply shows nothing yet. That is a state to render, not an error to report.

Same rate-limit class as the credential endpoints, roughly 5 per minute per caller with a small burst.

Auth: API key required.

Parameters:

Name In Required Description
id path yes Studio id.

Responses:

Status Description Body
200 The return-feed credential. Monitor
default Error envelope. Error

GET /v1/studios/{id}/sources

List a studio's sources.

Requires scope sources:read.

Auth: API key required.

Parameters:

Name In Required Description
id path yes Studio id.
cursor query no Opaque pagination cursor from a prior response's nextCursor.
limit query no Max items per page (default 50, max 200).

Responses:

Status Description Body
200 A page of sources. SourceList
default Error envelope. Error

POST /v1/studios/{id}/sources

Add a source to a studio.

Requires scope sources:write. Gated behind PUBLIC_API_WRITE_ENABLED.

To put a library file on the canvas, send mediaAssetId with a library-backed type (video-file, image, audio-file, media) and no url — the server resolves the encoder-facing URL itself and refreshes it as it expires, so you never hold a storage credential. url and mediaAssetId are mutually exclusive; sending both is a 400.

For a live input (generic-input, srt, rtmp) send neither and read the publish endpoints back from POST /v1/studios/{id}/sources/{sourceId}/ingest.

Auth: API key required.

Parameters:

Name In Required Description
id path yes Studio id.

Responses:

Status Description Body
201 The created source. Source
default Error envelope. Error

DELETE /v1/studios/{id}/sources/{sourceId}

Remove a source.

Requires scope sources:write. Gated behind PUBLIC_API_WRITE_ENABLED.

Auth: API key required.

Parameters:

Name In Required Description
id path yes Studio id.
sourceId path yes Source id.

Responses:

Status Description Body
200 The source was removed. Ok
default Error envelope. Error

GET /v1/studios/{id}/sources/{sourceId}

Read one source.

Requires scope sources:read.

The same object GET /v1/studios/{id}/sources returns, for one input. It exists to be POLLED: after a device dials the URLs from POST .../ingest, this is how the device confirms the media actually arrived.

Read publishing, not status. status is the control-plane provisioning state and says OK for a perfectly configured input that nobody is publishing to — it has never meant "live". publishing is media-plane truth: is a publisher attached to this input's path right now. It is the same signal the TRaX studio's own source LEDs burn, so the API and the UI cannot disagree.

publishing is nullable, and null is a THIRD answer. It means the liveness tracker could not be reached on this request: unknown, not offline. A poll loop must keep asking; treating null as "my stream is down" is exactly the wrong move during a blip.

publishedAt is when the CURRENT publish began, not when we last noticed it — it holds still while the publisher stays connected, so you can render "live for 4m" from it, and a NEW value means the publisher dropped and came back.

A sensible poll is every 2 seconds while waiting to go live, then stop. Liveness normally appears within a couple of seconds of the encoder's handshake, and it clears within about 15 seconds of the publisher going away.

A sourceId from a studio you cannot see answers 404 studio not found, identical to a studio that does not exist.

Auth: API key required.

Parameters:

Name In Required Description
id path yes Studio id.
sourceId path yes Source id.

Responses:

Status Description Body
200 The source. Source
default Error envelope. Error

PATCH /v1/studios/{id}/sources/{sourceId}

Update a source.

Requires scope sources:write. Gated behind PUBLIC_API_WRITE_ENABLED.

Auth: API key required.

Parameters:

Name In Required Description
id path yes Studio id.
sourceId path yes Source id.

Responses:

Status Description Body
200 The updated source. Source
default Error envelope. Error

POST /v1/studios/{id}/sources/{sourceId}/ingest

Get the publish endpoints for a source.

Requires scope sources:ingest. Gated behind PUBLIC_API_WRITE_ENABLED.

Returns the URLs an encoder — the TRaX phone app, OBS, a hardware unit — dials to put video INTO a source that already exists. Nothing is provisioned here. The input is the durable object and you point a device at it, so calling this twice hands two devices the same endpoint rather than creating a second input. Create the input first with POST /v1/studios/{id}/sources (type generic-input), then call this whenever a device needs to know where to publish.

Treat the response like a password. Every complete URL is a live credential to broadcast on this studio's input: keychain, not log file, not analytics event, not a crash report. That is also why this is a POST — a GET URL ends up in browser history, proxy logs and Referer headers, and this response has no business in any of them.

protocols is a REQUEST, not a guarantee. Read granted off the response and configure from that: it may be narrower than what you asked for and it may be empty. Today srt, rtmp and rtmps are grantable. webrtc is not — the WHIP URL for a plain input carries no publish token, so returning it would hand you an endpoint that fails at the handshake — and rtsp and hls have no publish form at all. Asking for one of those is not an error; it simply does not appear in granted. An unrecognised protocol NAME is a 400, because a silently dropped one leaves your client waiting for an endpoint that is never coming.

Rate limited separately from the rest of /v1, at roughly 5 per minute per caller with a small burst. A client fetches an input's endpoints once when the operator picks that input and again on reconnect; anything sustained above that is a script pulling publish credentials in a loop.

GET /v1/studios/{id}/sources deliberately carries none of this — list responses are what people log.

Auth: API key required.

Parameters:

Name In Required Description
id path yes Studio id.
sourceId path yes Source id.

Responses:

Status Description Body
200 The publish endpoints actually granted. Nothing was created; these already existed. SourceIngest
default Error envelope. Error

POST /v1/studios/{id}/sources/{sourceId}/ingest/rotate

Rotate a source's publish credential.

Requires scope sources:ingest. Gated behind PUBLIC_API_WRITE_ENABLED.

Replaces the stream key behind this input and returns the NEW endpoints, in exactly the shape POST .../ingest returns — so a client that can configure an encoder from a mint result needs no second code path for a rotate result. This is the answer to "that stream key got out": pasted into a support chat, on a laptop that walked off, in the hands of a contributor who no longer works here.

DESTRUCTIVE, and there is no overlap window. The old URLs stop authenticating the moment this returns, because an overlap window is precisely the interval in which the leaked key is still useful. Every device still configured with the old URL will fail its next handshake until you re-point it, so rotate when you can re-provision — not in the sixty seconds before a show.

kickPublishers decides what happens to a session that is publishing RIGHT NOW. The media server authorises a publish once, at handshake, and never re-checks, so an in-flight publisher survives the rotation until its socket drops. Send true to cut it — that is what "revoke" usually means — or false (the default) to let a broadcast in progress finish on the old credential while every NEW connection requires the new one.

protocols scopes what comes BACK, not what is invalidated. Rotation always replaces the single underlying stream key, so asking only for srt still kills the old rtmp URL. Read granted off the response and configure from that, exactly as with the mint endpoint. An unrecognised protocol NAME is a 400 and the credential is NOT rotated — a typo must not cost you a working key.

Same rate-limit class as the mint endpoint, roughly 5 per minute per caller with a small burst.

The studio's own operators see this without asking: open studio tabs re-render the Connection Information panel with the new URLs.

Auth: API key required.

Parameters:

Name In Required Description
id path yes Studio id.
sourceId path yes Source id.

Responses:

Status Description Body
200 The publish endpoints built from the NEW credential. The previous ones no longer authenticate. SourceIngest
default Error envelope. Error

GET /v1/studios/{id}/stream-status

Get a studio's live/offline stream status.

Requires scope stream:read.

Auth: API key required.

Parameters:

Name In Required Description
id path yes Studio id.

Responses:

Status Description Body
200 The stream status. StreamStatus
default Error envelope. Error

stream

Go-live / go-offline lifecycle

POST /v1/studios/{id}/go-live

Take a studio live.

Requires scope stream:golive. Gated behind PUBLIC_API_WRITE_ENABLED; returns 503 write_disabled while off.

Auth: API key required.

Parameters:

Name In Required Description
id path yes Studio id.

Responses:

Status Description Body
200 The studio is going live. GoLiveResult
default Error envelope. Error

POST /v1/studios/{id}/go-offline

Take a studio offline.

Requires scope stream:golive. Gated behind PUBLIC_API_WRITE_ENABLED; returns 503 write_disabled while off.

Auth: API key required.

Parameters:

Name In Required Description
id path yes Studio id.

Responses:

Status Description Body
200 The studio is going offline. GoOfflineResult
default Error envelope. Error

chat

Unified live-chat messages

GET /v1/studios/{id}/chat/messages

List a studio's recent unified-chat messages.

Requires scope chat:read. Returns the most recent messages (bounded by limit); no cursor.

Auth: API key required.

Parameters:

Name In Required Description
id path yes Studio id.
limit query no Max items per page (default 50, max 200).

Responses:

Status Description Body
200 Recent chat messages. ChatMessageList
default Error envelope. Error

POST /v1/studios/{id}/chat/messages

Send a message into the studio's unified chat.

Requires scope chat:send. Gated behind PUBLIC_API_WRITE_ENABLED. Fans out to the enabled platforms.

Auth: API key required.

Parameters:

Name In Required Description
id path yes Studio id.

Responses:

Status Description Body
201 The message was accepted for fan-out. ChatSendResult
default Error envelope. Error

connections

Linked platform accounts

GET /v1/connections

List the caller's linked platform accounts.

Requires scope connections:read. Metadata only — never tokens or stream keys.

Auth: API key required.

Parameters:

Name In Required Description
cursor query no Opaque pagination cursor from a prior response's nextCursor.
limit query no Max items per page (default 50, max 200).

Responses:

Status Description Body
200 A page of connections. ConnectionList
default Error envelope. Error

DELETE /v1/connections/{connectionId}

Unlink a connection.

Requires scope connections:write. Gated behind PUBLIC_API_WRITE_ENABLED.

Revokes the grant at the platform and THEN deletes the stored tokens. The revoke is best-effort: a provider that is down or slow does not block the delete, and two providers (LinkedIn, Trovo) expose no revoke endpoint at all, so for those the token remains valid at the provider until it expires naturally. The row and the tokens are gone from TRaX either way.

Destructive beyond the connection itself: destinations bound to it lose their credential source and will fail to resolve at the next go-live. Find them first with connectionId on GET /v1/studios/{id}/destinations.

Auth: API key required.

Parameters:

Name In Required Description
connectionId path yes Connection id.

Responses:

Status Description Body
200 The connection was unlinked. Ok
default Error envelope. Error

GET /v1/connections/{connectionId}

Read one linked account.

Requires scope connections:read. A connection that is missing and one that belongs to another user are both 404 — the two are deliberately indistinguishable.

Auth: API key required.

Parameters:

Name In Required Description
connectionId path yes Connection id.

Responses:

Status Description Body
200 The connection. Connection
default Error envelope. Error

PATCH /v1/connections/{connectionId}

Update a manual connection's label, URL or stream key.

Requires scope connections:write. Gated behind PUBLIC_API_WRITE_ENABLED.

Patches a MANUAL connection in place: only the fields sent are applied, and an absent or empty streamKey leaves the stored key untouched — so a label rename never has to re-enter the key. Rotating a key on the platform is a PATCH with the new streamKey; existing destinations bound to this connection pick it up at the next go-live.

Only valid on connectionType: "manual" rows — an OAuth connection answers 400 invalid_request (its credentials are managed by the platform link, not by hand). A connection that is missing and one that belongs to another user are both 404, deliberately indistinguishable.

Auth: API key required.

Parameters:

Name In Required Description
connectionId path yes Connection id.

Responses:

Status Description Body
200 The updated connection. No key material. Connection
default Error envelope. Error

POST /v1/connections/link

Start linking a platform account.

Requires scope connections:write. Gated behind PUBLIC_API_WRITE_ENABLED.

Returns a URL to HAND TO A BROWSER. This is the whole of what an API can do here: the authorization itself is a human consenting on the platform's own consent screen, which no server-to-server call can stand in for. Do not follow authorizationUrl from your backend.

There is no completion callback and no push when it lands. After the user consents they are returned to a TRaX page; POLL GET /v1/connections and watch for the new row to appear, or for an existing row's status to flip back to active. state is returned so a caller with several attempts in flight can tell which one completed.

returnTo must be a TRaX-owned https host — anything else is rejected rather than followed, because this value is a redirect target reached with the user's session. A third-party application cannot receive this redirect.

TO REPAIR A BROKEN CONNECTION, set reconnectConnectionId to the existing connection's id. That refreshes the grant IN PLACE and KEEPS THE ID. Without it you get a second connection row, and every destination bound to the old one is orphaned — it will still be listed and will still fail at go-live.

Auth: API key required.

Responses:

Status Description Body
200 A URL to open in a browser. LinkStart
default Error envelope. Error

POST /v1/connections/manual

Save a stream key as a manual connection.

Requires scope connections:write. Gated behind PUBLIC_API_WRITE_ENABLED.

Stores a pasted RTMP URL + stream key as a MANUAL connection — the path for platforms with no OAuth link (Facebook and Instagram today, plus rumble/twitter/tiktok/custom). The returned connection's id is what CreateDestination.connectionId takes, so the flow is: save the key here once, then create destinations against it from any studio.

The stream key is WRITE-ONLY: it is stored encrypted and never returned by any read on this API — the response carries no key material. Note that chat is NOT available on stream-key connections: with no API grant there is nothing to read the platform's chat with.

Not idempotent — a retry saves a second connection.

Auth: API key required.

Responses:

Status Description Body
201 The saved manual connection. No key material. Connection
default Error envelope. Error

media

Media library assets + uploads

GET /v1/media

List the caller's media-library assets.

Requires scope media:read.

The library belongs to the USER, not to a studio, so nothing here names one. Rows are cursor-paginated and carry a thumbnail; the file itself is read one asset at a time through GET /v1/media/{assetId}, because handing out object access on a list response is a different grant from saying what exists — and list responses are what end up in logs.

quotaUsedBytes / quotaLimitBytes ride along so you can render usage and refuse an over-large pick client-side, instead of finding the ceiling when an upload comes back 402.

Only READY assets appear. An upload in flight is not listed — poll it by id.

Auth: API key required.

Parameters:

Name In Required Description
cursor query no Opaque pagination cursor from a prior response's nextCursor.
limit query no Max items per page (default 50, max 200).

Responses:

Status Description Body
200 A page of media assets, plus the storage allowance. MediaAssetList
default Error envelope. Error

POST /v1/media

Start an upload — returns a presigned PUT or a multipart plan.

Requires scope media:write. Gated behind PUBLIC_API_WRITE_ENABLED.

Step 1 of THREE. This reserves the asset row and hands back the plan for pushing the bytes; you upload them yourself, straight to storage; then POST /v1/media/{assetId}/complete makes the upload real. An upload that stops after step 2 has produced nothing anybody can use — the row sits in UPLOADING, holding quota, attachable to nothing.

BRANCH ON mode.

  • single — PUT the whole file to uploadUrl, then call /complete with no body.
  • multipart — no uploadUrl at all. PUT each entry of parts at its own offset/byteSize to its own url, keep the ETag response header each PUT returns, then call /complete with the full list. Parts may go up in any order; three in flight is a good number, and each is worth a couple of retries. Cut the file by the plan, never by a part size of your own — the server may have grown it to stay under the storage layer's part-count ceiling.

The reason large files come back as a plan: every request to our storage host crosses a CDN edge that caps ONE request body at 100 MB, and it enforces that at the edge — a 150 MB single PUT dies at 413 after about 2 MB. Parts are sized to stay under that cap, so the ceiling on a file is the account's quota, not the edge. The single/multipart split point is server policy and it moves; that is why you branch on mode rather than on a size of your own.

Not idempotent: calling this twice creates two assets and reserves quota twice. There is no dedupe on filename or content.

byteSize is checked against the remaining quota BEFORE any URL is issued, so an upload that cannot fit is refused here (402 storage_quota_exceeded) rather than after the bytes are sent. contentType decides the asset's kind and must be one we accept — video/mp4, video/webm, video/quicktime, image/jpeg, image/png, image/webp, image/gif, audio/mpeg, audio/wav, audio/ogg, audio/aac.

Auth: API key required.

Responses:

Status Description Body
201 An upload was reserved. Read mode before anything else. MediaUpload
default Error envelope. Error

DELETE /v1/media/{assetId}

Delete an asset and release its storage.

Requires scope media:write. Gated behind PUBLIC_API_WRITE_ENABLED.

DESTRUCTIVE and not undoable: the stored file and its thumbnail go with the row, and the quota comes back.

It is REFUSED with 409 conflict while any studio source still uses the asset, and the message says how many. That is not politeness — a source pointing at a deleted file renders a permanent "unavailable" card, and if that studio is live the encoder's next read on the file fails and the input drops to a placeholder mid-show. Delete the sources first, then the asset.

For an upload still in flight use /abort instead; this endpoint is about finished library content.

Auth: API key required.

Parameters:

Name In Required Description
assetId path yes Media asset id, as returned by POST /v1/media or GET /v1/media.

Responses:

Status Description Body
200 The asset is gone and its storage is released. Ok
default Error envelope. Error

GET /v1/media/{assetId}

Read one asset, with its playback URLs.

Requires scope media:read.

This is the read you POLL after completing an upload, and the only place on /v1 that hands out access to the stored file. Watch status: an asset is usable when it reads READY. A large upload is assembled and probed server-side, so READY can lag /complete by a moment — poll every second or two, and stop.

thumbnailUrl and downloadUrl are presigned for the PUBLIC storage host, so they resolve from a phone. Both are absent unless the asset is READY, and thumbnailUrl is best-effort even then — an image or an audio file may simply not have one, which is a kind icon on your side, not an error.

Every URL here EXPIRES. Treat them as render-time values: fetch, use, discard. Storing one in your database produces a link that works in testing and 403s a day later.

An assetId belonging to anyone else answers 404 not_found, identical to one that does not exist.

Auth: API key required.

Parameters:

Name In Required Description
assetId path yes Media asset id, as returned by POST /v1/media or GET /v1/media.

Responses:

Status Description Body
200 The asset. MediaAssetDetail
default Error envelope. Error

POST /v1/media/{assetId}/abort

Cancel an upload in flight and give the quota back.

Requires scope media:write. Gated behind PUBLIC_API_WRITE_ENABLED.

Call it when a user cancels, or when an upload has failed past retrying. Every part already written is discarded and the reserved row is deleted.

The quota is the reason this matters. An abandoned upload does not appear in GET /v1/media — that lists finished assets — and it still occupies the ceiling, so a client that abandons uploads without aborting them slowly locks its own user out of uploading anything. There is a server-side sweeper, but it runs on the order of hours.

IDEMPOTENT, and NOT a delete: an upload that is already gone is a success, while a FINISHED asset is refused with 409 conflict. Removing finished content is DELETE /v1/media/{assetId}; a mistimed cancel must not become data loss.

Auth: API key required.

Parameters:

Name In Required Description
assetId path yes Media asset id, as returned by POST /v1/media or GET /v1/media.

Responses:

Status Description Body
200 The upload is discarded and its quota released. Ok
default Error envelope. Error

POST /v1/media/{assetId}/complete

Finish an upload — the step that makes it real.

Requires scope media:write. Gated behind PUBLIC_API_WRITE_ENABLED.

Step 3 of three, and the one that is easy to skip. Until it runs the asset stays UPLOADING, counts against quota, and can be attached to nothing; on the multipart path it is also the ASSEMBLY call — the parts are held separately and no whole object exists until this reassembles them.

Single-file upload: send no body.

Multipart upload: send every part with the ETag its PUT returned. Order does not matter; completeness does. If you could not read the ETags — a browser cannot unless the storage host exposes the header through CORS — send the parts with empty etags, or send no body at all, and the server recovers them from storage itself. That fallback is why a missing ETag is not fatal.

IDEMPOTENT. An already-completed asset comes back unchanged, so a client that lost the response to a dropped connection should simply call again rather than re-uploading.

The response is the asset. status may read READY immediately or lag by a moment while the file is probed and a thumbnail is made — poll GET /v1/media/{assetId} rather than assuming.

The bytes in storage are the authority, not what you declared: an upload larger than the byteSize you reserved is rejected and deleted here rather than quietly eating the quota.

Auth: API key required.

Parameters:

Name In Required Description
assetId path yes Media asset id, as returned by POST /v1/media or GET /v1/media.

Responses:

Status Description Body
200 The finished asset. MediaAssetDetail
default Error envelope. Error

canvas

The studio canvas: tile placement, presets, scenes

GET /v1/studios/{id}/canvas

Read the studio's whole canvas.

Requires scope canvas:read.

The whole canvas in ONE call: layout mode, active preset and scene, the coordinate space the rectangles live in, the program output format, and one placement row per source. Everything a phone needs to draw a miniature of what is on air, without a second request.

One call rather than a layout read plus a source list, on purpose. A tile is a source AND a rectangle; fetching those separately gives you two answers taken at two moments, and the picture drawn from them can show a tile that no longer exists or omit one that just appeared.

GEOMETRY IS THE SAME IN BOTH MODES. The server persists the auto planner's resolved rectangles into the same store advanced mode writes, so what you read here is what the encoder is compositing whichever mode the studio is in. You do not have to re-implement the planner to draw the picture.

canvasWidth x canvasHeight is the COORDINATE SPACE — a fixed 1920x1080 for every studio — and is NOT the broadcast resolution. output is. They differ constantly (a 720p60 program still composites on a 1920x1080 canvas), so scale your miniature by the canvas and label the stream by output.

CONDITIONAL. The response carries an ETag; send it back as If-None-Match and an unchanged canvas answers 304 with no body. The tag is computed over the whole snapshot, not just the layout revision, so a source renamed or deleted invalidates it too — a 304 always means your picture is still correct.

Hold layoutRev and send it back as baseRev on your next write.

Auth: API key required.

Parameters:

Name In Required Description
id path yes Studio id.
If-None-Match header no An ETag from a previous read. An unchanged canvas answers 304 with no body.

Responses:

Status Description Body
200 The studio's canvas. CanvasState
304 The canvas has not changed since the ETag you sent. No body.
default Error envelope. Error

PATCH /v1/studios/{id}/canvas

Set the layout mode or the canvas-wide settings.

Requires scope canvas:write. Gated behind PUBLIC_API_WRITE_ENABLED.

A PATCH: send only what you are changing. Returns the WHOLE canvas, so the answer to your change is the new picture rather than an acknowledgement.

layoutMode is the field that matters. auto hands geometry to the server's planner: you express intent (which preset, which source is focused) and the studio computes every rectangle, overriding whatever you last wrote. advanced hands geometry to you: each tile stays exactly where it was last put and the planner keeps its hands off. Sending a rectangle while the studio is in auto is not an error and not a no-op either — it lands, and the next re-plan overwrites it. Flip to advanced first if you mean it to stick.

Flipping auto -> advanced is SEAMLESS by construction: the studio seeds the freeform geometry from what auto is displaying at that instant, in the same write as the mode flip, so nothing on the program moves. Flipping back re-plans and DOES move things — that is the point of going back to auto.

Send baseRev (the layoutRev you read) to make a stale write a 409 instead of a clobber. Omit it for last-write-wins.

Auth: API key required.

Parameters:

Name In Required Description
id path yes Studio id.

Responses:

Status Description Body
200 The canvas after the change. CanvasState
default Error envelope. Error

GET /v1/studios/{id}/canvas/capabilities

Discover the preset catalog and the canvas vocabularies.

Requires scope canvas:read.

What this studio's canvas supports: the encoder-declared preset catalog — each preset's id, display name, slot count, normalized slot geometry, and an SVG schematic you can render as a picker thumbnail — plus the layout modes, fit modes and scene transitions a picker needs to enumerate, and the canvas coordinate space.

DISCOVER, DO NOT HARDCODE. The catalog is the encoder's, not this API's; ids come and go with encoder releases and a client with a baked-in list quietly loses the ones it never heard of. Cache on libraryVersion and refetch when it changes.

slots are normalized 0.0-1.0 fractions, not canvas units — they describe the SHAPE of an arrangement independent of resolution, which is what makes them drawable as a thumbnail. The rectangles a preset actually produces for the current source set come back from the canvas read.

DEGRADES RATHER THAN FAILS: an unreachable encoder yields an empty presets with an empty libraryVersion, never an error. That is a state to render — fall back to what you cached, or hide the picker — and it is the honest answer, because we do not know what that encoder offers right now. The vocabularies are never degraded; they are the gateway's own.

Auth: API key required.

Parameters:

Name In Required Description
id path yes Studio id.

Responses:

Status Description Body
200 What the canvas supports. Possibly a degraded preset half — see the description. CanvasCapabilities
default Error envelope. Error

PATCH /v1/studios/{id}/canvas/placements/{sourceId}

Move, resize, re-stack, crop, hide or show one tile.

Requires scope canvas:write. Gated behind PUBLIC_API_WRITE_ENABLED.

EVERY FIELD IS OPTIONAL AND OMITTED MEANS PRESERVE. That is what makes {"zIndex": 3} a legal call: bring a tile forward without knowing its rectangle; hide one without knowing its crop. A source with NO stored placement is the one exception — there is nothing to preserve, so a first placement must carry a rect, and the 400 says so.

THIS FLIPS THE STUDIO TO ADVANCED MODE. Per-source geometry IS the mode: the server writes the mode alongside the rectangle, atomically, because a rectangle sitting under auto mode is geometry the next re-plan is about to erase. If you wanted to nudge one tile inside an auto arrangement, you wanted the preset endpoint instead — or this, then a replan to put auto back.

Rectangles are in CANVAS units (the 1920x1080 space the canvas read reports), not output pixels and not fractions. A tile may hang off an edge but must intersect the canvas; width and height must be positive and no larger than the canvas. Values outside their ranges are 400s rather than silent clamps, for the same reason a mixer refuses a fader position in decibels: a client that guessed the units should find out.

SAFE TO RETRY. A stale baseRev is normally 409 — but a request that describes the tile EXACTLY AS IT ALREADY IS comes back 200, because the write it asks for is in effect and there is nothing left to do. That exception exists for the case this endpoint was built for: a phone sends a move, the write lands, the response never arrives, and the phone retries with the baseRev it started from. Without it that retry gets a conflict for a change it successfully made, and a client that trusts conflicts would undo the user's own edit.

NOT A DRAG PATH. Every call here persists, re-asserts to the encoder and fans out to every open studio tab. For real-time motion the studio WebSocket carries an ephemeral high-frequency transform frame that does none of those things; REST has no equivalent. Move the tile locally, then commit the final rectangle here — once.

Auth: API key required.

Parameters:

Name In Required Description
id path yes Studio id.
sourceId path yes Source id.

Responses:

Status Description Body
200 The canvas after the move — or unchanged, when a retried write was already in effect. CanvasState
default Error envelope. Error

POST /v1/studios/{id}/canvas/preset

Pick the auto-mode arrangement.

Requires scope canvas:write. Gated behind PUBLIC_API_WRITE_ENABLED.

The auto-mode counterpart to a placement patch: there you name pixels, here you name INTENT and the planner produces the pixels. For a phone this is almost always the better call — "put Dana big and everyone else in a strip" survives a guest joining, where six hand-placed rectangles do not.

The server persists the intent, re-resolves every rectangle from it, seats any source the new preset made room for, and pushes the result at a live encoder. Returns the whole canvas.

presetId must come from the capabilities endpoint. Do not hardcode the catalog. tileOrder, traySourceIds and slotAssignments are WHOLE-SET replacements when present and untouched when absent — send the complete list, because a partial one would silently drop the members it omitted.

LAST-WRITE-WINS, not revision-gated, deliberately: picking a preset is a discrete human action taken a few times a show, not the high-frequency optimistic path a revision clock exists to protect. Two people picking two presets a second apart should end on the second one, not on a conflict.

Auth: API key required.

Parameters:

Name In Required Description
id path yes Studio id.

Responses:

Status Description Body
200 The canvas after the arrangement changed. CanvasState
default Error envelope. Error

POST /v1/studios/{id}/canvas/replan

Hand the arrangement back to the auto planner.

Requires scope canvas:write. Gated behind PUBLIC_API_WRITE_ENABLED.

Re-runs the planner over the studio's CURRENT sources and preset, persists the rectangles it resolves, seats anything the plan now places, pushes the result at a live encoder, and returns the whole canvas. It changes no intent — same preset, same focus, same tray — it just recomputes.

This is the "put it back the way the studio would arrange it" button, and the way OUT of advanced mode: send layoutMode: "auto" and the row flips and re-plans in one call, so hand-placed rectangles are replaced by the planner's.

Sent without a mode against an ADVANCED studio it is a no-op, and reports that honestly by returning the unchanged canvas rather than an error: the planner does not own geometry in advanced mode, and quietly re-planning a studio whose operator took manual control would move tiles on air that nobody asked to move.

You should rarely need it. Membership changes — a guest joins, a camera goes live, a preset is picked — already re-plan on their own.

Auth: API key required.

Parameters:

Name In Required Description
id path yes Studio id.

Responses:

Status Description Body
200 The canvas after the re-plan — or unchanged, when the studio is in advanced mode and no mode flip was asked for. CanvasState
default Error envelope. Error

GET /v1/studios/{id}/scenes

List the studio's saved scenes.

Requires scope canvas:read.

The looks an operator captured and can recall. A scene is a SNAPSHOT OF THE CANVAS, not a channel: it holds the source rectangles, the layout mode and the active-source set as they were at capture time.

hasComposition false means the scene is a label with no saved canvas behind it — recalling it will change nothing. Grey the row rather than offering a recall that silently does nothing.

Not paginated: a studio's scene list is a handful of rows an operator made by hand.

Auth: API key required.

Parameters:

Name In Required Description
id path yes Studio id.

Responses:

Status Description Body
200 The studio's scenes. SceneList
default Error envelope. Error

POST /v1/studios/{id}/scenes/active

Recall a saved scene onto the program.

Requires scope canvas:write. Gated behind PUBLIC_API_WRITE_ENABLED.

Restores the scene's captured composition through the same geometry authority every other canvas write goes through, re-pushes each source's audio so levels do not snap back to encoder defaults, and tells every open studio tab. Returns the resulting canvas.

ON A LIVE STUDIO THIS CHANGES WHAT VIEWERS SEE, IMMEDIATELY. There is no preview bus here to rehearse it on: scene rows carry an is-preview flag, but no second canvas is composited from it, so exposing a "preview" would name a rehearsal that does not happen. Program is the only destination a scene has.

A metadata-only scene — hasComposition false — recalls cleanly and changes no geometry. The returned canvas will look identical to the one you read before the call. That is success, not a silent failure.

A sceneId from a studio you cannot see answers 404, identical to a scene that does not exist.

Auth: API key required.

Parameters:

Name In Required Description
id path yes Studio id.

Responses:

Status Description Body
200 The canvas after the scene was recalled. CanvasState
default Error envelope. Error

destinations

GET /v1/studios/{id}/broadcast-templates

List a studio's broadcast templates.

Requires scope destinations:read.

Returns the studio's saved broadcast templates, the pinned default first then newest-updated. A template is a named snapshot of the create-mode broadcast settings that prefills the create form; it creates nothing on YouTube. Each row carries its full config object.

Auth: API key required.

Parameters:

Name In Required Description
id path yes Studio id.
cursor query no Opaque pagination cursor from a prior response's nextCursor.
limit query no Max items per page (default 50, max 200).

Responses:

Status Description Body
200 A page of broadcast templates. BroadcastTemplateList
default Error envelope. Error

POST /v1/studios/{id}/broadcast-templates

Save a broadcast template.

Requires scope destinations:write. Gated behind PUBLIC_API_WRITE_ENABLED.

Saves a new named template from a name and a config JSON object. isDefault: true pins it as the studio's default in the same transaction (at most one default per studio). config is stored verbatim and never interpreted server-side.

Auth: API key required.

Parameters:

Name In Required Description
id path yes Studio id.

Responses:

Status Description Body
201 The created template. BroadcastTemplate
default Error envelope. Error

DELETE /v1/studios/{id}/broadcast-templates/{templateId}

Delete a broadcast template.

Requires scope destinations:write. Gated behind PUBLIC_API_WRITE_ENABLED.

Deletes one template, studio-scoped by id. Deleting the pinned default just leaves the studio with no default. A templateId that is not this studio's answers 404.

Auth: API key required.

Parameters:

Name In Required Description
id path yes Studio id.
templateId path yes The broadcast template id.

Responses:

Status Description Body
200 The template was deleted. Ok
default Error envelope. Error

PATCH /v1/studios/{id}/broadcast-templates/{templateId}

Overwrite a broadcast template.

Requires scope destinations:write. Gated behind PUBLIC_API_WRITE_ENABLED.

Overwrites the template's name and config in place, leaving its default pin untouched. A templateId that is not this studio's answers 404.

Auth: API key required.

Parameters:

Name In Required Description
id path yes Studio id.
templateId path yes The broadcast template id.

Responses:

Status Description Body
200 The updated template. BroadcastTemplate
default Error envelope. Error

POST /v1/studios/{id}/broadcast-templates/{templateId}/default

Pin a broadcast template as the default.

Requires scope destinations:write. Gated behind PUBLIC_API_WRITE_ENABLED.

Pins one template as the studio's default and clears any other default, atomically. Returns the now-default row. A templateId that is not this studio's answers 404.

Auth: API key required.

Parameters:

Name In Required Description
id path yes Studio id.
templateId path yes The broadcast template id.

Responses:

Status Description Body
200 The now-default template. BroadcastTemplate
default Error envelope. Error

GET /v1/studios/{id}/destinations/{destId}

Read one destination.

Requires scope destinations:read.

Same shape the list returns. Read lastError whenever status is ERROR or STALLED, and connectionId to find which linked account a failing leg depends on.

Auth: API key required.

Parameters:

Name In Required Description
id path yes Studio id.
destId path yes Destination id.

Responses:

Status Description Body
200 The destination. Destination
default Error envelope. Error

PATCH /v1/studios/{id}/destinations/{destId}/broadcast

Edit a destination's picked YouTube broadcast.

Requires scope destinations:write. Gated behind PUBLIC_API_WRITE_ENABLED.

Edits the mutable fields of the YouTube broadcast this destination has PICKED and pushes them to the platform, exactly as the studio's Broadcast Manager does. broadcastId is required and names the broadcast to edit, so the change never lands on the wrong stream. title and description are applied to the same picked broadcast; the status/contentDetails fields go through YouTube's liveBroadcasts.update.

YouTube FREEZES contentDetails (dvr, embed, recordFromStart, latency, delay, projection, monitorStream) once a broadcast enters testing/live. Any such field the platform refused comes back in rejectedFields — the call still succeeds, so read it to learn which edits did not apply. YouTube-only: other platforms answer 400 invalid_request "operation not supported on this platform".

Auth: API key required.

Parameters:

Name In Required Description
id path yes Studio id.
destId path yes Destination id.

Responses:

Status Description Body
200 The edit outcome, including any fields the platform locked. BroadcastEditResult
default Error envelope. Error

POST /v1/studios/{id}/destinations/{destId}/broadcast

Create a broadcast on the platform now and adopt it as the pick.

Requires scope destinations:write. Gated behind PUBLIC_API_WRITE_ENABLED.

Creates a broadcast on the destination's platform IMMEDIATELY (YouTube liveBroadcasts.insert — an absent scheduledStartTime stamps a near-immediate start; Facebook's legacy scheduled path wants a future time), seeded from the destination's SAVED broadcastConfig — the same template-aware seeding go-live's create mode uses, so privacy/latency/DVR/contentDetails match what going live would have minted. title/description here override the config's copies.

The created broadcast is then ADOPTED as the destination's pick (mode → "pick"), so the next go-live binds the stream to it. Returns the created broadcast; its id is what the edit and pick endpoints take. Platforms with no broadcast create answer 400 invalid_request.

Auth: API key required.

Parameters:

Name In Required Description
id path yes Studio id.
destId path yes Destination id.

Responses:

Status Description Body
201 The broadcast now created on the platform (and adopted as the pick). Broadcast
default Error envelope. Error

POST /v1/studios/{id}/destinations/{destId}/broadcast/pick

Point the destination at an existing broadcast, or clear the pick.

Requires scope destinations:write. Gated behind PUBLIC_API_WRITE_ENABLED.

Points the destination at ONE existing platform broadcast so go-live binds the stream to it — the same choice the studio's "Use existing" tab makes. broadcastId comes from GET .../broadcasts. Sending broadcastId: null (or empty) CLEARS the pick: the destination returns to create mode and go-live auto-creates a fresh broadcast from its saved config.

This is a POINTER write: the platform is not called to validate the id here, and sibling destination settings are never touched (the write is a targeted merge). A dead or foreign broadcast id surfaces at go-live.

Auth: API key required.

Parameters:

Name In Required Description
id path yes Studio id.
destId path yes Destination id.

Responses:

Status Description Body
200 The destination's resulting broadcast targeting. BroadcastPickResult
default Error envelope. Error

POST /v1/studios/{id}/destinations/{destId}/broadcast/thumbnail

Upload a custom broadcast thumbnail.

Requires scope destinations:write. Gated behind PUBLIC_API_WRITE_ENABLED.

Uploads a custom thumbnail image for a YouTube broadcast on a connection-backed destination and returns the URL YouTube now serves. The image is the RAW request body and its Content-Type header is the image MIME type (image/jpeg or image/png); the broadcast is named by the broadcastId query parameter. YouTube caps thumbnails at 2 MB. An unverified channel answers 400 invalid_request.

Auth: API key required.

Parameters:

Name In Required Description
id path yes Studio id.
destId path yes Destination id.
broadcastId query yes The YouTube broadcast/video id to set the thumbnail on.

Responses:

Status Description Body
200 The thumbnail URL YouTube now serves. ThumbnailResult
default Error envelope. Error

GET /v1/studios/{id}/destinations/{destId}/broadcasts

List the platform broadcasts a destination can pick from.

Requires scope destinations:read.

Returns the platform-side broadcasts on the destination's linked connection (YouTube liveBroadcasts / Facebook live_videos) — recent, upcoming and active — exactly what the studio's own "pick an existing broadcast" panel shows. Each row's id is what POST .../broadcast/pick and PATCH .../broadcast take. On YouTube the rows carry the broadcast's LIVE settings (privacy, latency, DVR, ...) so an editor can seed from actual values.

Channel-scoped platforms (Twitch, Kick, Trovo) and pasted-key destinations have no broadcast concept and answer an EMPTY list, not an error.

Auth: API key required.

Parameters:

Name In Required Description
id path yes Studio id.
destId path yes Destination id.
cursor query no Opaque pagination cursor from a prior response's nextCursor.
limit query no Max items per page (default 50, max 200).

Responses:

Status Description Body
200 A page of platform-side broadcasts. BroadcastList
default Error envelope. Error

embed

POST /v1/viewer-tokens

Mint a browser-safe credential for one of your own members.

Requires scope viewer:tokens. Gated behind PUBLIC_API_WRITE_ENABLED.

Call this from YOUR SERVER, never from a browser — it is authenticated with your secret API key. You have already authenticated your own member and decided what they may do; this exchanges that decision for a short-lived token you hand to that member's page, where it is safe.

memberRef is YOUR identifier for the person and stays opaque to TRaX: it is never resolved to anyone, and exists only so the token can be rate-limited and revoked. It is REQUIRED whenever caps contains a write capability, because a relayed chat message reaches the broadcaster's connected platform accounts and must be attributable to someone you vouched for and can stop vouching for.

ttlSeconds is clamped server-side (default 900, maximum 3600) and the response carries the real expiry. Revocation of a cancelled membership IS token expiry, so re-mint on a schedule rather than asking for a longer life.

The response echoes the capabilities actually GRANTED, which may be narrower than requested. Render your interface from those — a composer drawn from a capability you merely hoped for is a control that fails on submit.

Auth: API key required.

Responses:

Status Description Body
201 A viewer token was minted. ViewerToken
default Error envelope. Error

storage

GET /v1/storage-configs

List your connected object-storage buckets.

Requires scope storage:read, which is NOT in the default key scope set — a key must ask for it by name.

Owner-scoped, like connections: a bucket belongs to a person and is usable from any of their studios, so nothing here names one. Credentials are never returned.

503 storage_disabled when object storage is not configured on this deployment. That is deliberately not an empty list: "you have no buckets" and "we cannot answer" are different claims.

Auth: API key required.

Parameters:

Name In Required Description
cursor query no Opaque pagination cursor from a prior response's nextCursor.
limit query no Max items per page (default 50, max 200).

Responses:

Status Description Body
200 A page of storage configs. StorageConfigList
default Error envelope. Error

POST /v1/storage-configs

Connect a bucket.

Requires scope storage:write. Gated behind PUBLIC_API_WRITE_ENABLED, and requires an active subscription.

Stores the config and probes the bucket inline. A FAILING PROBE STILL CREATES THE ROW and still answers 201 — read status, not the HTTP code. That is deliberate: a typo'd endpoint should leave you something editable rather than nothing.

Credentials are encrypted before they touch storage and cannot be read back through any surface.

Auth: API key required.

Responses:

Status Description Body
201 Created. Check status for whether the bucket actually answered. StorageConfig
default Error envelope. Error

DELETE /v1/storage-configs/{configId}

Disconnect a bucket.

Requires scope storage:write. Gated behind PUBLIC_API_WRITE_ENABLED.

SOFT delete: TRaX stops using the bucket and it disappears from every list, but NOTHING IN YOUR BUCKET IS TOUCHED and the stored credentials are retained encrypted rather than scrubbed.

GUARDED: 409 conflict when imported media assets still resolve through this config, naming how many would be stranded. Pass ?force=true to delete anyway — those assets degrade to unresolvable rather than disappearing.

Auth: API key required.

Parameters:

Name In Required Description
configId path yes Storage config id.
force query no Delete even though imported media still references this config.

Responses:

Status Description Body
200 The config was disconnected. Ok
default Error envelope. Error

GET /v1/storage-configs/{configId}

Read one storage config.

Requires scope storage:read. A config that is missing and one belonging to another user are both 404.

Auth: API key required.

Parameters:

Name In Required Description
configId path yes Storage config id.

Responses:

Status Description Body
200 The storage config. StorageConfig
default Error envelope. Error

PATCH /v1/storage-configs/{configId}

Edit a storage config.

Requires scope storage:write. Gated behind PUBLIC_API_WRITE_ENABLED, and requires an active subscription.

Partial: an omitted field is left alone. Credentials are BOTH-OR-NEITHER — send accessKeyId and secretAccessKey together to rotate, or send neither. Re-probes after writing, with the same rule as create: the row survives a failed probe and the verdict lands in status.

Auth: API key required.

Parameters:

Name In Required Description
configId path yes Storage config id.

Responses:

Status Description Body
200 The updated storage config. StorageConfig
default Error envelope. Error

POST /v1/storage-configs/{configId}/verify

Re-probe a stored bucket.

Requires scope storage:write. Gated behind PUBLIC_API_WRITE_ENABLED, and requires an active subscription.

Re-runs the probe and writes the fresh verdict onto the row. The way to clear a stale forbidden after fixing a bucket policy, without re-entering credentials nobody can read back.

As with create, a failed probe is reported in status, not as an error.

Auth: API key required.

Parameters:

Name In Required Description
configId path yes Storage config id.

Responses:

Status Description Body
200 The config with its refreshed status. StorageConfig
default Error envelope. Error

POST /v1/storage-configs/probe

Test credentials without storing them.

Requires scope storage:write. Gated behind PUBLIC_API_WRITE_ENABLED.

The dry run behind a "test connection" button: no row is created, nothing is encrypted, nothing is persisted, and the credentials are discarded when the call returns. Use it before create so a bad bucket is caught BEFORE a row exists.

A bucket that refuses the credentials is a 200 with a non-ok status, not a 4xx — the probe SUCCEEDED, it found out the answer. Only malformed input is a client error.

Auth: API key required.

Responses:

Status Description Body
200 The probe verdict. Read status. StorageProbeResult
default Error envelope. Error

Schemas

AudioMaster

Field Type Required Description
muted boolean yes The master GATE. True silences the program for every viewer on every destination. INDEPENDENT of volume — the fader keeps its position underneath, so unmuting returns to exactly the level it was at.
volume number yes The master fader, LINEAR: 0.0 silent, 1.0 unity (0 dB), above 1.0 amplifies. Not decibels.

AudioSourceStrip

Field Type Required Description
active boolean yes Whether the operator has put this source on the canvas. INTENT, and a different question from muted: an inactive source still has a strip. Deactivating a source auto-mutes it and reactivating restores the prior mute, so active is what explains a strip that reads muted because it was switched off.
muteGroups integer[] yes Mute-group ids (1..4) this channel belongs to. READ-ONLY on this API: the group MASTER button that gives membership its meaning lives in the studio's mixer panel. Always an array — an empty membership is [], never null.
muted boolean yes This channel's GATE, independent of volume — the fader keeps its position underneath.
name string yes Display name, echoing the source's. Present so a fader can be labelled without a second call.
pan number yes Stereo position: -1.0 full left, 0.0 centre, +1.0 full right.
soloed boolean yes The operator's solo latch, synced across every operator in the studio. A mixing-desk intent flag — it does NOT by itself mute the other channels, so do not read it as "everything else is off".
sourceId string yes The source this strip belongs to — the same id the sources endpoints use.
trim number yes Pre-fader input gain, where a hot or quiet input is brought to a nominal level before the fader. LINEAR, 1.0 nominal (-20 dB … +24 dB).
type string yes Source kind, echoing the source's. Every source row carries a strip, including ones with no audio track; read this to decide what to draw.
volume number yes Channel fader, LINEAR: 0.0 silent, 1.0 unity (0 dB). Not decibels.

Broadcast

Field Type Required Description
description string no
enableAutoStart boolean no
enableAutoStop boolean no
enableDvr boolean no
enableEmbed boolean no
enableMonitorStream boolean no
id string yes The platform's broadcast id. On YouTube this is also the video id — the value the pick and edit endpoints take.
latency string no normal | low | ultraLow (YouTube).
madeForKids boolean no An absent boolean here and below means "false or not reported", never a guarantee.
privacy string no public | unlisted | private (YouTube). Live current-state so an editor can seed from actual values; absent on platforms that don't report it.
projection string no rectangular | 360 (YouTube).
recordFromStart boolean no
scheduledStartTime string no The platform-side planned start, RFC 3339. Absent when none is set.
status string yes Lifecycle status as the platform reports it: created | ready | testing | live | complete | ... (Facebook uses its own vocabulary). Only broadcasts not yet complete/revoked are worth picking.
thumbnailUrl string no
title string yes

BroadcastConfig

Field Type Required Description
autoStart boolean no enableAutoStart — the broadcast goes live automatically once ingest begins.
autoStop boolean no enableAutoStop — the broadcast stops automatically when ingest ends.
description string no
dvr boolean no enableDvr — viewers can rewind the live stream.
enableEmbed boolean no contentDetails.enableEmbed. Tri-state: omit to leave unset.
enableMonitorStream boolean no contentDetails.monitorStream.enableMonitorStream. Tri-state: omit to preserve the legacy auto-start derivation; set true under autoStart to park the broadcast in testing.
latency string (enum) no ultraLow disables closed captions and caps resolution at 1080p. (one of: normal, low, ultraLow)
madeForKids boolean no status.selfDeclaredMadeForKids (COPPA).
mode string (enum) no create = auto-create a fresh broadcast at go-live from this template; pick = the operator chose a specific existing broadcast. (one of: create, pick)
privacy string (enum) no Privacy of the auto-created broadcast. (one of: public, unlisted, private)
projection string (enum) no contentDetails.projection. Empty omits the field. (one of: rectangular, 360)
recordFromStart boolean no contentDetails.recordFromStart. Tri-state: omit to leave unset.
streamDelayMs integer no contentDetails.broadcastStreamDelayMs. 0 = no delay.
title string no Title stamped on the auto-created broadcast.

BroadcastEditResult

Field Type Required Description
lifecycleStatus string no The broadcast's lifecycle status after the edit (created | ready | testing | live | complete | …), as YouTube reported it. Absent if the platform did not return one.
rejectedFields string[] yes The fields YouTube refused because the broadcast is already testing/live (contentDetails freezes then). Empty when everything applied.

BroadcastList

Field Type Required Description
data Broadcast[] yes
nextCursor string no

BroadcastPickResult

Field Type Required Description
broadcastId string no The picked broadcast id; absent after a clear.
mode string yes "pick" when a broadcast is now picked; "create" after a clear.
title string no The stored pick title; absent after a clear.

BroadcastTemplate

Field Type Required Description
config object yes The BroadcastConfig snapshot, stored verbatim. The client owns the shape.
createdAt string no
id string yes
isDefault boolean yes Whether this is the studio's pinned default (at most one per studio).
name string yes
studioId string yes
updatedAt string no

BroadcastTemplateList

Field Type Required Description
data BroadcastTemplate[] yes
nextCursor string no

CanvasBackground

Field Type Required Description
color string yes CSS colour string, e.g. "#000000".

CanvasCapabilities

Field Type Required Description
canvasHeight integer yes Canvas coordinate-space height.
canvasWidth integer yes Canvas coordinate-space width, repeated here so a client can size a picker thumbnail before it has read a canvas.
fitModes string[] yes The fit modes a tile or the frame may take.
layoutModes string[] yes The layout modes a write accepts.
libraryVersion string yes Catalog version. Cache on it; refetch when it changes. Empty means the preset half is degraded.
presets CanvasPreset[] yes The presets on offer, or empty when the encoder is unreachable.
transitions string[] yes The scene transition names the studio UI offers. ADVISORY, not a closed enum: a scene stores its transition as free text, so a scene made elsewhere may report a name this list does not contain. Render it as the picker's options, not as a validator.

CanvasCrop

Field Type Required Description
panX number yes Horizontal centre of the visible rect, 0.0 (left edge) to 1.0 (right edge) of the uncropped frame. 0.5 is centred. Meaningless at zoom 1.0.
panY number yes Vertical centre of the visible rect, 0.0 (top) to 1.0 (bottom).
zoom number yes Punch-in factor. 1.0 is the whole frame; above 1.0 is zoomed in.

CanvasPlacement

Field Type Required Description
active boolean yes Whether the source itself is enabled. An inactive source is off the canvas whatever its rectangle says.
alpha number yes Opacity. 1.0 is fully opaque.
broughtIn boolean no True for an EXTRA leg the operator pulled out of its owner's folded person-unit so it gets its own cell. Only meaningful under the person-grid presets.
crop CanvasCrop no
fitMode string (enum) no Per-source fit inside the rectangle. Absent means unset — the encoder default, which is contain. (one of: contain, stretch)
inTray boolean yes True when this is an ACTIVE, canvas-eligible input that did not get a seat — an input waiting, not an input on the program. The exact complement of onCanvas over the sources that could hold one. An INACTIVE source is NEITHER on canvas nor in the tray: it is an input that exists. Calling it 'in the tray' would say it is one action from air when it is two.
name string yes Display name, so a tile can be labelled without a second call.
onCanvas boolean yes True when the canvas is compositing this source RIGHT NOW. Resolved PER MODE, from the same state the encoder is driven from, so you never reimplement either rule. AUTO: the source holds a seat in the server planner's CURRENT resolution of activePreset. A source the preset displaced is false here AND carries no rect, even though the layout row still holds the rectangle it had under the previous preset. ADVANCED: the source has a stored, visible, non-degenerate rectangle. traySourceIds is NOT consulted — nothing maintains that list in advanced mode, so a tile you dragged onto the canvas reads true here while still appearing in it. An INACTIVE source is always false: deactivating removes it from the encoder canvas whatever geometry survives. See membershipBasis for how this answer was reached.
ownerSub string no The person a DEVICE source belongs to (browser camera, shared screen, phone leg); absent for every other kind. It is what lets a client fold one person's several legs into one tile the way the studio's own seating does. Absent on a shared input, whose sharer's identity is stripped upstream.
rect CanvasRect no
sourceId string yes The source this tile is — the same id the sources endpoints use.
type string yes Source kind (webcam, srt, rtmp, video-file, ...). Decides what to draw for a tile that has no picture yet.
visible boolean yes Whether the tile is drawn. False hides it while keeping its rectangle, so showing it again does not make it jump.
zIndex integer yes Stacking order. Higher draws in front; ties break on source id.

CanvasPreset

Field Type Required Description
displayName string yes Human label, e.g. "Two up".
id string yes The id to send as presetId. Show displayName, send this.
overflowToTray boolean yes Whether sources beyond slotCount go to the tray rather than being dropped.
schematicSvg string no An SVG schematic of the arrangement, ready to render as a picker thumbnail. May be empty.
slotCount integer yes How many sources this preset seats. Actives beyond it go to the tray.
slots CanvasPresetSlot[] yes The slot geometry, normalized.

CanvasPresetSlot

Field Type Required Description
h number yes Height as a fraction of canvas height.
index integer yes Slot index, 0-based. Slot 0 is the focus/hero seat where a preset has one.
rect CanvasSlotRect yes
w number yes Width as a fraction of canvas width.
x number yes Left edge as a fraction of canvas width.
y number yes Top edge as a fraction of canvas height.

CanvasRect

Field Type Required Description
height integer yes Height in canvas units. Must be positive and no larger than the canvas height.
width integer yes Width in canvas units. Must be positive and no larger than the canvas width.
x integer yes Left edge in canvas units. May be negative — a tile can hang off the left.
y integer yes Top edge in canvas units. May be negative.

CanvasSlotRect

Field Type Required Description
height integer yes Height in canvas units. 0 only on a degraded read.
width integer yes Width in canvas units. 0 only on a degraded read.
x integer yes Left edge in canvas units.
y integer yes Top edge in canvas units.

CanvasState

Field Type Required Description
activePreset string yes The active preset id, e.g. "2up". Meaningful in auto mode; retained but not applied in advanced mode.
activeSceneId string no The active scene's id, absent when no scene is active.
aspectRatio string yes The studio's aspect-ratio intent, e.g. "16/9".
autoEmphasisEnabled boolean yes Whether the encoder may hero the loudest seat automatically this session.
broughtInSourceIds string[] yes Extra legs pulled out of their owners' folded person-units so each has its own cell. Kept disjoint from traySourceIds.
canvasBackground CanvasBackground no
canvasHeight integer yes Height of the coordinate space. 1080.
canvasWidth integer yes Width of the COORDINATE SPACE the rectangles live in — 1920, fixed for every studio, and NOT the broadcast width. Scale a miniature by this; label the stream by output.
fitMode string (enum) yes Frame-level fit for sources with no per-tile override. (one of: contain, stretch)
focusSourceId string yes The source promoted to the focus slot. Auto-mode intent.
layoutMode string (enum) yes Who owns geometry. 'auto': the server's planner resolves every rectangle from the active preset and overrides what you wrote. 'advanced': each tile stays exactly where you last put it. (one of: auto, advanced)
layoutRev integer yes The layout revision. Monotonic per studio, bumped by every canvas write. Hold it and send it back as baseRev to make your next write refuse to clobber someone else's.
membershipBasis string (enum) yes How onCanvas/inTray on every placement were reached. 'resolved' — the normal, authoritative answer: membership came from the auto planner, or (in advanced mode) from the stored placements the encoder is driven from. Trust it. 'stored' — the auto planner could NOT resolve this activePreset. The encoder declares the preset catalog and ships new presets independently, so a studio can legitimately hold one this server build does not know. Membership then falls back to the persisted tray column and the stored rectangles, both of which may be stale. Render the canvas, but do not conclude from it that a source is off air. '' (empty) — an older server. It makes NO claim; do not read it as either value. (one of: resolved, stored, ``)
output ProgramOutput yes
pinnedOwnerId string yes The person pinned as the permanent hero for this session, by owner key. Only meaningful under the person-grid presets.
placements CanvasPlacement[] yes One row per source in the studio, seated or not. Ordered for painting: placed tiles first in draw order (z ascending, source id breaking ties), then the unplaced ones by id.
screenPriority boolean yes Whether a shared screen outranks cameras when the planner seats people.
slotAssignments object yes Sticky seating, per preset: preset id -> slot index (as a string) -> source id. ALWAYS PRESENT, never null. An EMPTY object means 'no sticky seats are assigned on this studio' — a real state, not a failed read and not an unpopulated field. Sparse within itself: a preset or slot with no entry has no sticky seat and the planner picks for it. Read this before POST /canvas/preset, whose slotAssignments is a WHOLE-SET replacement: take this map, change the entries you mean, send the whole thing back.
spotlightId string yes The spotlit source id, EMPTY when none. Always present, never omitted, so a client mirroring the studio row does not have to tell absent from unset. Legacy sibling of focusSourceId, carried because the studio row carries it and a client mirroring the row would otherwise lose it on a round trip.
tileOrder string[] yes Slot-order intent, by source id. Auto-mode only.
traySourceIds string[] yes The overflow tray: active sources the current preset had no slot for.
updatedAt string yes When the layout last changed, RFC 3339.
verticalOverlay VerticalOverlay no

ChatConnector

Field Type Required Description
desired boolean yes Whether the studio wants this platform's chat attached.
platform string yes The connector's identity: youtube, twitch, kick, ...
requiresLiveBroadcast boolean yes True for platforms (YouTube) whose chat cannot attach until the broadcast is live, so an off-air "not connected" reads as calm rather than broken.
state string yes The connector lifecycle state that drives the pill: connected, reconnecting, failed, idle, ...

ChatMessage

Field Type Required Description
authorDisplayName string yes
authorId string no
id string yes
platform string yes
text string yes
timestamp string no

ChatMessageList

Field Type Required Description
data ChatMessage[] yes
nextCursor string no

ChatSendResult

Field Type Required Description
messageId string no
ok boolean yes

CompleteMediaUploadRequest

Field Type Required Description
parts CompletedMediaPart[] no The finished parts. Omit the body entirely for a single-file upload. Order does not matter — the server sorts, because parts finish out of order when several are in flight — but COMPLETENESS does: a partial list is an error, not a partial completion.

CompletedMediaPart

Field Type Required Description
etag string no The ETag response header that part's PUT returned, verbatim — quotes included, the server normalises. Browsers only see this header when the storage host exposes it via CORS; if you cannot read it, send the whole list with empty etags (or send no list at all) and the server recovers them from storage itself.
partNumber integer yes The partNumber from the plan.

Connection

Field Type Required Description
avatarUrl string no
connectedAt string no
connectionType string yes oauth | manual
displayName string yes
expiresAt string no
externalUserId string no
grantedScopes string[] no
id string yes
platform string yes
status string yes active | needs_reauth | expired | revoked
updatedAt string no

ConnectionList

Field Type Required Description
data Connection[] yes
nextCursor string no

ContributeRequest

Field Type Required Description
deviceId string yes Your app's own stable identifier for THIS DEVICE. Mint it once, keep it in the Keychain / Keystore, and send the same value forever. It is the IDEMPOTENCY KEY for this endpoint. The same value always resolves to the same input on the same studio, which is what makes a reconnection land back on the same camera tile with the same credential instead of littering the studio with half-made inputs. A value your app regenerates each launch produces a new input each launch, and the operator watches their source list fill up with duplicates of one phone. Opaque to TRaX and never resolved to anyone: send an identifier for the DEVICE, not for the person holding it.
deviceLabel string no The human name for this input — "EJ's iPhone". Optional, and refreshed on every call, so renaming the phone renames the input the operator sees. Send nothing and the input keeps whatever it is already called.
protocols string[] no Which publish protocols to hand back. Omit it (or send []) for ["srt"], which is what the TRaX phone app dials and the protocol whose URL carries a real per-input credential. A REQUEST, not a guarantee — read granted off the response. Ask for exactly what you will dial: every extra entry is another copy of the same live publish secret sitting in your app's memory, logs and crash reports.

Contribution

Field Type Required Description
autoSeat boolean yes Whether this input takes a seat on the canvas by itself when the studio is ALREADY live. It NEVER starts the broadcast — going live is the operator's call and nothing on this path can make it happen. Read it to tell your user what will happen when they hit publish: a seat that appears on air, or a source the operator has to place.
deviceId string yes Your deviceId, echoed back, so an app provisioning several devices can match a reply to a request without relying on ordering.
granted string[] yes The protocols actually GRANTED, which may be narrower than requested and may be empty. Configure from this list, never from what you asked for.
ingest object yes Publish endpoints keyed by protocol. Contains exactly the keys listed in granted. Same shape POST .../sources/{sourceId}/ingest returns, so if you already configure an encoder from a mint result you need no second code path here.
ingestHost string no The hostname your credential points at, lifted out of the URLs so you can show "publishing to …" without parsing — or displaying — a string that is also a secret. Display only: dial the URLs.
ingestMetro string yes Which metro this device is being steered to, or "auto". Today it is always "auto", and that is a measurement rather than a placeholder: nearest-point-of-presence selection happens in DNS, when your device RESOLVES the ingest hostname, not when this call mints the URL. The server genuinely does not know which PoP you will reach, and inferring one from your IP address would be wrong for every VPN, every carrier-grade NAT, and most mobile subscribers — which is exactly the population this endpoint exists for. DISPLAY it; do not branch on it. A later release may name a real metro here, and a client that special-cased the string would break on the improvement.
sourceId string yes The input this device is bound to. Stable for the life of the device on this studio — store it, and use it for the ordinary source calls: rename with PATCH /v1/studios/{id}/sources/{sourceId}, rotate the credential with POST .../ingest/rotate, remove the input with DELETE.
srtStreamId string no The raw SRT streamid to dial with — publish:<path>:<key>:<key>, NOT url-encoded, because libsrt wants literal colons as delimiters. It carries no JWT: the scoped key is the whole capability, which is what keeps it comfortably under SRT's 512-byte streamid cap. Present only when srt is in granted. Prefer the complete URL; this exists for encoder SDKs that want the streamid as its own socket option.
streamKey string yes The input's stream key, on its own, for encoders whose UI asks for a server and a stream key as two separate fields. Already embedded in every complete URL in this response, so it adds no exposure — and it is still a live publish credential. Keychain, not log line.
streamName string yes The media path you are publishing to, e.g. inputs/mobile-contrib-mc-<hex>. The one field in this response that is safe to log — use it to correlate this device's session with GET /v1/studios/{id}/stream-status.

CreateBroadcastRequest

Field Type Required Description
description string no Description body. Optional.
scheduledStartTime string no RFC 3339 planned start. Absent = CREATE NOW on YouTube (a near-immediate start is stamped); Facebook's scheduled path requires a future time at least ~10 minutes out.
title string yes Title for the created broadcast. Required — YouTube rejects untitled broadcasts. Overrides the saved broadcastConfig's title.

CreateBroadcastTemplateRequest

Field Type Required Description
config object yes The BroadcastConfig snapshot as a JSON object. Required; a scalar/array is rejected.
isDefault boolean no Pin the new template as the studio's default in the same transaction. Default false.
name string yes Human label. Required.

CreateDestinationRequest

Field Type Required Description
broadcastConfig BroadcastConfig no
connectionId string no Bind to a linked account; mutually exclusive with rtmpUrl+streamKey.
enabled boolean no
name string yes
platform string yes
rtmpUrl string no
streamKey string no Write-only; never returned on any read.
vertical boolean no

CreateManualConnectionRequest

Field Type Required Description
label string yes The name shown in destination pickers, e.g. "My Facebook Page". Required.
platform string no Free-form: facebook | instagram | rumble | twitter | tiktok | custom | ... Empty defaults to custom.
rtmpUrl string yes RTMP(S) push URL from the platform's stream dashboard, e.g. rtmps://live-api-s.facebook.com:443/rtmp. Required.
streamKey string yes The stream key. WRITE-ONLY: stored encrypted and never returned by any read on this API. Required.

CreateMediaUploadRequest

Field Type Required Description
byteSize integer yes
contentType string yes
filename string yes
folder string no

CreateSourceRequest

Field Type Required Description
active boolean no
mediaAssetId string no Attach a media-library asset as this source, instead of a URL. Pass an id from GET /v1/media; it must be READY and yours, and the type must be a library-backed kind. The server resolves it to a storage URL the ENCODER can reach, which is a different signature from the downloadUrl a phone gets — and it re-mints that URL whenever it expires. That is why an asset-backed source keeps working and a pasted presigned URL rots. Mutually exclusive with url: sending both is a 400, not a precedence rule you have to remember.
name string yes
type string yes webcam | rtmp | srt | webrtc-input | media | ... The library-backed kinds — video-file, image, audio-file, media — are the ones mediaAssetId applies to.
url string no Source URL for URL-backed kinds. Mutually exclusive with mediaAssetId. Ignored for kinds where TRaX provisions the ingest itself (webcam, webrtc-input, generic-input) — for those leave it empty and read the endpoints back from POST .../ingest.

CreateStudioRequest

Field Type Required Description
name string yes

Destination

Field Type Required Description
connectionId string no The linked account supplying this destination's credentials; absent for a pasted-credential row. This is the join key to GET /v1/connections — the only way to find out WHICH account to re-link when a leg stops resolving.
createdAt string no
degraded boolean no True while the encoder is DROPPING frames toward this leg to protect the others: the stream is still live and the picture is degrading. Meaningful only during a live session.
drainPct integer no How much of this leg's buffer is being shed, 0-100. On an idle destination this is 0, which means "not applicable", not "measured healthy".
enabled boolean yes VIDEO ONLY. A disabled destination is retained but no video leg is opened for it. It does NOT govern chat: for an always-on platform (twitch, kick, trovo, linkedin) the chat connector attaches from the linked account whether this is true or false, and whether or not the studio is live. See requiresLiveBroadcast.
encoderStreamId string no The encoder's id for this leg while a session runs. Correlation only; absent when the studio is not live.
id string yes
lastError string no Why the leg last failed, verbatim from whatever refused it. Read this whenever status is ERROR or STALLED: status alone cannot tell a revoked token from an unreachable ingest host.
name string yes
platform string yes youtube | twitch | kick | custom_rtmp | ... Not validated against a closed set: an unrecognised value is stored as given.
requiresLiveBroadcast boolean no Whether this destination's PLATFORM can only attach chat once a broadcast is live (youtube, facebook, instagram, tiktok have no chat channel id before then). A property of the platform, not this row.
rtmpUrl string no Set for a pasted-credential destination. ABSENT for a connection-backed one: that URL is resolved fresh from the linked account at each go-live and never stored, so there is nothing truthful to return between streams.
settings object no Opaque per-destination settings, round-tripped unchanged. Its schema is not part of this contract.
status string no IDLE | CONNECTING | LIVE | ERROR | STALLED | STOPPING. Meaningful only while the studio is live; treat the value set as open and match defensively.
streamKey string no MASKED: "" plus the last four characters, or "" for a key of four characters or fewer. Absent when no key is stored, which is every connection-backed destination. No surface returns the plaintext — losing it means rotating at the platform.
studioId string yes
updatedAt string no
vertical boolean yes

DestinationList

Field Type Required Description
data Destination[] yes
nextCursor string no

Error

Field Type Required Description
error object yes

GoLiveResult

Field Type Required Description
destinationIds string[] yes
encoderStreamId string yes

GoOfflineResult

Field Type Required Description
ok boolean yes

Health

Field Type Required Description
status string yes
writeEnabled boolean no Whether mutating (write) endpoints are currently enabled.

IngestEndpoint

Field Type Required Description
base string no The same endpoint with the credential stripped, e.g. srt://ingest.traxstreaming.live:8890. INFORMATIONAL ONLY — it does not authenticate, and base + streamKey is not a working substitute for complete. An RTMP publish is authenticated by the ?user=/?pass= query parameters that only complete carries, so an encoder configured with base as the server and streamKey as the key is rejected at the handshake. Use it to SHOW an operator where a contributor publishes without showing what lets them; dial complete.
complete string yes The URL to hand your encoder, credential included. For SRT that is the whole srt://host:port?streamid=publish:<path>:<key>:<key>&latency=... string — the stream key appears TWICE, as the streamid's user and pass fields. For RTMP/RTMPS the credential rides as ?user=<key>&pass=<key> query parameters, not as URL userinfo. This IS the secret. Store it in the platform keychain, never in a log line or an analytics event. Dial it VERBATIM. Do not reassemble it from parts: the credential form is the server's to decide and it has changed before, and a client that rebuilds the URL from a remembered shape keeps sending the old one long after the server moved on.

LinkStart

Field Type Required Description
authorizationUrl string yes Open this in a browser the user controls. Single-use, and it expires with the state below. Do NOT follow it server-side: the platform shows a consent screen, not a token.
expiresAt string no When the URL stops working, RFC 3339. Ten minutes out today. After this, call the endpoint again.
state string yes The opaque CSRF/state token embedded in the URL, returned so a caller with several link attempts in flight can tell which one came back. Single-use.

LinkStartRequest

Field Type Required Description
platform string (enum) yes Which platform to link. A platform TRaX has no OAuth application configured for is a 400, not an empty URL. (one of: twitch, youtube, kick, facebook, linkedin, trovo)
reconnectConnectionId string no RE-AUTH IN PLACE. Set this to an existing connection.s id to refresh its grant while KEEPING that id, instead of creating a second row. This is the repair for a connection whose status has gone needs_reauth or expired.
returnTo string no Where the browser lands after the platform redirects back. Must be a TRaX-owned https host. Omit for the default landing page.
scopes string[] no OAuth scopes to request. LEAVE THIS OUT to get the platform's default set, which is what TRaX's own features need — that is the right choice unless you know exactly why it is not. Requesting less links successfully and then silently fails at whichever feature needed the missing scope; check grantedScopes on the resulting connection to see what the platform actually gave.

MediaAsset

Field Type Required Description
byteSize integer yes
contentType string no
createdAt string no
durationSeconds number no Fractional seconds. 0 for a still image, and 0 when the file could not be probed — picker metadata, not a guarantee.
filename string yes
folder string no
height integer no Pixels. 0 when unknown or not applicable (audio).
id string yes
kind string yes video | image | audio
status string yes UPLOADING | READY | FAILED. Only READY can be attached to a studio.
thumbnailUrl string no Short-lived presigned GET for a generated thumbnail. Absent for a non-READY asset and for one with no extractable thumbnail (audio, most images) — draw a kind icon, do not treat it as an error. It EXPIRES: fetch it at render time, never store it. A list row carries a thumbnail and no download URL; reading the file itself is GET /v1/media/{assetId}.
width integer no Pixels. 0 when unknown or not applicable (audio).

MediaAssetDetail

Field Type Required Description
assetId string yes
contentType string no
createdAt string no
downloadUrl string no Short-lived presigned GET for the original file, signed for the public storage host so it resolves from a phone. Absent unless READY. It EXPIRES — fetch it when you render, never persist it. You do NOT need it to put the file on a canvas: pass mediaAssetId to POST /v1/studios/{id}/sources and the server resolves its own encoder-facing URL.
durationSeconds number yes Fractional seconds. 0 for a still image and 0 when the file could not be probed.
errorMessage string no Why the asset is FAILED, when it is.
filename string yes
folder string no
height integer yes
kind string yes video | image | audio
sizeBytes integer yes The size MEASURED in storage once the upload completed — not the size you declared.
status string yes UPLOADING | READY | FAILED. Poll until READY after completing an upload; only a READY asset carries URLs and only a READY asset can back a source.
thumbnailUrl string no Short-lived presigned GET for the generated thumbnail. Absent unless READY, and absent when none could be extracted.
updatedAt string no Moves when the upload completes — the field to watch while polling.
width integer yes

MediaAssetList

Field Type Required Description
data MediaAsset[] yes
nextCursor string no
quotaLimitBytes integer yes The account's storage ceiling in bytes. An upload that would cross it is refused at POST /v1/media with 402 storage_quota_exceeded.
quotaUsedBytes integer yes Bytes the library currently holds. Rides on the list rather than a separate endpoint so you can render usage, and warn a user their library is full, BEFORE they pick a file.

MediaUpload

Field Type Required Description
assetId string yes The reserved asset. It exists immediately in status UPLOADING and counts against quota from this moment — if you give up, call /abort.
kind string yes video | image | audio, derived from the contentType you declared. Returned so you can branch before the upload finishes.
mode string yes single or multipart. BRANCH ON THIS. The two shapes are mutually exclusive: single carries uploadUrl and no parts, multipart carries parts and no uploadUrl. The split point is server policy and it moves, so a client that reads mode keeps working when it does — one that only reads uploadUrl uploads nothing the day its user picks a big file.
objectKey string yes Where the bytes land in storage. Informational — you do not need it.
partBytes integer no Multipart path only: the size of every part but the last. Informational — cut the file by each part's own offset/byteSize.
parts MediaUploadPart[] no Multipart path only: one presigned PUT per part, in order. PUT each part's slice to its URL, keep the ETag response header each one returns, then call /complete with the full list. Parts may go up in any order and a few at a time; completeness is what matters, not order.
uploadUrl string no Single-file path only: a short-lived presigned PUT. Send the raw bytes with HTTP PUT and a Content-Type matching what you declared. SECRET — never log it.

MediaUploadPart

Field Type Required Description
byteSize integer yes How many bytes to send for this part. Equal to partBytes for every part but the last.
offset integer yes Byte offset into your local file where this part starts. Do NOT re-derive it from a part size of your own — the server may have grown the part size to keep the plan under the storage layer's part-count ceiling, so this plan is the only correct description of how the file is cut.
partNumber integer yes 1-based part index. Send it back on /complete paired with the ETag the storage host returned for this part.
url string yes Presigned PUT for this part. SECRET — the URL itself grants write access, so keep it out of logs. Short-lived; an expired plan needs a new POST /v1/media, not a retry.

MintViewerTokenRequest

Field Type Required Description
caps string (enum)[] yes Requested capabilities. An unrecognised value is rejected rather than ignored — a silently dropped capability is a viewer with quietly wrong access.
displayName string no The sender name PUBLISHED with a relayed chat message. REQUIRED whenever caps contains chat.send. A relayed message goes out on the BROADCASTER's connected platform accounts, so without a name it reads as the broadcaster speaking. Unlike memberRef this value is PUBLIC by construction — send a display name, never an email or an internal id. Sanitized and truncated to 32 characters.
memberRef string no YOUR opaque identifier for the person, e.g. their user id in your system. Never resolved to anyone by TRaX; used only as a rate-limit and revocation key. Optional for read-only capabilities; REQUIRED whenever caps contains chat.send.
studioId string yes The studio the viewer will reach. The token is scoped to exactly this studio and is refused if presented against another.
ttlSeconds integer no Requested lifetime. Omit or use 0 for the 900s default; anything above 3600 is clamped rather than refused. Read the real expiry off the response.

Monitor

Field Type Required Description
expiresAt string yes RFC 3339. Reflects the CLAMPED lifetime, which may be shorter than you asked for. It bounds when a NEW connection may be made. A session that is ALREADY playing outlives it: the media server authorises a read once, at session establishment, and never re-checks — so your picture does not go black when this passes. Mint again to (re)connect, not to keep watching.
srt object yes An SRT alternative for a client that cannot speak WHEP, or null when one cannot be built. null is a real answer, not a missing field: the read credential rides INSIDE SRT's streamid, which libsrt caps at 512 bytes and rejects locally — before anything reaches the network — when exceeded. When the composed URL would be over the cap, or no SRT endpoint is configured, the server returns null rather than a URL that cannot dial. Handle it by falling back to WHEP, which is the path you should be on anyway.
streamName string yes The program path being monitored, e.g. s/<studio-id>/pp. Safe to log.
whep MonitorWhep yes

MonitorRequest

Field Type Required Description
ttlSeconds integer no Requested lifetime for the credential. Omit it (or send 0 or less) for the 300-second default; anything above the 900-second ceiling is CLAMPED, not refused. Read the real expiry off expiresAt — never schedule from the value you asked for.

MonitorWhep

Field Type Required Description
token string yes Send as Authorization: Bearer <token> on the WHEP signaling request. It is NOT your API key and NOT the user's login token: it authorises exactly this studio's program path, for a few minutes, and nothing else. That is what makes it safe to put on a signaling request your API key has no business appearing on.
url string yes POST your SDP offer here. This is the path every client should take — WebRTC degrades on a lossy cellular uplink far better than the alternative.

Ok

Field Type Required Description
ok boolean yes

PickBroadcastRequest

Field Type Required Description
broadcastId string no The platform broadcast id to pick, from the broadcasts list. null or empty CLEARS the pick and returns the destination to create mode (auto-create at go-live).
title string no Human title stored on the pick pointer so status rows can label it without a platform round trip. Optional; ignored when clearing.

ProgramOutput

Field Type Required Description
fpsDen integer yes Frame-rate denominator, e.g. 1 — or 1001 for 59.94. Rational rather than rounded, because 59.94 and 60 are different broadcasts.
fpsNum integer yes Frame-rate numerator, e.g. 60 — or 60000 for 59.94.
height integer yes Output height in pixels.
width integer yes Output width in pixels.

RemovedResource

Field Type Required Description
id string yes

ReplanCanvasRequest

Field Type Required Description
layoutMode string (enum) no Send "auto" to flip an advanced studio back to the planner and re-plan in one call — this is the way OUT of advanced mode. Omit to re-plan in place, which is a no-op on an advanced studio. (one of: auto, advanced)

Scene

Field Type Required Description
category string yes Grouping label, e.g. "general".
description string no Free-text description.
hasComposition boolean yes FALSE means this scene is a label with no saved canvas behind it — recalling it changes nothing. Reported explicitly so a client can grey the row instead of offering a recall that silently does nothing.
hotkey string no Keyboard shortcut the studio UI binds it to.
id string yes The scene id — what the recall endpoint takes.
isActive boolean yes Whether this is the scene currently on the program.
isFavorite boolean yes Whether the operator marked it a favourite.
name string yes Display name.
transitionDuration number yes Transition length in seconds. 0 for a cut.
transitionType string yes How the program cuts to it: "cut", "fade", ... Free text — see the capabilities endpoint's transitions for the names the studio UI offers.
updatedAt string yes When the scene last changed, RFC 3339.

SceneList

Field Type Required Description
data Scene[] yes

SendChatMessageRequest

Field Type Required Description
platforms string[] no Subset to fan out to; empty = every enabled platform.
text string yes

SetActiveSceneRequest

Field Type Required Description
sceneId string yes The scene to recall. A scene id from a studio you cannot see answers 404, identical to one that does not exist.

SetCanvasPresetRequest

Field Type Required Description
focusSourceId string no The source to promote to the focus slot. Send an empty string to clear the focus.
layoutMode string (enum) no Usually omitted. Picking a preset while advanced persists the intent without applying it. (one of: auto, advanced)
presetId string no A preset id from the capabilities endpoint. Do not hardcode the catalog — it is declared by the encoder and it changes. Present but empty is a 400.
slotAssignments object no The WHOLE sticky-seat map: preset id -> slot index (as a string) -> source id.
tileOrder string[] no The WHOLE slot order. Present (including empty) REPLACES the stored order.
traySourceIds string[] no The WHOLE overflow tray. Present (including empty) REPLACES the stored set.

Source

Field Type Required Description
active boolean yes
createdAt string no
id string yes
name string yes
publishedAt string no When the CURRENT publish began (RFC 3339). Absent when nothing is publishing or when the start time is unknown. Stable while the publisher stays connected — a new value means it dropped and reconnected.
publishing boolean no Is a publisher attached to this input right now — media-plane truth, the same signal the studio UI's source LEDs use. NULL means the liveness tracker could not be reached on this request: UNKNOWN, not offline. A client polling "am I live yet" must treat null as "ask again".
status string no Control-plane provisioning state (OK, MISSING, RECREATING, INGEST_DOWN). Says whether this input still has a backing ingest stream — NOT whether media is arriving. A perfectly provisioned input nobody is publishing to reads OK. For "is anything arriving", read publishing.
studioId string yes
type string yes

SourceIngest

Field Type Required Description
granted string[] yes The protocols actually GRANTED, which may be narrower than requested and may be EMPTY. Configure your client from this list, never from what you asked for.
ingestUrls object yes Endpoints keyed by protocol. Contains exactly the keys listed in granted — never a protocol you asked for but cannot publish over.
streamKey string yes The input's stream key. ALWAYS PRESENT — on a mint and on a rotate alike. It is the SAME credential that is already embedded inside every complete URL in this response: for SRT it is the streamid's user and pass fields, for RTMP/RTMPS the ?user= and ?pass= query parameters. Repeating it here therefore adds no exposure, and it exists for encoders whose UI asks for a server and a stream key as two separate fields. It is still a live publish credential. Store it in the platform keychain alongside complete, never in a log line. Note that base + streamKey is NOT a working substitute for complete — dial complete verbatim.
streamName string yes The media path this source publishes to, e.g. inputs/generic-input-<uuid>. Stable for the life of the source and the one field here that is safe to log — use it to correlate a device's session with stream status and metrics.

SourceIngestRequest

Field Type Required Description
protocols string (enum)[] no The protocols you want endpoints for. Omit the body entirely, or send an empty array, for the ["srt"] default. Ask for exactly what your client will dial: every extra entry is another copy of a live publish credential in your app's memory, logs and crash reports. An unrecognised NAME is a 400; a recognised protocol this source cannot publish over is simply absent from granted (see the response).

SourceIngestRotateRequest

Field Type Required Description
kickPublishers boolean no Cut whatever is publishing to this input right now, immediately after the new credential is saved. The media server authorises a publish once, at handshake, and never re-checks — so a session that connected with the OLD key keeps broadcasting through the rotation until its socket drops. Send true when you are revoking a leaked credential and mean it. Leave it false (the default) for a planned hand-over: the broadcast in progress finishes on the old key while every NEW connection requires the new one.
protocols string (enum)[] no The protocols you want endpoints for AFTER rotating. Same semantics as the mint endpoint: omit the body or send an empty array for the ["srt"] default, and read granted off the response rather than assuming you got what you asked for. This scopes what comes BACK, not what is invalidated — rotation replaces the single underlying stream key, so asking only for srt still kills the old rtmp URL. An unrecognised NAME is a 400 and nothing is rotated.

SourceList

Field Type Required Description
data Source[] yes
nextCursor string no

StorageConfig

Field Type Required Description
accessKeyLast4 string no The last four characters of the stored access key id, so you can tell two credentials apart. Never the whole key, never the secret.
bucket string yes
createdAt string no
endpoint string yes
forcePathStyle boolean yes Address the bucket as a path segment rather than a subdomain. True for most non-AWS providers; defaults true on create when unset.
id string yes
lastVerifiedAt string no
name string yes
prefix string no The key prefix TRaX confines itself to. Absent or empty means the bucket root.
provider string yes s3 is the only value today; any S3-compatible endpoint (AWS, Backblaze B2, Cloudflare R2, MinIO, Garage) is reached through it.
region string no
status string yes unverified | ok | unreachable | invalid_creds | forbidden. EVENT-DRIVEN, never swept: written on create, update and verify, downgraded when a real operation fails, healed by the next success. An old "ok" means "worked when last exercised", not "works now".
statusDetail string no Human-readable detail behind status — usually your provider's own error text.
updatedAt string no

StorageConfigList

Field Type Required Description
data StorageConfig[] yes
nextCursor string no

StorageProbeResult

Field Type Required Description
status string yes ok | unreachable | invalid_creds | forbidden. A non-ok verdict arrives as a SUCCESSFUL 200 — read this field, not the HTTP code.
statusDetail string no

StorageWrite

Field Type Required Description
accessKeyId string no Write-only. Only its last four characters are ever readable again.
bucket string no
endpoint string no Full S3 endpoint URL, e.g. https://s3.us-west-002.backblazeb2.com. A bare hostname is rejected.
forcePathStyle boolean no Defaults to true when omitted on create, which is right for most non-AWS providers.
name string no
prefix string no
region string no
secretAccessKey string no Write-only. Never returned by any surface.

StreamStatus

Field Type Required Description
broadcastStartedAt string no
encoderStreamId string no
live boolean yes
liveStartedAt string no
mode string no cloud | local

Studio

Field Type Required Description
createdAt string no
description string no
id string yes
lastOpenedAt string no
name string yes
ownerSub string yes
role string yes owner | VIEW | EDIT | ADMIN

StudioAudio

Field Type Required Description
master AudioMaster yes
sources AudioSourceStrip[] yes One strip per source row, in the same order the sources list returns them. Empty when the studio has no sources — an empty mix, not an error.

StudioEventSnapshot

Field Type Required Description
audio StudioAudio yes The studio.s audio mixer — the same shape GET /v1/studios/{id}/audio returns.
canvas CanvasState yes The studio.s canvas/scenes/presets state — the same shape GET /v1/studios/{id}/canvas returns.
chatConnectors ChatConnector[] yes Every linked platform's chat connector state. Empty when no chat platforms are linked — state, not an omission.
destinations Destination[] yes
sources Source[] yes
streamStatus StreamStatus yes

StudioList

Field Type Required Description
data Studio[] yes
nextCursor string no

ThumbnailResult

Field Type Required Description
thumbnailUrl string yes The thumbnail URL YouTube now serves for the broadcast.

UpdateAudioMasterRequest

Field Type Required Description
muted boolean no New master gate state. True silences the program on every destination.
volume number no New master fader position, LINEAR. Outside 0.0–10.0 is a 400.

UpdateAudioSourceRequest

Field Type Required Description
muted boolean no New gate state. Set THIS to mute someone; writing volume 0.0 instead loses their fader position.
pan number no New stereo position. Outside -1.0–1.0 is a 400.
soloed boolean no New solo latch.
trim number no New pre-fader input gain, LINEAR. Outside 0.1–16.0 is a 400.
volume number no New fader position, LINEAR. Outside 0.0–10.0 is a 400, not a clamp.

UpdateBroadcastRequest

Field Type Required Description
broadcastId string yes The YouTube broadcast to edit. Required so the change never lands on the wrong stream.
categoryId string no snippet.categoryId.
description string no snippet.description. Applied to the picked broadcast.
enableAutoStart boolean no contentDetails.enableAutoStart.
enableAutoStop boolean no contentDetails.enableAutoStop.
enableDvr boolean no contentDetails.enableDvr. Immutable once testing/live.
enableEmbed boolean no contentDetails.enableEmbed. Immutable once testing/live.
enableMonitorStream boolean no contentDetails.monitorStream.enableMonitorStream. Immutable once testing/live.
latency string (enum) no contentDetails.latencyPreference. Immutable once testing/live. (one of: normal, low, ultraLow)
madeForKids boolean no status.selfDeclaredMadeForKids (COPPA).
privacy string (enum) no status.privacyStatus. (one of: public, unlisted, private)
projection string (enum) no contentDetails.projection. Immutable once testing/live. (one of: rectangular, 360)
recordFromStart boolean no contentDetails.recordFromStart. Immutable once testing/live.
streamDelayMs integer no contentDetails.broadcastStreamDelayMs. Immutable once testing/live.
title string no snippet.title. Applied to the picked broadcast.

UpdateBroadcastTemplateRequest

Field Type Required Description
config object yes New BroadcastConfig snapshot as a JSON object. Required. The default pin is left untouched.
name string yes New human label. Required.

UpdateCanvasPlacementRequest

Field Type Required Description
alpha number no New opacity. Outside 0.0-1.0 is a 400, not a clamp.
baseRev integer no The layoutRev this edit was based on. Omit for last-write-wins. A stale baseRev is a 409 — EXCEPT when the tile already holds exactly the values this request asks for, which is the retry case and comes back 200. That exception is what makes this call safe to retry over a connection that drops after the write lands.
cropPanX number no New visible-rect centre X.
cropPanY number no New visible-rect centre Y.
cropZoom number no New punch-in factor. Send 1.0 to clear a crop.
fitMode string (enum) no New per-source fit. (one of: contain, stretch)
rect CanvasRect no
visible boolean no Whether to draw the tile. False keeps the rectangle so the tile does not jump when shown again.
zIndex integer no New stacking order. Higher draws in front.

UpdateCanvasRequest

Field Type Required Description
aspectRatio string no New aspect-ratio intent, e.g. "16/9".
baseRev integer no The layoutRev this edit was based on. Omit for last-write-wins. Send it and a write based on a revision the canvas has already passed is refused with 409 rather than applied.
canvasBackground CanvasBackground no
fitMode string (enum) no New frame-level fit. (one of: contain, stretch)
layoutMode string (enum) no Who owns geometry from now on. Anything else is a 400. (one of: auto, advanced)
screenPriority boolean no Whether a shared screen outranks cameras in auto seating.
verticalOverlay VerticalOverlay no

UpdateDestinationRequest

Field Type Required Description
broadcastConfig BroadcastConfig no
enabled boolean no
name string no
platform string no
rtmpUrl string no
streamKey string no Write-only; never returned on any read.
vertical boolean no

UpdateManualConnectionRequest

Field Type Required Description
label string no New label. Absent = keep; empty string is rejected (a connection always has a label).
rtmpUrl string no New RTMP(S) push URL. Absent = keep; empty string is rejected.
streamKey string no New stream key. Absent OR empty = keep the stored key — there is no key-clear; rotate the key on the platform and PATCH it here, or delete the connection.

UpdateSourceRequest

Field Type Required Description
active boolean no
name string no
url string no

UpdateStudioRequest

Field Type Required Description
autoGoLive boolean no
autoGoOfflineOnDrop boolean no
name string no

VerticalOverlay

Field Type Required Description
enabled boolean yes Whether the guide is shown.
height integer yes Guide height in canvas units.
width integer yes Guide width in canvas units.
xOffset integer yes Horizontal offset of the guide, in canvas units.

ViewerToken

Field Type Required Description
caps string[] yes The capabilities actually GRANTED, which may be narrower than requested. Render your interface from these.
expiresAt string yes Absolute expiry. Reflects the CLAMPED lifetime, which may be shorter than requested — schedule the next mint from this.
token string yes The credential. Safe to hand to a browser, unlike the API key that minted it.