GENERATED from
trax-public-api /v1 openapi.json (baked copy: scripts/specs/v1-openapi.json)byscripts/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
/v1OpenAPI 3.0 spec. The live, machine-readable contract is served athttps://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 (version1.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 theX-API-Keyheader), or an interactive-login user JWT (Authorization: Bearer <oidc-jwt>, the native-app OAuth + PKCE path) — see Authentication. Themetaendpoints (/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 touploadUrl, then call/completewith no body.multipart— nouploadUrlat all. PUT each entry ofpartsat its ownoffset/byteSizeto its ownurl, keep theETagresponse header each PUT returns, then call/completewith 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. |