GENERATED from
trax-public-api /v1 openapi.json (baked copy: scripts/specs/v1-openapi.json)byscripts/sync-generated.sh— do not edit by hand. Baked copy. Refresh with:cp ../trax-public-api/internal/openapi/openapi.json scripts/specs/v1-openapi.json && npm run sync.
TRaX Developer API — /v1 reference
This page is generated from the
/v1OpenAPI 3.0 spec. The live, machine-readable contract is served athttps://api.traxstreaming.live/v1/openapi.json— point Postman, Insomnia, or a codegen tool at that URL. This page is a browsable rendering of the same spec (version1.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 theX-API-Keyheader), or an interactive-login user JWT (Authorization: Bearer <oidc-jwt>, the native-app OAuth + PKCE path) — see Authentication. Themetaendpoints (/v1/health,/v1/openapi.json) need no auth. - Errors: one envelope,
{ "error": { "code", "message", "requestId" } }— see Errors & status codes. - Lists: cursor-paginated,
{ "data": [ … ], "nextCursor": "…" }— see Pagination. - Rate limits: per-key token bucket,
429 + Retry-After— see Rate limits.
Endpoints
meta
Health + spec (unauthenticated)
GET /v1/health
Liveness probe (unauthenticated).
Auth: none (public).
Responses:
| Status | Description | Body |
|---|---|---|
200 |
Service is up. | Health |
default |
Error envelope. | Error |
GET /v1/openapi.json
This OpenAPI 3.0 document (unauthenticated).
Auth: none (public).
Responses:
| Status | Description | Body |
|---|---|---|
200 |
The OpenAPI spec. | object |
default |
Error envelope. | Error |
studios
Studios, sources, destinations, stream status
GET /v1/studios
List the caller's studios.
Requires scope studios:read.
Auth: API key required.
Parameters:
| Name | In | Required | Description |
|---|---|---|---|
cursor |
query | no | Opaque pagination cursor from a prior response's nextCursor. |
limit |
query | no | Max items per page (default 50, max 200). |
Responses:
| Status | Description | Body |
|---|---|---|
200 |
A page of studios. | StudioList |
default |
Error envelope. | Error |
POST /v1/studios
Create a studio.
Requires scope studios:write. Gated behind 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 touploadUrl, then call/completewith no body.multipart— nouploadUrlat all. PUT each entry ofpartsat its ownoffset/byteSizeto its ownurl, keep theETagresponse header each PUT returns, then call/completewith the full list. Parts may go up in any order; three in flight is a good number, and each is worth a couple of retries. Cut the file by the plan, never by a part size of your own — the server may have grown it to stay under the storage layer's part-count ceiling.
The reason large files come back as a plan: every request to our storage host crosses a CDN edge that caps ONE request body at 100 MB, and it enforces that at the edge — a 150 MB single PUT dies at 413 after about 2 MB. Parts are sized to stay under that cap, so the ceiling on a file is the account's quota, not the edge. The single/multipart split point is server policy and it moves; that is why you branch on mode rather than on a size of your own.
Not idempotent: calling this twice creates two assets and reserves quota twice. There is no dedupe on filename or content.
byteSize is checked against the remaining quota BEFORE any URL is issued, so an upload that cannot fit is refused here (402 storage_quota_exceeded) rather than after the bytes are sent. contentType decides the asset's kind and must be one we accept — video/mp4, video/webm, video/quicktime, image/jpeg, image/png, image/webp, image/gif, audio/mpeg, audio/wav, audio/ogg, audio/aac.
Auth: API key required.
Responses:
| Status | Description | Body |
|---|---|---|
201 |
An upload was reserved. Read mode before anything else. |
MediaUpload |
default |
Error envelope. | Error |
DELETE /v1/media/{assetId}
Delete an asset and release its storage.
Requires scope media:write. Gated behind 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 fromeffective.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 · |
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. |