referencegenerated — do not edit

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

TRaX Developer API — /v1 reference

This page is generated from the /v1 OpenAPI 3.0 spec. The live, machine-readable contract is served at https://api.traxstreaming.live/v1/openapi.json — point Postman, Insomnia, or a codegen tool at that URL. This page is a browsable rendering of the same spec (version 1.28.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 sit behind a per-deployment write switch and return 503 write_disabled while it is off; production has writes on. The ACCOUNT surface (#666) is live: read/update your own profile (the @username identity), manage your developer API keys, and delete the account itself (DELETE /v1/account — immediate and irreversible) — key management and account deletion are JWT-bearer-only (an API key is refused with 403 interactive_auth_required regardless of scopes). Self-serve PLAYBACK (#705): publish a source with POST /v1/studios/{id}/sources/{sourceId}/playback, embed the returned player, and anonymous viewers get a two-minute read token from POST /v1/playback/{playbackId}/token — the one /v1 route that takes no credential.

At a glance

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

Endpoints

meta

Health + spec (unauthenticated)

GET /v1/health

Liveness probe (unauthenticated).

Auth: none (public).

Responses:

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

GET /v1/openapi.json

This OpenAPI 3.0 document (unauthenticated).

Auth: none (public).

Responses:

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

studios

Studios, sources, destinations, stream status

GET /v1/studios

List the caller's studios.

Requires scope studios:read.

Auth: API key required.

Parameters:

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

Responses:

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

POST /v1/studios

Create a studio.

Requires scope studios:write. Gated behind the deployment write switch (503 write_disabled while off).

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 the deployment write switch (503 write_disabled while off). 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 the deployment write switch (503 write_disabled while off).

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 the deployment write switch (503 write_disabled while off).

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

DELETE /v1/studios/{id}/contribute

Release a device's input on a studio (keep-source teardown).

Requires scopes sources:ingest AND sources:write — the SAME pair as provisioning, so a phone that could create the session can release it and no other key can. Gated behind the deployment write switch (503 write_disabled while off).

The teardown twin of POST /v1/studios/{id}/contribute: it gives this device's input back to the studio when the phone is done, stopping the encoder from re-arming a pull against a publisher-less path. Call it from applicationWillTerminate.

KEEP-SOURCE. The input row SURVIVES — its id, media path, canvas placement and layout stay. A later POST .../contribute with the SAME deviceId re-seats the SAME tile instead of littering the studio with a new one. This is deactivate, not delete; DELETE /v1/studios/{id}/sources/{sourceId} is the call that removes the row.

Answers 204 on everything that is merely "nothing to do": an unknown deviceId, one already released, one that never published, or an absent/empty body. The caller is a client shutting down with no UI to show an error and no chance to retry, so a teardown must never fail on request framing it cannot fix. The only non-204 answers are the ones you must not ignore: 403 when the deviceId belongs to another user, 401 without auth, and 5xx if the release genuinely did not happen.

Unlike provisioning, this is NOT gated on the deployment's contribute switch: that flag stops new sessions being created; it must never strand one that already exists.

Auth: API key required.

Parameters:

Name In Required Description
id path yes Studio id.

Responses:

Status Description Body
204 The input was released, or there was nothing to release (unknown/already-released/empty deviceId). No body. —
403 The deviceId belongs to another user. Error
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 the deployment write switch (503 write_disabled while off).

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.

supportedColorimetry is what to CAPTURE. It lists the color systems that survive the ingest — one entry today, SDR BT.709 8-bit — and it is the same list the output endpoint's caps.supportedColorimetry serves, because one pipeline carries one color system. HDR / Main10 input is ACCEPTED here and tone-flattened to SDR before the canvas, so sending HLG or PQ is not an error and also not useful. Build your capture picker from this array and it will offer HDR the day a second entry appears, with no app update.

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 the deployment write switch (503 write_disabled while off). 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 the deployment write switch (503 write_disabled while off).

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 the deployment write switch (503 write_disabled while off).

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. Optional canvas/audio/chat resources additionally require canvas:read, audio:read and chat:read respectively. Without those scopes their snapshot fields and corresponding events are omitted; authorized base events continue normally. Write scopes imply their matching read scope.

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.

Chat reconnect progress. Existing chatConnector events and snapshot chatConnectors entries include reconnectAttemptId, reconnectState, reconnectError and reconnectUpdatedAt. Match the platform and attempt, reject older timestamps (preserving fractional precision), and replace the complete row. Unrelated platform events never complete a pending attempt. Snapshot refresh reconciles missed completion events; replay remains bounded and is not a durable delivery guarantee. GET /v1/studios/{id}/chat/connectors is also available with chat:read. Chat connector snapshot entries and chatConnector/chatConnectorRemoved events require chat:read; no chat message contents are carried. Intercom room snapshots (intercomRooms) and intercom events additionally require intercom:read. They carry whole credential-free room rosters including client-reported media progress/errors; apply an empty roster as removal. Unauthorized intercom fields/events are omitted. On reconnect, accept a fresh snapshot when replay cannot be honored; no durable replay guarantee is added. Private grants are only returned by the membership endpoints, never this stream.

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 the deployment write switch (503 write_disabled while off).

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}/output

Read the program resolution and frame rate.

Requires scope studios:read.

Three things come back and they are deliberately not the same thing:

  • selected — what the operator picked and what is persisted.
  • effective — what the NEXT go-live will actually run, after the full policy chain.
  • caps — why they differ, and what the ceiling is right now.

Reading only selected will mislead you. The offered resolution is soft-capped by the ENABLED destinations' real platform limits (Twitch ingests 1080p, Instagram 720p, YouTube 4K), so a studio set to 2160p with Twitch enabled airs at 1080p. effective is that answer; caps.cappedBy names the platform that set the ceiling, and is an EMPTY STRING when nothing is capping.

SET BEFORE LIVE. The encoder does not renegotiate resolution or frame rate mid-stream, so effective describes the next go-live, not the session currently on air.

The frame rate is a RATIONAL — 59.94 is 60000/1001 — so NTSC rates are exact rather than rounded. fps is the decimal rendering for display only.

This is the same format the canvas read reports as output, in the same ProgramOutput shape; this endpoint adds the intent, the cap and the reason.

Auth: API key required.

Parameters:

Name In Required Description
id path yes Studio id.

Responses:

Status Description Body
200 The studio's program output format. StudioOutput
default Error envelope. Error

PATCH /v1/studios/{id}/output

Set the program resolution and/or frame rate.

Requires scope studios:write. Gated behind the deployment write switch (503 write_disabled while off).

THIS ENDPOINT REJECTS RATHER THAN CLAMPS, and that is the point of it. Before it existed the four columns took whatever a client sent — a width of 7, a zero denominator, a negative — and the value was quietly replaced with a default or snapped to the nearest supported rung at the next go-live. The caller was told "saved" and got something else. Anything off the supported ladder is now a 400 naming the field and listing what is allowed.

Resolution must be one of the supported 16:9 sizes exactly: 1280x720, 1920x1080, 2560x1440, 3840x2160. Frame rate is a rational: 24000/1001 (23.976), 30000/1001 (29.97), 60000/1001 (59.94), and 24/25/30/50/60 over 1. The ratio may not exceed 60, which is the encoder's own pacing ceiling rather than a product limit.

THE DESTINATION SOFT-CAP IS REPORTED, NOT ENFORCED HERE. Setting 2160p on a Twitch-only studio SUCCEEDS and the response says it will air at 1080p. Rejecting it would be wrong: destinations are enabled and disabled after the format is chosen, and a resolution that is illegal this minute becomes legal the moment YouTube is turned on. The selection is the operator's intent; the cap is a fact about today's destination set.

Applying this to a LIVE studio persists and takes effect at the next go-live. It does not disturb the session on air.

The response is the whole re-resolved view, the same shape GET returns, so you never have to model the cap chain yourself.

CODEC (#674). codec is validated against caps.supportedCodecs — the egress capability of the moment, exactly ["h264"] until an HEVC-capable egress exists, so "h265" is a 400 that says so rather than a silent clamp. "" clears back to automatic. Colorimetry has NO write path: the pipeline runs one color system (SDR BT.709 8-bit, reported read-only on selected/effective) and a writable field it cannot honour would be the false-success class this endpoint exists to end.

Auth: API key required.

Parameters:

Name In Required Description
id path yes Studio id.

Responses:

Status Description Body
200 The updated program output format, re-resolved. StudioOutput
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 the deployment write switch (503 write_disabled while off).

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 the deployment write switch (503 write_disabled while off).

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 the deployment write switch (503 write_disabled while off).

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

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

Read a source's Remote Camera Control state.

Requires scope sources:read.

Returns the source with its device and camera blocks populated (capabilities / desired / actual / control). Pass your own deviceId to have device.mine set true for the phone that owns this source — that is how a phone renders "you are attached" without ever receiving another device's id. The device id itself is never returned, and no ingest secret or session token appears in this read.

Auth: API key required.

Parameters:

Name In Required Description
id path yes Studio id.
sourceId path yes Source id.
deviceId query no The caller's own device id. When it matches the source's bound device, device.mine is true in the response. Omit it and mine is false.

Responses:

Status Description Body
200 The source, with device + camera populated. Source
default Error envelope. Error

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

Write the studio's desired camera state.

Requires scope sources:write. Gated behind the deployment write switch (503 write_disabled while off).

The body IS a sparse desired patch — an object of control groups, each replacing that group whole, a group set to null releasing it. This is an ordinary authorized studio write; it carries NO publisher secret. Every field is validated against the source's reported capabilities before a generation is minted, so an out-of-range or unsupported value is refused 400 invalid_request (camera_unsupported) and nothing is sent to the device. The server assigns the generation and per-group revision. Returns the re-read source.

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 re-read source. Source
default Error envelope. Error

POST /v1/studios/{id}/sources/{sourceId}/camera/attach

Bind a phone to a generic input it proves it publishes to.

Requires scope sources:write. Gated behind the deployment write switch (503 write_disabled while off).

The phone presents the publishNonce it carried in the ingest credential of the push now in progress; the server matches it to the open (or pending) publish session the media plane witnessed, writes the device identity + capabilities onto the row, and returns a sessionToken — a secret returned ONLY here — plus the public publishSessionId. Answers 403 publish_session_invalid when no open session carries that nonce, and 409 source_attached when another device holds the open session.

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 binding, with the one-time session token. CameraAttachResult
default Error envelope. Error

POST /v1/studios/{id}/sources/{sourceId}/camera/heartbeat

Renew an SSE-only phone's camera control lease.

Requires scope sources:write. Gated behind the deployment write switch (503 write_disabled while off).

Renews the control lease for an idle SSE-only phone — the one timer in the design, and only for a phone with no control socket. Same authentication as the report path (API key + deviceId + the session token in X-Trax-Publish-Session). It carries and changes no camera state.

Auth: API key required.

Parameters:

Name In Required Description
id path yes Studio id.
sourceId path yes Source id.
X-Trax-Publish-Session header no The per-device session token from a prior attach.

Responses:

Status Description Body
200 The lease was renewed. CameraHeartbeatResult
default Error envelope. Error

POST /v1/studios/{id}/sources/{sourceId}/camera/prepare

Mint a nonce-bearing ingest for camera control on a generic input.

Requires scope sources:write. Gated behind the deployment write switch (503 write_disabled while off).

Returns the complete, nonce-bearing ingest for a generic live input so a phone can attach to a source it did not contribute. The phone mints the publishNonce; the server embeds it in the returned credential and records a pending publish session so the following attach can prove possession. It hands back a LIVE PUBLISH CREDENTIAL, so it takes exactly the permission that reveals a stream key today and is rate-limited in the credential-handout class.

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 nonce-bearing ingest. CameraIngest
default Error envelope. Error

POST /v1/studios/{id}/sources/{sourceId}/camera/report

Report a device's camera read-back.

Requires scope sources:write. Gated behind the deployment write switch (503 write_disabled while off).

Folds the device's hardware read-back (actual) and/or a fresh capabilities revision onto the row. Authenticated by the API key plus deviceId plus the session token in the X-Trax-Publish-Session header — a publishSessionId copied from public state grants nothing without the token. Answers 401 publish_session_auth with no / a wrong token, 403 publish_session_invalid for a token presented for a different device, 409 publish_session_stale once the session has closed, and 409 report_stale for an out-of-order reportSequence (an exact retry with the same reportId is idempotent). Returns the re-read source.

Auth: API key required.

Parameters:

Name In Required Description
id path yes Studio id.
sourceId path yes Source id.
X-Trax-Publish-Session header no The per-device session token from a prior attach. Required for a real report; its absence is 401 publish_session_auth.

Responses:

Status Description Body
200 The re-read 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 the deployment write switch (503 write_disabled while off).

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 the deployment write switch (503 write_disabled while off).

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

GET /v1/studios/{id}/viewers

Get a studio's live viewer counts.

Requires scope stream:read.

The concurrent-viewer count of each enabled destination, read from its platform while the studio is live (every 30s; YouTube every 60s), plus a studio total.

Each destination has a state: live (the platform reported a count), hidden (live, but the platform withheld the count — YouTube omits it when the owner hides it and when nobody is watching), offline (the platform does not consider the channel live yet or any more), unavailable (the platform has no viewer API: LinkedIn, X, Rumble, Instagram, TikTok and custom RTMP), rate_limited, needs_reauth (reconnect the account), error, or pending (not polled yet). viewers is a number ONLY in state live; every other state is null, never 0.

total sums the destinations in state live and is null when none is. partial is true when any destination is in another state, so the total undercounts the audience. An offline studio returns live: false and no destinations.

Auth: API key required.

Parameters:

Name In Required Description
id path yes Studio id.

Responses:

Status Description Body
200 The current viewer counts. ViewerCounts
default Error envelope. Error

GET /v1/studios/{id}/viewers/history

List per-broadcast viewer statistics.

Requires scope stream:read.

One entry per broadcast (a cold go-live to go-offline, keyed by broadcastStartedAt), newest first: the peak and average of the studio total, and per destination. Averages are over polls with a known total; polls are evenly spaced. Samples are kept for 90 days.

Auth: API key required.

Parameters:

Name In Required Description
id path yes Studio id.
limit query no Broadcasts to return, 1 to 100. Default 20.

Responses:

Status Description Body
200 Broadcast viewer statistics, newest first. object
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 the deployment write switch (503 write_disabled while off); returns 503 write_disabled while off. This request has no body; a supplied body is rejected. Configure sources/layout/destinations and PATCH the studio output before calling go-live, which uses that saved state. Inspect per-destination results and poll YouTube broadcastLifecycle instead of treating HTTP 200 as audience-live.

Auth: API key required.

Parameters:

Name In Required Description
id path yes Studio id.

Responses:

Status Description Body
200 Encoder start accepted; inspect destinationFailures, skippedDuplicates and destinations, then poll destination broadcastLifecycle for YouTube audience-live. GoLiveResult
default Error envelope. Error

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

Take a studio offline.

Requires scope stream:golive. Gated behind the deployment write switch (503 write_disabled while off); 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/connectors

Read platform chat status and reconnect progress.

Requires scope chat:read. Returns the current snapshot for the caller's studio, including the latest reconnect attempt per platform. Use this read after an uncertain POST response or missed completion event. It does not reconnect, send messages, or change provider state. Empty data means no connector rows are currently known.

Auth: API key required.

Parameters:

Name In Required Description
id path yes Studio id.

Responses:

Status Description Body
200 Current platform connector states. ChatConnectorList
default Error envelope. Error

POST /v1/studios/{id}/chat/connectors/{platform}/reconnect

Reconnect one platform chat connector.

Requires scope studios:write and studio edit permission. Gated behind the deployment write switch (503 write_disabled while off). Only the selected desired platform is cycled; its messages may pause briefly. No request body. HTTP 202 acknowledges the operation; reconnectState remains reconnecting until actual attachment succeeds or fails. Concurrent requests coalesce onto the active studio/platform attempt. A request after completion starts a new attempt, so this is not indefinitely idempotent: after a timeout, read GET /chat/connectors before retrying. Ineligible or unlinked platforms return 409 with safe guidance. Foreign studios collapse to 404. Follow the same reconnectAttemptId in existing chatConnector SSE events or read the snapshot; do not infer success from HTTP acceptance or an unrelated platform event.

Auth: API key required.

Parameters:

Name In Required Description
id path yes Studio id.
platform path yes Lower-case platform key such as kick, twitch or youtube, not a connection or session id.

Responses:

Status Description Body
202 Operation accepted or current active attempt returned; inspect reconnectState. ChatConnector
default Error envelope. Error

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 the deployment write switch (503 write_disabled while off). 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

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

Push a destination's chat in over a WebSocket (custom chat bridge).

Requires scope chat:ingest. Gated behind the deployment write switch.

The WebSocket form of POST /v1/studios/{id}/destinations/{destId}/chat/messages, for a tool that would otherwise make one request per message. Upgrade with the usual Authorization: Bearer (or X-API-Key) header — a browser's WebSocket cannot set one, so browser tools use the POST.

Frames. Every TEXT frame is one batch body, in the format named on the upgrade URL, exactly as the POST takes it. Each frame is answered by exactly one text frame, in order:

{"type":"result","results":[...],"accepted":1}
{"type":"error","status":409,"error":{"code":"chat_session_not_running",...}}
{"type":"error","status":429,"retryAfter":2,"error":{"code":"rate_limited",...}}

A result frame is the POST's 202 body; an error frame carries the status the POST would have answered and the POST's error body. A bad frame is answered and the socket stays open. A binary frame closes the socket (1003), as does a frame over 64 KiB (1009).

Limits. The same as the POST, and shared with it: every frame costs one request against your key's rate limit, and its valid messages are charged to the destination's bucket (20 a second, burst 100) that POSTs to the same destination also draw on. A socket counts against the same 5-per-credential cap as an SSE stream. The server pings an idle socket every 30 seconds.

Refused before the upgrade, as an ordinary JSON error: a destination you cannot see (404), the bridge off for it (409 chat_bridge_not_enabled), an unknown format (400), the write switch off (503). chat_session_not_running can change while the socket is open, so it is answered per frame.

Auth: API key required.

Parameters:

Name In Required Description
id path yes Studio id.
destId path yes Destination id.
format query no The body's format. streamerbot is BETA: its mapping follows the commonly used Streamer.bot ChatMessage shape and has not yet been confirmed against a live Streamer.bot event.

Responses:

Status Description Body
101 Switching to the WebSocket protocol. See the description for the frames. —
default Error envelope. Error

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

Push a destination's chat in (custom chat bridge).

Requires scope chat:ingest. Gated behind the deployment write switch.

For a platform TRaX has no chat API for: a tool you run reads that platform's chat and pushes it here, and it appears in the studio chat and its overlays as platform custom, under this destination's name. TRaX never connects anywhere on this path and never fetches a URL you send. The destination must have chatBridgeEnabled (PATCH {"chatBridge": true}), and the studio must have a running chat session.

format picks the body: trax (default, the JSON below), streamerbot (BETA, not yet confirmed against a live Streamer.bot event: a Streamer.bot WebSocket ChatMessage event, or an array of them), irc (raw IRCv3 PRIVMSG lines with Twitch-style tags, text/plain, one per line) or ssn (a Social Stream Ninja message object, or an array of them). Every format is mapped to the v1 schema and the same limits apply. A message with no id gets one derived from its author, text and time, so its dedupe is only as good as the source's own repeats.

Each message is answered on its own: one bad message never fails the batch. Messages are never cross-posted to the studio's other platforms.

Rate limit: 20 messages a second per destination, burst 100, charged per valid message. Over it, 429 with Retry-After; nothing in the batch was delivered.

Auth: API key required.

Parameters:

Name In Required Description
id path yes Studio id.
destId path yes Destination id.
format query no The body's format. streamerbot is BETA: its mapping follows the commonly used Streamer.bot ChatMessage shape and has not yet been confirmed against a live Streamer.bot event.

Responses:

Status Description Body
202 Each message's outcome, in order. IngestChatResponse
default Error envelope. Error

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

Stream one destination's chat as it arrives (Server-Sent Events).

Requires scope chat:read.

The chat the studio chat panel shows for this destination, live: platform chat for a connected destination, bridged chat for one with chatBridgeEnabled. Another destination's chat never appears.

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

event: chat.message
data: {"id":"dst-1:7","platform":"custom","destinationId":"dst-1","author":{...},"text":"hi","timestamp":"..."}

data is a DestinationChatMessage. A bridged message's id is <destinationId>:<your id>.

Live only. Events carry no id and there is no resume: what arrived while you were disconnected is read with GET /v1/studios/{id}/chat/messages. A slow reader can miss messages, as the studio's own panel can.

Keepalives. A : keepalive comment every 25 seconds on a quiet chat. Comments are not events; a standard EventSource client ignores them.

Limits. Counts against the same 5-per-credential cap as GET /v1/studios/{id}/events and the chat ingest socket (429 with Retry-After beyond it). A destination you cannot see is a 404 before the stream opens.

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 chat stream. Stays open until the client disconnects or access ends. —
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 the deployment write switch (503 write_disabled while off).

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 one provider (LinkedIn) exposes no revoke endpoint at all, so for it 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 the deployment write switch (503 write_disabled while off).

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 the deployment write switch (503 write_disabled while off).

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 the deployment write switch (503 write_disabled while off).

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 the deployment write switch (503 write_disabled while off).

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

BRANCH ON mode.

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

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

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

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

Auth: API key required.

Responses:

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

DELETE /v1/media/{assetId}

Delete an asset and release its storage.

Requires scope media:write. Gated behind the deployment write switch (503 write_disabled while off).

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 the deployment write switch (503 write_disabled while off).

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 the deployment write switch (503 write_disabled while off).

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 the deployment write switch (503 write_disabled while off).

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 the deployment write switch (503 write_disabled while off).

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 the deployment write switch (503 write_disabled while off).

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 the deployment write switch (503 write_disabled while off).

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 the deployment write switch (503 write_disabled while off).

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

account

The caller's own account: profile (@username), developer API keys, deletion request

DELETE /v1/account

Delete your account. IMMEDIATE and IRREVERSIBLE.

JWT-BEARER-ONLY: this requires an interactive sign-in (OIDC JWT). An API key — regardless of its scopes — is refused with 403 interactive_auth_required, because a key must never be able to erase the account that owns it. Gated behind the deployment write switch (503 write_disabled while off).

THIS EXECUTES THE ERASURE OF THE AUTHENTICATED ACCOUNT, IMMEDIATELY. It is the same legally recorded flow as 'Delete my account' in the TRaX account app: a row is opened in the append-only deletion record first, then all account data is erased and the identity deleted last. There is no undo, no grace period, and no queued 'request' — present your own confirmation UI before calling.

Legally idempotent: repeating the call returns the ORIGINAL record's id with alreadyDeleted: true rather than writing a second erasure record. No confirmation email is sent on this path (the API hop carries no email address) — show the user requestId, their receipt reference. 503 unavailable means deletion is not armed on this deployment and nothing was deleted; 500 erasure_incomplete means the account still exists and the call should be retried.

The request body is optional: { "reason": "..." } records why on the legal record.

Auth: API key required.

Responses:

Status Description Body
200 The erasure acknowledgment — the caller's receipt. AccountDeletionResult
default Error envelope. Error

GET /v1/account

Read your account profile.

Requires scope account:read for API-key callers (opt-in, not in the default key scopes); a signed-in user JWT always passes. Returns the @username identity — a user who has not claimed a handle gets usernameSet: false, not an error. Display name and email are deliberately NOT here: they are OIDC claims on the IdP token a signed-in client already holds.

Auth: API key required.

Responses:

Status Description Body
200 The caller's account profile. Account
default Error envelope. Error

PATCH /v1/account

Update your account profile (claim or change your @username).

Requires scope account:write for API-key callers (opt-in); a signed-in user JWT always passes. Gated behind the deployment write switch (503 write_disabled while off).

Runs the EXACT validation the TRaX account app uses: same format rules, same reserved blocklist, race-safe uniqueness, and a once-per-cooldown change limit (re-claiming your current handle is an idempotent no-op that does not burn the cooldown). Refusals are typed: 422 invalid_username (message carries the format reason), 409 username_taken / username_reserved, 429 username_cooldown with a Retry-After header.

Auth: API key required.

Responses:

Status Description Body
200 The updated profile. Account
default Error envelope. Error

GET /v1/account/api-keys

List your developer API keys (metadata only).

JWT-BEARER-ONLY: an API key — regardless of its scopes — is refused with 403 interactive_auth_required. API keys cannot manage API keys: a leaked key must not be able to enumerate or reshape its owner's other credentials.

Returns METADATA only, newest first, revoked keys included (with revokedAt set). The full secret is never in any list — it existed once, in the create response.

Auth: API key required.

Responses:

Status Description Body
200 The caller's key metadata. AccountApiKeyList
default Error envelope. Error

POST /v1/account/api-keys

Mint a new developer API key.

JWT-BEARER-ONLY (403 interactive_auth_required for API-key callers — a key must never mint itself broader, longer-lived successors). Gated behind the deployment write switch (503 write_disabled while off).

THE RESPONSE IS THE ONLY TIME THE FULL SECRET APPEARS. key is never stored (only a peppered hash is) and can never be shown again — hand it to the user or store it in your secret manager immediately. Scopes are validated against the canonical /v1 scope vocabulary; an unknown scope refuses the whole request with 400. Omitting scopes yields the safe read-only default set.

Auth: API key required.

Responses:

Status Description Body
201 The minted key, INCLUDING the one-time secret. AccountApiKeyCreated
default Error envelope. Error

DELETE /v1/account/api-keys/{keyId}

Revoke one of your API keys.

JWT-BEARER-ONLY (403 interactive_auth_required for API-key callers). Gated behind the deployment write switch (503 write_disabled while off).

Soft-revokes the key: it stops verifying within the gateway's short verification-cache window (seconds) and stays listable with revokedAt set. Idempotent on an already-revoked key. A key id that does not exist OR belongs to another user is the same 404 — deliberately indistinguishable.

Auth: API key required.

Parameters:

Name In Required Description
keyId path yes The key's id from the list.

Responses:

Status Description Body
204 Revoked (or already revoked). —
default Error envelope. Error

playback

Self-serve public playback: publish a source, embed a player, mint anonymous viewer tokens

POST /v1/playback/{playbackId}/token

Get a short-lived read token for a published playback (no authentication).

NO AUTHENTICATION and no request body. This is the endpoint a player embedded on any website calls for an anonymous viewer, so it answers CORS with Access-Control-Allow-Origin: * — the only /v1 route that does. Possession of a playback id whose owner has turned playback on is the whole authorisation.

Returns a WHEP endpoint and a read token for exactly one source. Send the token ONLY as Authorization: Bearer <whep.token> on the WHEP signaling POST to whep.url — never as a query parameter, where it would land in server logs, browser history and Referer headers. The token lives TWO MINUTES and cannot be extended. A session already playing outlives it; mint a fresh one each time the player (re)connects, never cache one.

404 not_found means the id is unknown OR its playback was turned off — deliberately the same answer. DO NOT RETRY A 404: it will not change until the owner turns playback back on, which issues a different id.

RATE LIMITED, in two buckets that must both admit the request: one per client address (default 10 a minute, burst 20) and one per playback id (default 50 a second, burst 200). A 429 carries Retry-After in seconds — the bucket's own refill time, never less than 1 — and the same number as error.retryAfterSeconds in the body. Wait at least that long. Every successful response carries retryPolicy, the backoff bounds a player should use for reconnects and retries of 429/503; it is set server-side so it can change without a player release. Add jitter: a fleet of players retrying on the same schedule is what the limits exist to absorb.

Auth: none (public).

Parameters:

Name In Required Description
playbackId path yes The public playback id from POST /v1/studios/{id}/sources/{sourceId}/playback.

Responses:

Status Description Body
200 A read credential for the playback's source. PlaybackToken
404 Unknown or disabled playback. Do not retry. Error
429 Rate limited. Wait Retry-After seconds (also error.retryAfterSeconds). Error
default Error envelope. Error

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

Turn public playback off for a source.

Requires scope playback:write, and Studio EDIT on the studio. Gated behind the deployment write switch (503 write_disabled while off).

From this call on, the token endpoint answers 404 for the playback's id, so no NEW viewer can connect. Viewers ALREADY watching are not cut off: the media server authorises a read once, when the session starts. A disable therefore takes full effect within one token lifetime (two minutes) plus the length of the longest session already open.

IDEMPOTENT: 204 whether or not playback was on.

Auth: API key required.

Parameters:

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

Responses:

Status Description Body
204 Public playback is off for this source. —
default Error envelope. Error

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

Read a source's public playback.

Requires scope playback:read (implied by playback:write).

Returns the source's ENABLED public playback, with the same watchUrl, embedHtml and player.tokenUrl the enable call returned. 404 not_found when public playback is off for this source — absent is not an error state to render, it is the answer "nobody outside the studio can watch this".

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 enabled playback. Playback
default Error envelope. Error

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

Publish a source for public playback and get its embed snippet.

Requires scope playback:write, and Studio EDIT on the studio. Gated behind the deployment write switch (503 write_disabled while off).

Turns PUBLIC playback on for one source and returns a playbackId plus everything needed to put a player on your own site: the hosted watchUrl, an embedHtml iframe snippet pointing at it, and player.tokenUrl — the anonymous endpoint a custom player calls to get a short-lived read token. After this call, anyone holding the id can watch this source with no key and no sign-in. That is the point, and it is why playback:write is its own scope and in no default set.

IDEMPOTENT. With playback already on for the source you get the SAME playbackId back with 200; the first enable answers 201. Sending a different visibility updates it in place. Turning playback off (DELETE) and on again mints a NEW id on purpose, so links shared before the disable stay dead.

visibility is unlisted (the default) or public. Token minting is identical for both; the difference is whether TRaX's own surfaces may list or index the watch page. Neither keeps a published id secret — treat every id you embed as public.

Auth: API key required.

Parameters:

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

Responses:

Status Description Body
200 Public playback was already on; the existing playback (visibility updated if you named a different one). Playback
201 Public playback is now on for this source. Playback
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 the deployment write switch (503 write_disabled while off).

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 the deployment write switch (503 write_disabled while off).

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 the deployment write switch (503 write_disabled while off).

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 the deployment write switch (503 write_disabled while off).

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 the deployment write switch (503 write_disabled while off).

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. Those names are snake_case (enable_dvr, stream_delay_ms, …) and do NOT match the camelCase properties you sent — see the rejectedFields schema for the full mapping. Note also that contentDetails is frozen from testing onward AND on complete, revoked, or any status this API does not recognise; only created and ready accept these edits. 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 the deployment write switch (503 write_disabled while off).

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/bind

Bind a stream key to a broadcast, completing it.

Requires scope destinations:write. Gated behind the deployment write switch (503 write_disabled while off).

Binds a stream key to an existing broadcast (streamKeyId set) or UNBINDS it (streamKeyId empty). This is the explicit act that completes an INCOMPLETE broadcast — one with lifecycle created and no bound key, which cannot receive video — so it can go live. TRaX never auto-binds a key onto a broadcast it did not create. Returns the broadcast's resulting boundStreamId (empty when unbound). YouTube-only: other platforms 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
200 The broadcast's resulting bound key. BindBroadcastResult
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 the deployment write switch (503 write_disabled while off).

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 the deployment write switch (503 write_disabled while off).

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. An unverified channel answers 400 invalid_request.

This route has its OWN body ceiling of 2 MB — YouTube's limit for a custom thumbnail — rather than the surface-wide JSON body cap that applies everywhere else. An image over 2 MB answers 413 payload_too_large with the limit in the message; shrink it and retry. An empty or unreadable body stays 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) 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

GET /v1/studios/{id}/destinations/{destId}/next-broadcast

What the next go-live will do to this destination.

Requires scope destinations:read.

A dry run of go-live for ONE destination. It writes nothing, calls no platform API and spends no quota, so you may call it from a settings screen as often as you like.

It exists because neither half of the answer can be worked out from the destination itself. First, the DEFAULTS are applied at go-live rather than stored — an absent autoStart means true, an absent latency means normal, an absent title becomes TRaX Stream, an absent privacy becomes unlisted, and an absent scheduled start becomes thirty seconds from whenever go-live runs — so the saved broadcastConfig is not what will be sent. effective is what will be sent, and defaulted names the fields nobody chose. Second, action is the TARGETING decision, which depends on mode, on whether a broadcast is picked, and on how recently one was auto-created, all together.

action is one of:

  • create — a new broadcast will be inserted from effective.
  • reuse — a broadcast created moments ago will be replayed instead, so a retried go-live does not produce a duplicate. If YouTube reports it has already ended, a fresh one is created.
  • pick — the broadcast you picked will be used; nothing is created.
  • legacy — NO broadcast: go-live falls back to the channel's default stream key and nothing appears in YouTube Studio. Always accompanied by a warning saying how to fix it.
  • none — not a broadcast-capable destination (a pasted RTMP key, or a platform with no broadcast concept).

estimatedQuotaUnits is OUR INFERENCE, not a published figure: Google documents no per-method quota costs for the YouTube Live Streaming API. quotaEstimateInferred is always true and travels with the number so it is never shown as YouTube's own.

Auth: API key required.

Parameters:

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

Responses:

Status Description Body
200 What go-live will do, and with which values. NextBroadcastPlan
default Error envelope. Error

GET /v1/studios/{id}/destinations/{destId}/stream-keys

List a destination's stream keys.

Requires scope destinations:read.

Returns the manageable ingest keys (YouTube liveStreams) on the destination's linked channel, with each key's status (active = receiving video now), reusable/default flags, resolution and frame rate. The ingest SECRET is never returned — a key carries only its 4-char keyLast4 recognition tail.

YouTube-only: channel-scoped platforms (Twitch, Kick) and pasted-key destinations have no key 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 the destination's stream keys. StreamKeyList
default Error envelope. Error

POST /v1/studios/{id}/destinations/{destId}/stream-keys

Mint a new reusable stream key.

Requires scope destinations:write. Gated behind the deployment write switch (503 write_disabled while off).

Mints a new reusable ingest key on the destination's linked channel. An empty title, resolution or frameRate takes a server default (variable accepts any ingest). The response carries the minted key including its keyLast4; the ingest secret is never returned. YouTube-only: other platforms 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 stream key now minted on the platform. StreamKey
default Error envelope. Error

DELETE /v1/studios/{id}/destinations/{destId}/stream-keys/{keyId}

Delete a stream key.

Requires scope destinations:write. Gated behind the deployment write switch (503 write_disabled while off).

Removes a key. Refused with 409 stream_key_bound while a broadcast is still bound to it — unbind (POST .../broadcast/bind with an empty streamKeyId) or delete the broadcast first.

Auth: API key required.

Parameters:

Name In Required Description
id path yes Studio id.
destId path yes Destination id.
keyId path yes The stream key's platform-side id, from the list endpoint.

Responses:

Status Description Body
200 The key was deleted. DeleteStreamKeyResult
default Error envelope. Error

PATCH /v1/studios/{id}/destinations/{destId}/stream-keys/{keyId}

Rename a stream key or change its resolution / frame rate.

Requires scope destinations:write. Gated behind the deployment write switch (503 write_disabled while off).

Edits a key's title and/or resolution / frame rate in place; an empty field is left unchanged. Refused with 409 stream_key_active while the key is receiving video. YouTube-only: other platforms answer 400 invalid_request.

Auth: API key required.

Parameters:

Name In Required Description
id path yes Studio id.
destId path yes Destination id.
keyId path yes The stream key's platform-side id, from the list endpoint.

Responses:

Status Description Body
200 The updated stream key. StreamKey
default Error envelope. Error

GET /v1/studios/{id}/show-settings

Get the studio's show settings.

Requires scope studios:read.

What the show is called, ONCE, for every destination. Going live pushes this title, description, per-platform category, tags and language to every enabled destination, so a client sets them here instead of per destination.

404 when none are saved, and that is not an error to hide. Absent settings mean going live applies nothing, which is how a studio behaves until someone opts in. It is deliberately different from saved-and-blank: render the 404 as "not configured", never as an empty show you then PUT back, or you turn "never configured" into "every destination gets an empty title".

The category is THREE fields because it is three unrelated id spaces. A YouTube categoryId, a Twitch game_id and a Kick category_id are different numbers for the same human concept, so there is no single value to send. A platform whose field is empty is sent NO category, which is not the same as being sent an empty one — some APIs read that as a clear.

Auth: API key required.

Parameters:

Name In Required Description
id path yes Studio id.

Responses:

Status Description Body
200 The studio's show settings. ShowSettings
default Error envelope. Error

PUT /v1/studios/{id}/show-settings

Replace the studio's show settings.

Requires scope studios:write.

A PUT, not a PATCH: these values are applied together at go-live, so a partial write invites a half-configured show that only reveals itself when the stream starts. Send the whole object back, including the fields you did not change — omitted string fields are written EMPTY.

title is capped at 100 CHARACTERS, the only length every platform accepts (YouTube 100, Twitch 140, Kick at least 1). Over-length is a 400 naming the field rather than a silent truncation, because a shortened show title is worse than a refusal.

tags are shared and each platform drops what it cannot take: Twitch forbids spaces and caps each tag at 25 characters, Kick does not. There is no way to express that difference in one list.

applyOnGoLive false keeps the settings saved and stops go-live pushing them — what an operator wants mid-edit.

Auth: API key required.

Parameters:

Name In Required Description
id path yes Studio id.

Responses:

Status Description Body
200 The saved show settings. ShowSettings
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 the deployment write switch (503 write_disabled while off).

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

intercom

POST /v1/studios/{id}/intercom/memberships

Join a private room listening only.

Requires intercom:join and Studio VIEW access. Joining never opens a microphone, camera, screen, or program source. The application owns microphone permission UI and must obtain explicit consent before requesting a publish grant. Retry with the same idempotencyKey after an uncertain response; a retired key cannot create a replacement. The expiring lease must be refreshed explicitly. A retry of an already publish-enabled membership also requires intercom:publish before its private grant can be returned. Grant responses are no-store.

Auth: API key required.

Parameters:

Name In Required Description
id path yes Studio id.

Responses:

Status Description Body
200 Current control state. This does not assert audible media or publish anything into the program. IntercomGrant
default Error envelope. Error

DELETE /v1/studios/{id}/intercom/memberships/{membership}

Leave the exact private membership.

Requires intercom:join and current Studio VIEW access. Stops only this membership. The caller must close its own media peers and microphone tracks. No request body; does not remove another client or route audio into the program.

Auth: API key required.

Parameters:

Name In Required Description
id path yes Studio id.
membership path yes Opaque membership id returned by join. Must belong to this caller and studio and remain unexpired.

Responses:

Status Description Body
200 Current control state. This does not assert audible media or publish anything into the program. IntercomRoom
default Error envelope. Error

PATCH /v1/studios/{id}/intercom/memberships/{membership}

Update private talk, listen, or client audio status.

Requires intercom:join and exact current membership. talking=true additionally requires intercom:publish and an existing publish grant. Omitted fields remain unchanged. Media states are client reports, with server receipt timestamp; only safe enumerated errors are accepted. They do not prove remote audibility.

Auth: API key required.

Parameters:

Name In Required Description
id path yes Studio id.
membership path yes Opaque membership id returned by join. Must belong to this caller and studio and remain unexpired.

Responses:

Status Description Body
200 Current control state. This does not assert audible media or publish anything into the program. IntercomRoom
default Error envelope. Error

POST /v1/studios/{id}/intercom/memberships/{membership}/microphone

Set explicit microphone publish permission.

Requires intercom:join; publish=true also requires intercom:publish and Studio EDIT access. publish=false requires only Studio VIEW and returns a listen grant. Applications must stop their owned publisher/tracks immediately on withdrawal. This control operation does not guarantee instant revocation of an already-issued media JWT or accepted WHIP session. No implicit program routing.

Auth: API key required.

Parameters:

Name In Required Description
id path yes Studio id.
membership path yes Opaque membership id returned by join. Must belong to this caller and studio and remain unexpired.

Responses:

Status Description Body
200 Current control state. This does not assert audible media or publish anything into the program. IntercomGrant
default Error envelope. Error

POST /v1/studios/{id}/intercom/memberships/{membership}/refresh

Refresh the current private membership and credentials.

Requires intercom:join, current Studio access, and the exact unexpired membership. Publish-enabled memberships additionally require intercom:publish; a lower-scope request is rejected without changing intent. Refresh never upgrades listening into publishing. Follow expiresAt and credentialExpiresAt; expired/replaced memberships require a new intentional join. No request body.

Auth: API key required.

Parameters:

Name In Required Description
id path yes Studio id.
membership path yes Opaque membership id returned by join. Must belong to this caller and studio and remain unexpired.

Responses:

Status Description Body
200 Current control state. This does not assert audible media or publish anything into the program. IntercomGrant
default Error envelope. Error

GET /v1/studios/{id}/intercom/rooms

Read private intercom room status.

Requires intercom:read and Studio VIEW access. Returns credential-free rosters and bounded client-reported connection state. Membership is not proof of connected or audible media.

Auth: API key required.

Parameters:

Name In Required Description
id path yes Studio id.

Responses:

Status Description Body
200 Current control state. This does not assert audible media or publish anything into the program. IntercomRoomList
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 the deployment write switch (503 write_disabled while off), 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 the deployment write switch (503 write_disabled while off).

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 the deployment write switch (503 write_disabled while off), 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 the deployment write switch (503 write_disabled while off), 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 the deployment write switch (503 write_disabled while off).

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

Account

Field Type Required Description
createdAt string no When the profile row was first written (RFC 3339). Absent when the user has no profile row yet.
updatedAt string no When the profile row last changed (RFC 3339). Absent when the user has no profile row yet.
userId string yes The acting subject (the key's owner, or the JWT's sub) — the stable user id other resources reference.
username string yes The claimed @handle in canonical (lowercase) form, or "" when unclaimed. Check usernameSet rather than comparing with the empty string.
usernameChangedAt string no When the handle last CHANGED (RFC 3339); absent when never changed. The once-per-cooldown change limit counts from this.
usernameSet boolean yes Whether a handle has been claimed.

AccountApiKey

Field Type Required Description
createdAt string yes Mint time, RFC 3339.
expiresAt string no Expiry, RFC 3339. Absent = never expires.
id string yes Opaque key id — what DELETE /v1/account/api-keys/{keyId} takes.
lastUsedAt string no Last successful verification, RFC 3339 (bumped at most once a minute). Absent = never used.
name string yes The user-chosen label.
prefix string yes The first characters of the secret (e.g. sk_live_7Qm2xB) so a UI can say WHICH key this is.
revokedAt string no Revocation time, RFC 3339. Absent = active.
scopes string[] yes The key's scope ceiling, from the canonical /v1 scope vocabulary.

AccountApiKeyCreated

Field Type Required Description
createdAt string yes —
expiresAt string no —
id string yes —
key string yes The one-time full sk_live_ secret. SHOWN ONLY HERE.
lastUsedAt string no —
name string yes —
prefix string yes —
revokedAt string no —
scopes string[] yes —

AccountApiKeyList

Field Type Required Description
data AccountApiKey[] yes The caller's keys, newest first, including revoked ones.

AccountDeletionRequestBody

Field Type Required Description
reason string no Optionally, why — recorded on the legal deletion record. Never required and never changes what is erased.

AccountDeletionResult

Field Type Required Description
alreadyDeleted boolean yes True when a completed erasure was already on file; nothing ran again.
confirmationEmailSent boolean yes Always false on this path today — the API hop carries no email address.
ok boolean yes False when a data step failed (operators are alerted and finish by hand); the identity itself is gone either way.
requestId string yes The deletion record's id. On a repeat request this is the ORIGINAL record's id.
retained object no Each retained-data category mapped to the legal record's own wording for why it is kept (billing records, the deletion log itself).

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.

BindBroadcastRequest

Field Type Required Description
broadcastId string yes The broadcast to (un)bind. Required.
streamKeyId string no The stream key to bind. An empty (or absent) value UNBINDS the key — the broadcast returns to incomplete (created).

BindBroadcastResult

Field Type Required Description
boundStreamId string yes The broadcast's bound key after the operation; empty when unbound.

Broadcast

Field Type Required Description
boundStreamId string no The stream key (YouTube liveStream) currently feeding this broadcast, or absent when none is bound. A broadcast with no bound key is INCOMPLETE (lifecycle created) and cannot receive video until one is bound — see POST .../broadcast/bind.
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. Omission preserves an existing value; defaults to true for a new template.
autoStop boolean no enableAutoStop — the broadcast stops automatically when ingest ends. Omission preserves an existing value; defaults to true for a new template.
description string no —
dvr boolean no enableDvr — viewers can rewind the live stream. Omission preserves an existing value; defaults to true for a new template.
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. These names are snake_case and do NOT match the camelCase properties you sent — compare against this list, not against your request body. The nine that can appear: enable_auto_start, enable_auto_stop, enable_dvr, enable_embed, record_from_start, enable_monitor_stream, stream_delay_ms, latency, projection — corresponding to the request's enableAutoStart, enableAutoStop, enableDvr, enableEmbed, recordFromStart, enableMonitorStream, streamDelayMs, latency and projection (only the last two are spelled identically). A field appears here only if you asked to change it and the value differed from what the broadcast already had.

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 —

CameraAttachRequest

Field Type Required Description
capabilities object no Optional initial capability report persisted on the row.
device object no Optional device identity ({ platform, model, osVersion, appVersion }) persisted on the row.
deviceId string yes The device claiming the input. Becomes the ownership key every later device-write is checked against.
publishNonce string yes The nonce carried in the ingest credential of the push now in progress. Only the device the media plane actually admitted can present the matching nonce. Never log it.

CameraAttachResult

Field Type Required Description
attached boolean yes —
publishSessionId string yes The PUBLIC identifier of the publish session. It appears in public state and grants nothing on its own.
sessionToken string yes The SECRET the device presents in the X-Trax-Publish-Session header on every later report / heartbeat. Returned ONLY here — never on the row, never in an event. Store it in the keychain; a lost token is recovered by re-attaching.
sourceId string yes —

CameraDesiredPatch

(no documented properties)

CameraHeartbeatRequest

Field Type Required Description
deviceId string yes The heartbeating device. Authenticated with the session token in the X-Trax-Publish-Session header.
publishSessionId string no The publish session being kept alive.

CameraHeartbeatResult

Field Type Required Description
ok boolean yes True when the SSE control lease was renewed.

CameraIngest

Field Type Required Description
path string yes The canonical mediamtx path. Safe to log, unlike the credentials below.
rtmpUrl string no The complete RTMP URL carrying the credential as user=<nonce>&pass=<key>. Absent when RTMP was not granted.
sourceId string yes —
srtStreamId string no The SRT streamid to dial — publish:<path>:<nonce>:<key>, raw and NOT url-encoded. A LIVE PUBLISH CREDENTIAL — keychain, never a log. Absent when SRT was not granted.
srtUrl string no The complete SRT URL with the nonce-bearing streamid inside. Absent when SRT was not granted.

CameraPrepareRequest

Field Type Required Description
deviceId string yes The device that will publish. Opaque; identifies the device, never a person.
publishNonce string yes A fresh 128-bit nonce the phone mints for this push. It is embedded in the returned credential and recorded against a pending publish session so the following attach can prove possession. Never log it.

CameraReportRequest

Field Type Required Description
actual object no Optional hardware read-back — the confirmed actual, never an echo of the request.
capabilities object no Optional fresh capabilities (a lens switch or camera rebuild bumps the revision).
deviceId string yes The reporting device. Must equal the source's bound device. Authenticated together with the session token in the X-Trax-Publish-Session header.
publishSessionId string no The publish session this report is for. A token whose session has closed on disconnect / takeover is refused 409 publish_session_stale.
reportId string no A unique id for this report, for the idempotent-retry escape.
reportSequence integer no A session-scoped, monotonic sequence. The server accepts a report only if it is greater than the stored one, except an exact retry (same publishSessionId + reportId) which is idempotent (409 report_stale otherwise).

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.
error string yes Safe application-authored connector explanation; never raw provider credentials or errors.
platform string yes The connector's identity: youtube, twitch, kick, ...
reconnectAttemptId string yes Latest studio/platform reconnect attempt identity. Concurrent requests share an active attempt. Empty when none is known.
reconnectError string yes Safe actionable failure explanation, or empty to clear the previous error.
reconnectState string (enum) yes Empty when no attempt is known, otherwise reconnecting, succeeded, or failed. Succeeded means attachment completed, not guaranteed future message delivery. (one of: ``, reconnecting, succeeded, failed)
reconnectUpdatedAt string yes UTC RFC3339Nano update timestamp, or empty when no attempt is known. Preserve fractional precision when rejecting late updates.
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, ...

ChatConnectorList

Field Type Required Description
data ChatConnector[] yes —

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 —

Colorimetry

Field Type Required Description
bitDepth integer yes Bits per component the compositor carries: 8 today; 10 when the canvas goes 10-bit.
primaries string yes Color primaries: "bt709" today; HDR adds "bt2020".
range string yes Dynamic range class: "sdr" today; "hdr" once the egress tone-map exists.
transfer string yes Transfer characteristic: "bt709" today; HDR adds "hlg" and "pq".

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 —

ContributeEncodeCaps

Field Type Required Description
audioBitratesKbps integer[] yes The audio bitrates accepted, in kbps.
cappedBy string no WHAT is holding the ladder down: program (the studio's program resolution is not bigger, so sending more would be scaled straight back down), uplink (the platform's ceiling for a mobile uplink), or device (this device reported it cannot capture more). ABSENT when nothing is capping. Read this rather than inferring a cap from maxWidth, which equals the top of the ladder in the uncapped case and would otherwise read as capped — the same contract caps.cappedBy has on the program output endpoint.
codecs string[] yes The exact codec set a selection may name. ["h264"] today.
frameRates FrameRate[] yes The frame rates selectable at or below the current ceiling, as rationals.
keyframeSecondsOptions integer[] yes The keyframe intervals accepted, in whole seconds.
maxHeight integer yes Top of the currently-offered ladder, height.
maxWidth integer yes Top of the currently-offered ladder, width.
resolutions ContributeEncodeRung[] yes Every rung the ladder knows, ascending, available and unavailable alike.

ContributeEncodeFormat

Field Type Required Description
audioBitrateKbps integer yes Audio bitrate in kbps.
bitrateKbps integer yes The video bitrate to configure, in kbps. With adaptive bitrate enabled this is the CEILING to walk down from, never a floor to climb to.
codec string yes Video codec to encode with, h264 or h265. Automatic is always h264; h265 appears only where the ingest is known to carry it.
fpsDen integer yes Frame-rate DENOMINATOR. 1 for integer rates, 1001 for the NTSC family.
fpsNum integer yes Frame-rate NUMERATOR. The rate is a RATIONAL: 59.94 is 60000/1001 and nothing else, and rounding it to a decimal is drift the pipeline cannot correct. Pair it with fpsDen; never compute one without the other.
height integer yes Pixel height to encode at.
keyframeSeconds integer yes Keyframe interval in whole seconds. Mirrors the studio's program setting, so the composited program does not inherit a worse GOP than it asked for.
maxBitrateKbps integer yes The highest bitrate accepted at this rung, in kbps. A selection above it is REFUSED, not silently reduced.
minBitrateKbps integer yes The lowest bitrate worth sending at this rung, in kbps. Adaptive bitrate may walk down to here and no further; below it the picture is not worth the battery.
width integer yes Pixel width to encode at.

ContributeEncodeProfile

Field Type Required Description
caps ContributeEncodeCaps yes —
effective ContributeEncodeFormat yes —
error string no What was wrong with the selection, when valid is false. Absent otherwise.
mode string yes auto — track the recommendation, and keep tracking it when the studio's program format changes — or manual, a pinned selection.
recommended ContributeEncodeFormat yes —
recommendedReason string yes Why the platform recommends that profile, in words you can show a user. Show it: a number with no reason is indistinguishable from a number a client made up, which is the situation this whole object replaces.
valid boolean yes False when a stored manual selection no longer fits — the program dropped to 720p under a pinned 1080p, say. effective then reports the RECOMMENDATION so there is always something runnable to configure from, and error says what was refused.

ContributeEncodeRung

Field Type Required Description
available boolean yes Whether this rung may be selected right now.
height integer yes Pixel height of this rung.
label string yes Short human label, e.g. 1080p. Display only — key on the dimensions.
maxBitrateKbps integer yes Highest bitrate accepted at this rung, kbps.
minBitrateKbps integer yes Lowest bitrate worth sending at this rung, kbps.
status string no Why it may not be selected, when available is false. Absent otherwise. Caller-facing text naming the binding constraint, suitable to show a user.
targetBitrateKbps integer yes The bitrate to configure at this rung, kbps.
width integer yes Pixel width of this rung.

ContributeRequest

Field Type Required Description
camera object no Optional { capabilities } — the initial camera capability report, so the source row is born knowing what it can do. A phone that learns a lens's ranges only after contributing sends them later on the camera report path instead.
device object no Optional Remote Camera Control device identity ({ platform, model, osVersion, appVersion }) persisted so the studio's Camera panel is labelled before the first frame. Identifies the DEVICE, never a person.
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.
publishNonce string no Optional Remote Camera Control nonce. Send a fresh 128-bit value and the returned srtStreamId embeds it, so the device that dials the credential is provably the one that later attaches for camera control. Omit it and contribute is unchanged. Never log it.

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.
encodeProfile ContributeEncodeProfile no —
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.
supportedColorimetry Colorimetry[] yes The color systems that SURVIVE this ingest — read this to choose your capture mode instead of hardcoding one. Exactly one entry today, SDR BT.709 8-bit, and it is the SAME list caps.supportedColorimetry serves on the output endpoint, from the same source, because there is one pipeline carrying one color system end to end. It reports what SURVIVES, not what is ADMITTED, and right now those differ. HDR / Main10 input is accepted at ingest and then tone-flattened to SDR before the canvas. Publishing HLG or PQ is therefore not an error and also not useful: the extra range is discarded upstream of anything a viewer sees, and you paid battery and bitrate for it. Capture the entry listed here. Render your capture UI from this array rather than branching on its contents. The day the pipeline preserves HDR a second entry appears and an app built this way starts offering it with no update — which is why the field ships now, with one entry, instead of waiting. Always present and never null. An absent array would read as "this server does not say", and a client falling back to its own guess is the exact mistake the field exists to prevent.

CreateAccountApiKeyRequest

Field Type Required Description
expiresAt string no Optional expiry, RFC 3339. Omit for a key that never expires.
name string yes A label for the key ("CI uploader", "my phone"). Required.
scopes string[] no Requested scope ceiling. Omit for the safe read-only default set. An unknown scope refuses the WHOLE request with 400 — never a key silently missing a grant.

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 The ingest URL to publish to, for a pasted-credential destination. Required (with streamKey) when connectionId is absent, and ignored when connectionId is set — a connection-backed destination resolves its URL from the linked account at each go-live. Must be an absolute rtmp:// or rtmps:// URL including a host, e.g. rtmps://live-api-s.facebook.com:443/rtmp; any other scheme, a bare path or hostname, or leading/trailing whitespace is rejected with 400 invalid_request. SRT is not accepted here — an SRT endpoint belongs on a manual connection's own srtUrl field (POST /v1/connections/manual).
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.

CreateStreamKeyRequest

Field Type Required Description
frameRate string no Optional cdn.frameRate; empty defaults to variable.
resolution string no Optional cdn.resolution; empty defaults to variable.
title string no Optional title; empty yields a TRaX-generated name.

CreateStudioRequest

Field Type Required Description
name string yes —

DeleteStreamKeyResult

Field Type Required Description
deleted boolean yes True when the key was deleted.

Destination

Field Type Required Description
activeBroadcastId string yes The platform broadcast this destination is CURRENTLY operating on, or "" when there is none. THIS is the id to pass to PATCH /v1/studios/{studioId}/destinations/{destinationId}/broadcast and to the thumbnail endpoint — the supported way to obtain one; do not read it out of settings, which has no contract. While attached with a resolved ingest binding, this remains the actual session broadcast even if the saved pick is changed or cleared; that edit applies to the next attachment. For idle destinations and older sessions without a binding, a PICKED id follows the saved pick. An AUTO-CREATED one (create mode) names the broadcast minted for the current or most recent session and goes STALE once that broadcast completes, since the next go-live mints a fresh one — re-read it after going live rather than caching it across sessions. Always present; empty for a channel-scoped platform (twitch, kick, linkedin), a create-mode destination that has never gone live, or a cleared pick.
broadcastConfig BroadcastConfig no —
broadcastError string no Platform go-live error independent of encoder lastError. Transient errors retry within a bounded start window; terminal errors include recovery guidance.
broadcastLifecycle string no Last YouTube-observed lifecycle for the current activeBroadcastId and encoder session. Empty or absent means unknown. Only live confirms audience-live at broadcastObservedAt; destination status LIVE describes encoder delivery.
broadcastObservedAt string no RFC3339 time of the last provider lifecycle observation. This timestamp is a past observation, not a continuous live guarantee.
chatBridgeEnabled boolean no Whether this destination's chat is pushed in through the custom chat bridge (POST .../chat/messages, scope chat:ingest), for a platform TRaX has no chat API for. Its messages appear in the studio chat as platform custom with this destination's name.
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, 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 —

DestinationChatMessage

Field Type Required Description
author object yes —
destinationId string yes —
id string yes TRaX's id for the message. For a bridged message, <destinationId>:<your id>.
platform string yes The chat platform, or custom for bridged chat.
text string yes —
timestamp string yes —

DestinationList

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

DestinationViewers

Field Type Required Description
destinationId string yes —
name string no —
platform string yes Lower-case platform name.
state string (enum) yes (one of: live, hidden, offline, unavailable, rate_limited, needs_reauth, error, pending)
updatedAt string no —
viewers integer yes Concurrent viewers. A number only in state live; null otherwise, never 0 for an unknown count.

EffectiveBroadcastConfig

Field Type Required Description
autoStart boolean yes contentDetails.enableAutoStart. Defaults to TRUE when unset — which is why it is reported: leaving it alone still gets you auto-start.
autoStop boolean yes contentDetails.enableAutoStop. Defaults to TRUE when unset.
description string yes snippet.description. Empty is omitted from the insert.
dvr boolean yes contentDetails.enableDvr. Defaults to TRUE when unset.
enableEmbed boolean no Absent means the field is omitted and YouTube's default applies.
enableMonitorStream boolean no Absent means the legacy derivation applies: the monitor stream is on when auto-start is off.
latency string (enum) yes contentDetails.latencyPreference. Defaults to normal. (one of: normal, low, ultraLow)
madeForKids boolean yes status.selfDeclaredMadeForKids. Defaults to false.
privacy string (enum) yes status.privacyStatus. Defaults to unlisted. (one of: public, unlisted, private)
projection string yes contentDetails.projection. Empty means the field is omitted entirely.
recordFromStart boolean no Absent means the field is omitted and YouTube's default applies.
scheduledStartTime string yes snippet.scheduledStartTime, RFC3339. EMPTY means none is stored and go-live will stamp thirty seconds from whenever it runs — a fact, not a missing field.
streamDelayMs integer yes contentDetails.monitorStream.broadcastStreamDelayMs. Written only when greater than zero AND the monitor stream is on.
title string yes snippet.title. Defaults to "TRaX Stream".

Error

Field Type Required Description
error object yes —

FrameRate

Field Type Required Description
den integer yes Denominator, e.g. 1001.
fps number yes num/den as a decimal, e.g. 59.94. DISPLAY ONLY.
num integer yes Numerator, e.g. 60000.

GoLiveDestinationFailure

Field Type Required Description
id string yes —
message string yes —
name string yes —
platform string yes —

GoLiveResult

Field Type Required Description
destinationFailures GoLiveDestinationFailure[] no Destinations that failed to start. Other destinations or the program session may still have started.
destinationIds string[] yes Enabled destination IDs armed by this start, excluding known failures and duplicates. May be empty for a valid program-only session; does not prove audience-live.
destinations Destination[] no Current destination delivery details, including failed or skipped legs. Poll destinations after this accepted start.
encoderStreamId string yes —
skippedDuplicates string[] no Destination IDs skipped because their push targets duplicate another destination.

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.

IngestChatEmote

Field Type Required Description
end integer yes Exclusive character offset into text.
id string yes —
name string no —
start integer yes Inclusive character offset into text.
url string yes https only; never fetched by TRaX.

IngestChatMessage

Field Type Required Description
author object yes —
emotes IngestChatEmote[] no —
id string yes Your id for the message. Dedupe key within the destination: an id seen recently answers duplicate, so a retry is safe.
sentAt string no RFC 3339. Absent or in the future means the arrival time.
text string yes Plain text, up to 500 characters.

IngestChatRequest

Field Type Required Description
messages IngestChatMessage[] yes —

IngestChatResponse

Field Type Required Description
accepted integer yes How many messages were delivered.
results IngestChatResult[] yes One per message, in request order.

IngestChatResult

Field Type Required Description
id string yes —
outcome string (enum) yes accepted: delivered to the studio chat. duplicate: this id was seen recently; nothing to do. overloaded: retry this message shortly. rejected: it breaks the schema; see reason. closed: the bridge closed while delivering; retry. (one of: accepted, duplicate, overloaded, rejected, closed)
reason string no Why a rejected message was refused.

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.

IntercomGrant

Field Type Required Description
canJoinMic boolean yes —
canPublish boolean yes —
credentialExpiresAt string yes RFC3339 timestamp; empty when no observation exists.
expiresAt string yes RFC3339 timestamp; empty when no observation exists.
iceServers IntercomICEServer[] yes —
mediamtxBaseUrl string yes —
membershipId string yes —
publishPath string yes Own private room audio slot, also present for listeners. Exclude self when subscribing.
room IntercomRoom yes —
selfId string yes —
token string yes Private media credential. Never log, cache, or include in SSE.
whipUrl string yes —

IntercomICEServer

Field Type Required Description
credential string no Private TURN credential. Do not log or share.
urls string[] yes —
username string no —

IntercomMember

Field Type Required Description
audioPath string yes —
canPublish boolean yes —
expiresAt string yes RFC3339 timestamp; empty when no observation exists.
listenLevel number yes —
listenMuted boolean yes —
listenState string (enum) yes Client-reported transport status, not server proof of audible audio. (one of: unspecified, idle, connecting, connected, failed)
mediaErrorCode string (enum) yes (one of: none, permission_denied, publish_failed, subscribe_failed, playback_blocked, credential_expired)
mediaObservedAt string yes RFC3339 timestamp; empty when no observation exists.
membershipId string yes —
microphoneState string (enum) yes Client-reported publisher status, not proof of program routing. (one of: unspecified, idle, connecting, connected, failed)
name string yes —
participantId string yes —
talking boolean yes —

IntercomRoom

Field Type Required Description
channel string yes —
participants IntercomMember[] yes —
studioId string yes —
updatedAt string yes RFC3339 timestamp; empty when no observation exists.

IntercomRoomList

Field Type Required Description
data IntercomRoom[] yes —

JoinIntercomRequest

Field Type Required Description
channel string (enum) yes (one of: crew)
idempotencyKey string yes Required fresh key for a new intentional join. Reuse only to retry that same request; retired keys never replace newer memberships.

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. Trovo shut down live streaming on 2026-06-30 and is refused with a 400. (one of: twitch, youtube, kick, facebook, linkedin)
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.

NextBroadcastPlan

Field Type Required Description
action string (enum) yes What go-live will do. legacy means no broadcast at all — the channel's default stream key — and always carries a warning. (one of: create, reuse, pick, legacy, none)
broadcastId string yes The broadcast go-live will target, when one is already known. Empty for create and legacy: there is nothing to name yet.
defaulted string[] yes The effective field names whose value came from a DEFAULT rather than from something the operator saved. This is the half you cannot derive: autoStart true because it was chosen and autoStart true because it was never set are the same byte and different facts. Never null.
destinationId string yes The destination this plan describes.
effective EffectiveBroadcastConfig yes —
estimatedQuotaUnits integer yes OUR ESTIMATE of the YouTube quota this go-live spends. Google publishes no per-method costs for the Live Streaming API, so this is inferred from the Data API pattern and assumes the persistent-stream optimisation is on. Never present it as YouTube's published figure.
mode string yes The stored broadcastConfig.mode: create, pick, or empty when none is saved. An empty mode is why action can be legacy.
quotaEstimateInferred boolean yes Always true. Present so the caveat above travels with the number.
reason string yes One sentence explaining the action, written for a person. Where the outcome depends on YouTube's own state this says so rather than guessing.
warnings string[] yes Conditions worth showing before going live — a destination that will fall back to the legacy stream key, a pick that create mode is about to discard. Never null; empty is the normal case.

Ok

Field Type Required Description
ok boolean yes —

OutputAdvancedPolicy

Field Type Required Description
appliesTo string (enum) yes (one of: next_server_session)
caps object yes —
effective object no Optional resolved next-session plan. Absent when native profile/vendor defaults are unresolved; never an applied acknowledgement.
error string no —
mode string (enum) yes (one of: auto, manual)
selected object yes —
valid boolean yes —

OutputBitrateAdvisory

Field Type Required Description
code string yes —
message string yes —
platform string yes —

OutputBitrateConstraint

Field Type Required Description
guidanceVerified boolean yes Primary guidance reviewed; not proof of a live provider ingest test.
hard OutputBitrateLimits yes —
platform string yes —
preferredKbps integer no Trax AUTO preference, including a choice within a recommendation range.
recommendedMaxKbps integer no Soft provider recommendation, not an enforced maximum.
recommendedMinKbps integer no Soft provider recommendation, not an enforced minimum.
sourceUrl string no Official supporting provider guidance when available.

OutputBitrateLimits

Field Type Required Description
maxKbps integer no Verified positive maximum; absent means not established, not unlimited.
minKbps integer no Verified positive minimum; absent means not established.

OutputBitratePolicy

Field Type Required Description
advisories OutputBitrateAdvisory[] yes —
appliesTo string (enum) yes Local browser/WebCodecs/WHIP publishing does not consume this target. Saved cloud intent remains available. (one of: next_server_session)
constraints OutputBitrateConstraint[] yes —
error string no Safe actionable reason when invalid.
hard OutputBitrateLimits yes —
localBrowserSupported boolean yes False in this producer. Do not render the target as a local browser bitrate.
mode string (enum) yes (one of: auto, manual)
scope string (enum) yes (one of: shared_program)
selectedKbps integer no Present only for manual intent; AUTO has no manual override.
targetKbps integer yes Canonical next-session video target; zero when invalid. Not measured throughput.
valid boolean yes —

OutputCaps

Field Type Required Description
cappedBy string yes The platform whose ingest limit set the ceiling, e.g. "twitch". EMPTY STRING when nothing is capping — read this rather than inferring a cap from maxWidth, which equals the ladder top in the uncapped case.
maxHeight integer yes The matching height. Equals the top of the ladder (3840x2160) when nothing caps.
maxWidth integer yes The highest width every enabled destination will accept today.
supported Resolution[] yes The full supported ladder, ascending. This is the exact set PATCH accepts; offer these and nothing else.
supportedCodecs string[] yes The exact codec values PATCH accepts right now, e.g. ["h264"]. This is the egress truth, not a roadmap: "h265" appears here the moment an HEVC-capable egress leg exists, and the validator accepts it the same moment. Offer these and nothing else.
supportedColorimetry Colorimetry[] yes The color systems the pipeline can actually run. Exactly one entry today — SDR BT.709 8-bit — and read-only everywhere: there is no colorimetry write path until the pipeline can honour one. Render your capability UI from this rather than hardcoding SDR.
supportedFrameRates FrameRate[] yes The frame rates PATCH accepts, as exact rationals.
supportsHdr boolean no Whether hdr=true is admitted by this deployment; false does not reject explicit hdr=false.

OutputFormat

Field Type Required Description
colorimetry Colorimetry yes —
fps number no The decimal rendering of fpsNum/fpsDen, e.g. 59.94. DISPLAY ONLY, and present only where the server computed it (the output endpoint). Never send it back as a setting: 59.94 could be 60000/1001 or 5994/100, and the encoder treats those differently.
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.

OutputVideoAdmission

Field Type Required Description
bitrateKbps integer yes Requested current-session video target, not measured or acknowledged applied bitrate.
codec string yes —
format object yes Immutable requested geometry and frame rate.
hdr boolean yes —
sessionId string 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.

Playback

Field Type Required Description
createdAt string yes When playback was turned on, RFC 3339.
embedHtml string yes A ready-to-paste <iframe> pointing at watchUrl + ?embed=1.
enabled boolean yes Always true on a returned playback.
playbackId string yes The PUBLIC playback id (128 random bits, base64url). Safe to embed; anyone holding it can watch while playback is on.
player object yes —
sourceId string yes —
studioId string yes —
updatedAt string yes When it last changed, RFC 3339.
visibility string (enum) yes (one of: public, unlisted)
watchUrl string yes The hosted watch page: https://watch.traxstreaming.live/p/<playbackId>.

PlaybackIceServer

Field Type Required Description
credential string no —
urls string[] yes —
username string no —

PlaybackRequest

Field Type Required Description
visibility string (enum) no unlisted (default) or public. Token minting is identical for both; public additionally lets TRaX's own surfaces list or index the watch page. (one of: public, unlisted)

PlaybackToken

Field Type Required Description
expiresAt string yes RFC 3339; two minutes after minting. Bounds when a NEW connection may be made; a playing session outlives it.
iceServers PlaybackIceServer[] yes ICE servers for the peer connection. May be empty.
retryPolicy object yes Backoff bounds for reconnects and for retrying a 429 or 503 (never a 404). Start at minBackoffMs, grow exponentially with jitter, cap at maxBackoffMs, and never retry a 429 sooner than its Retry-After.
whep object yes —

ProgramOutput

Field Type Required Description
fps number no The decimal rendering of fpsNum/fpsDen, e.g. 59.94. DISPLAY ONLY, and present only where the server computed it (the output endpoint). Never send it back as a setting: 59.94 could be 60000/1001 or 5994/100, and the encoder treats those differently.
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.

ReleaseContributeRequest

Field Type Required Description
deviceId string no The SAME stable device identifier passed to POST .../contribute. It resolves to the input to release. An empty or unknown value is a no-op success (idempotent teardown); a value that belongs to another user is 403.

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)

Resolution

Field Type Required Description
available boolean yes false = the rung is coming soon and cannot be selected yet.
height integer yes —
status string (enum) yes (one of: available, coming_soon)
width integer yes —

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 Must contain at least one non-whitespace character.

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.

SetIntercomMicrophoneRequest

Field Type Required Description
publish boolean yes Explicit true requests mic publishing; false returns to listen-only. A client must close its owned publisher and tracks on false.

ShowSettings

Field Type Required Description
applyOnGoLive boolean no False keeps the settings saved and stops go-live pushing them. Defaults true on first save.
createdAt string no —
description string no —
kickCategoryId string no Kick category_id.
kickCategoryName string no Display name for kickCategoryId. The ID is authoritative — see youtubeCategoryName.
language string no BCP-47, e.g. en. Twitch takes it as broadcaster_language; the others ignore it.
tags string[] no Shared across platforms; each drops what it cannot take. Twitch forbids spaces and caps each tag at 25 characters, Kick does not.
title string no At most 100 CHARACTERS, not bytes. 100 is YouTube's limit and the lowest of the three (Twitch 140, Kick at least 1), so it is the only length that works everywhere.
twitchBrandedContent boolean no Twitch is_branded_content, the paid-promotion disclosure. NULL or omitted leaves it untouched; false is a real value that is sent.
twitchContentLabels string[] no Twitch content_classification_labels ids, e.g. DrugsIntoxication. NULL or omitted means say nothing to Twitch, leaving whatever the operator set in Twitch's own UI; an EMPTY ARRAY means enable none. Twitch replaces the complete set on every write, so those two are not the same. Twitch-only: YouTube's nearest equivalent is a made-for-kids boolean with the opposite polarity and Kick has no such concept.
twitchGameId string no Twitch game_id. A YouTube categoryId is NOT valid here — these are unrelated id spaces.
twitchGameName string no Display name for twitchGameId, e.g. "Just Chatting". The ID is authoritative — see youtubeCategoryName.
updatedAt string no —
youtubeCategoryId string no YouTube categoryId. Empty means send NO category to YouTube, which is not the same as sending an empty one.
youtubeCategoryName string no Display name for youtubeCategoryId, stored so a reload shows "Gaming" rather than 20. The ID is authoritative: this is never resolved back into one, and a mismatched pair sends the ID.

Source

Field Type Required Description
active boolean yes —
camera object no Remote Camera Control state, or absent on an ordinary source: { capabilities, desired, actual, control }. The device owns capabilities+actual, the studio owns desired, the server owns control. The ingest secret and the device session token NEVER appear here — only the public publishSessionId does. The schema is open (per-lens, evolving).
createdAt string no —
device object no Remote Camera Control device block for a phone bound to this source, or absent on an ordinary source: { attached, kind, label, platform, model, attachedAt, mine }. mine is true only for the caller whose deviceId matches (pass it as the ?deviceId= query on GET). The deviceId itself is NEVER returned. The schema is open because it evolves.
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.

StreamKey

Field Type Required Description
frameRate string no cdn.frameRate: 30fps | 60fps | variable. Absent when unknown.
id string yes Platform-side id. The value the edit, delete and bind endpoints take.
ingestionType string no Ingest protocol; rtmp today.
isDefault boolean no The channel's built-in default key.
keyLast4 string no Last 4 characters of the ingest stream name — NON-SENSITIVE. The only recognition handle this API exposes; the ingest key itself is never returned.
mintedByTrax boolean no True when TRaX minted this key.
resolution string no cdn.resolution: 240p .. 2160p, or variable. Absent when unknown.
reusable boolean no A reusable key may be bound to many broadcasts over time.
streamStatus string no active (receiving video now — cannot be edited or deleted) | inactive | ready | created | error. Absent when unknown.
title string yes Display title. TRaX-minted keys are named "TRaX · · 1080p60".

StreamKeyList

Field Type Required Description
data StreamKey[] yes —
nextCursor string no —

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 no The studio.s audio mixer — the same shape GET /v1/studios/{id}/audio returns. Present only with audio:read (or a write scope that implies it); omitted otherwise.
canvas CanvasState no The studio.s canvas/scenes/presets state — the same shape GET /v1/studios/{id}/canvas returns. Present only with canvas:read (or a write scope that implies it); omitted otherwise.
chatConnectors ChatConnector[] no Every linked platform's chat connector state. Empty when no chat platforms are linked — state, not an omission. Present only with chat:read (or a write scope that implies it); omitted otherwise.
destinations Destination[] yes —
intercomRooms IntercomRoom[] no —
sources Source[] yes —
streamStatus StreamStatus yes —

StudioList

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

StudioOutput

Field Type Required Description
advancedPolicy OutputAdvancedPolicy no —
appliesToNextGoLive boolean yes Always true, and stated here rather than only in prose: resolution and frame rate are SET-BEFORE-LIVE. The encoder does not renegotiate format mid-session, so a studio on air keeps what it started with until it goes offline and live again.
bitratePolicy OutputBitratePolicy no —
caps OutputCaps yes —
codec string yes The video codec the EFFECTIVE format encodes with, e.g. "h264" — what the next go-live actually runs, after the egress capability gate. Settable since #674 via PATCH codec, but only to values in caps.supportedCodecs; see selectedCodec for the persisted intent.
currentAdmission OutputVideoAdmission no —
effective OutputFormat yes What the NEXT go-live will actually composite and encode at, after the destination soft-cap and the frame-rate ceiling.
hdr boolean no Effective next-session HDR canvas decision; does not enable unsupported HDR.
selected OutputFormat yes What the operator picked and what is persisted. Returned unchanged even when a destination cap means it is not what airs.
selectedCodec string yes The persisted codec INTENT: what was written via PATCH, empty string when never selected (automatic — the resolution decides). Differs from codec only when a stored intent is no longer in caps.supportedCodecs (a capability was withdrawn), in which case the effective codec falls back to the automatic choice rather than airing dead video.
selectedHdr boolean no Persisted HDR intent.
studioId string yes —
targetBitrateKbps integer yes Canonical next-GoLive video target after shared destination policy. Zero when bitratePolicy.valid is false. Not observed live throughput.

ThumbnailResult

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

UpdateAccountRequest

Field Type Required Description
username string yes The requested @handle. Mixed case and a leading @ are accepted and canonicalized (lowercased). Rules: 3-20 chars of [a-z0-9_], no leading/trailing/double underscore, not on the reserved blocklist. Refusals: 422 invalid_username (with the format reason), 409 username_taken / username_reserved, 429 username_cooldown (with Retry-After).

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 —
chatBridge boolean no Turn the custom chat bridge on or off. Refused with 409 destination_has_platform_chat when TRaX already reads this destination's chat from the platform.
enabled boolean no —
name string no —
platform string no —
rtmpUrl string no New ingest URL for a pasted-credential destination. Absent = keep the stored value. When supplied it must be an absolute rtmp:// or rtmps:// URL including a host; any other scheme, a bare path or hostname, leading/trailing whitespace, or an empty string is rejected with 400 invalid_request (there is no way to clear this field — a destination without an ingest URL has nowhere to publish). SRT is not accepted here; see POST /v1/connections/manual.
streamKey string no Write-only; never returned on any read.
vertical boolean no —

UpdateIntercomMembershipRequest

Field Type Required Description
listenLevel number no —
listenMuted boolean no —
listenState string (enum) no (one of: idle, connecting, connected, failed)
mediaErrorCode string (enum) no (one of: none, permission_denied, publish_failed, subscribe_failed, playback_blocked, credential_expired)
microphoneState string (enum) no (one of: idle, connecting, connected, failed)
talking 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 —

UpdateStreamKeyRequest

Field Type Required Description
frameRate string no New cdn.frameRate; empty leaves it unchanged.
resolution string no New cdn.resolution; empty leaves it unchanged.
title string no New title; empty leaves it unchanged.

UpdateStudioOutputRequest

Field Type Required Description
advancedMode string (enum) no auto clears all four overrides; omit explicit values with auto. Normal Studio edit authorization applies; no live retuning. (one of: auto, manual)
audioBitrateKbps integer (enum) no Next-session AAC kbps; destination hard limits may narrow this list. Omit to preserve. (one of: 128, 160, 192, 256, 320)
bitrateKbps integer no Manual video kbps. Actual destination/encoder hard bounds can be narrower and are reported in bitratePolicy.hard; invalid requests return actionable400. A value-only PATCH requires existing manual mode. No silent clamp.
bitrateMode string (enum) no Next-GoLive policy. AUTO clears the saved override and requires bitrateKbps to be omitted. MANUAL requires a positive value when first selected. Existing Studio edit permission; no live retune. (one of: auto, manual)
clearFields string (enum)[] no Reset only named next-session overrides, preserving other settings. Rejects setting and clearing the same field, unknown names, duplicates, and combination with advancedMode auto.
codec string no Video codec intent, validated against current caps.supportedCodecs and actual destination routes. Empty clears to automatic. Omit to preserve.
encoderPreset string (enum) no Abstract next-session intent; not a raw vendor profile value. (one of: quality, balanced, low_latency)
fpsDen integer no Frame-rate denominator. Must pair with fpsNum and must be positive — a zero here was silently accepted before this endpoint existed, and became 60/1 at the next go-live while the caller was told "saved".
fpsNum integer no Frame-rate numerator. Must pair with fpsDen.
hdr boolean no Next-session HDR canvas intent, validated against caps.supportsHdr. Explicit false is preserved. This does not enable HDR on unsupported deployments.
height integer no Program height: 720, 1080, 1440 or 2160.
keyframeIntervalSeconds integer (enum) no Next server session; whole seconds. Omit to preserve. Use advancedMode auto to clear all advanced overrides. (one of: 1, 2, 4)
rateControl string (enum) no Requested next-session mode; hardware may report a downgrade. Omit to preserve. (one of: cbr, vbr)
width integer no Program width. Must pair with height and land on the ladder exactly: 1280, 1920, 2560 or 3840.

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.

ViewerBroadcast

Field Type Required Description
average number yes Mean studio total over polls with a known total.
broadcastStartedAt string yes —
destinations object[] yes —
lastSampledAt string yes —
peak integer yes Highest studio total. Null when no destination ever reported a count.
samples integer yes Polls recorded.

ViewerCounts

Field Type Required Description
broadcastStartedAt string no Start of the current broadcast; the key used by viewers/history.
destinations DestinationViewers[] yes —
live boolean yes Whether the studio is live. False means no destinations are listed.
partial boolean yes True when at least one destination is not in state live, so total undercounts.
sampledAt string no Time of the latest poll. Absent before the first poll.
total integer yes Sum over destinations in state live. Null when none is.

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.