# TRaX Developer Docs — full text # Our API philosophy TRaX is **API-first and community-friendly**. The platform is a set of capabilities — studios, sources, destinations, going live, stream status, platform connections, chat, and media — and we want you to build on all of it. Apps, bots, dashboards, mobile controllers, full third-party "thick clients" that produce a show without ever opening our web studio: that's the point, not a side effect. ## Open-by-default Our default posture is to **expose as much of the platform as we safely can**. When we look at a capability and ask whether third-party developers should be able to use it, we treat that as the wrong question. The only question we ask is: > **Can we offer this safely?** If a capability can be offered safely, the default answer is *yes, expose it.* We don't hoard surface area, and we don't make you screen-scrape the web app to do something the platform already knows how to do. ## What "safely" means "Safely" isn't a vibe — it's a checklist, and it's the *only* thing we gate on: - **Authenticated.** Every request carries an identity. You act as yourself using an API key you create, or your app acts on a user's behalf with their consent through OAuth. - **Scoped.** A credential can do only what its scopes allow, and never more than you can already do yourself. A read-only key stays read-only. - **Isolated to your tenant.** A credential can only touch resources that belong to it. It can never reach another account's studios, streams, or data. - **Rate-limited.** Every credential has fair-use limits so one integration can't degrade the platform for everyone else. - **Leak-free.** Our public surfaces never hand back another user's data or our internal plumbing. Clear those, and we lean toward giving you the key. ## Where this is going Today the media ingest plane (SRT, RTMP, WHIP/WHEP) is a real, documented contract you can push and pull streams over — see the [Reference](/reference/authentication) section. The control-plane **[Developer API (`/v1`)](/reference/public-api)** is live in dev preview: list your studios, read sources / destinations / live status, with **user-minted, scoped API keys** as the first-class way to authenticate as yourself, and go-live / go-offline plus stream-event webhooks close behind. Building a mobile or desktop app? Start with **[Build a client](/guides/build-a-client)**. Want to build something specific? Tell us — the roadmap follows what the community wants to build. # Getting started with TRaX TRaX is a browser-based live production platform: an OBS-style scene canvas in the browser, multi-host shows, and simultaneous output to multiple destinations (Twitch, YouTube, Kick, Facebook, and any custom RTMP endpoint). Under the studio sits a media ingest plane that accepts and serves standard streaming protocols: | Direction | Protocols | |---|---| | Push a feed into TRaX | SRT, RTMP / RTMPS, WHIP (WebRTC) | | Pull a feed out of TRaX | WHEP (WebRTC), SRT | These protocol contracts are documented in the [Reference](/reference/authentication) section and are usable today with credentials provisioned for you by the TRaX team. ## Building an app or integration? Two dedicated guides cover programmatic access: - **[Developer API (`/v1`)](/reference/public-api)** — a live, versioned REST + JSON API (dev preview at `api-dev.traxstreaming.live/v1`): list your studios, read sources / destinations / stream status, with an OpenAPI spec you can generate a typed client from. Authenticated with a `sk_live_…` API key you mint yourself. - **[Build a client](/guides/build-a-client)** — for iOS / Android / desktop apps: the transport map (WebSocket to control a studio, WebRTC to publish and watch, REST for reads) and how a native app logs a user in with OAuth PKCE. - **[Put TRaX chat on your own site](/guides/embed-chat)** — a drop-in `` element that shows the broadcast's merged Twitch / YouTube / Kick chat to *your* members, on *your* domain, with no TRaX account for them and no API key in the browser. - **[Control the mixer from your app](/guides/control-the-mixer)** — read a studio’s audio mix and mute or set a level on any source, through the same control path the studio’s own mixer uses. Development only today. - **[Control the canvas over /v1](/guides/control-the-canvas)** — read a studio’s whole canvas in one call, move a tile, pick a preset or recall a scene, and hand the arrangement back to the auto planner. Built for a phone: conditional reads, small bodies, and writes that are safe to retry. Development only today. - **[Stream to a live input from your app](/guides/stream-to-a-live-input)** — create an input over `/v1`, read its SRT / RTMP publish URL, and point an encoder at it. Development only today. ## Media authentication Access to the TRaX **media plane** is authenticated per transport. Browser publishing and playback (WHIP / WHEP) use short-lived **ES256 session JWTs** minted by the platform (our identity provider is OIDC-based). Publishing to an input over **SRT or RTMP** uses the input's **stream key**, carried inside the publish URL the platform hands you. What that means for pushing and pulling media: - You can push to an input **today**: create it over `/v1` and read its publish URL ([Stream to a live input](/guides/stream-to-a-live-input), dev only), or copy the URL from the studio's Connection Information panel. - You can verify any TRaX-issued token yourself against our published JWKS — see [Authentication & session tokens](/reference/authentication). - A first-party app logs the user in with OAuth (PKCE) and uses that user token across the control and media planes — see [Build a client](/guides/build-a-client). ## Where to go next 1. [Authentication & session tokens](/reference/authentication) — how every request is authorized, token claims, JWKS verification. 2. [SRT: the streamid contract](/reference/srt-streamid) — the most robust way to push a feed. 3. [RTMP / RTMPS](/reference/rtmp) — the most compatible way. 4. [WHIP / WHEP](/reference/whip-whep) — browser-native, sub-second latency. 5. [Push a feed with OBS or ffmpeg](/guides/obs-ffmpeg-quickstart) — a 10-minute end-to-end win; [Stream to a live input](/guides/stream-to-a-live-input) is the same thing driven from your own code. 6. [Bring your own storage](/guides/byo-storage-setup) — use your own S3-compatible bucket as a media library and record your program straight into it ([cloud recording](/guides/cloud-recording)). # Build a client (iOS, Android, desktop) TRaX is built to be driven by your own code — including full third-party "thick clients" that produce a show without ever opening our web studio. A rich client doesn't speak one protocol; it speaks **several, one per job**. This guide is the map: which transport for which task, and how to authenticate each. ## The two authentication models — pick by who you are Before anything else, decide *who your code acts as*: | You're building… | Authenticate as | Credential | |---|---|---| | A **first-party app a user logs into** (mobile / desktop that produces or controls a show) | **the user** | OAuth 2.0 (PKCE) → user access token (JWT), sent as `Authorization: Bearer ` | | **Server-to-server automation / a third-party integration** (dashboards, bots, headless jobs) | **yourself** (the developer) | an API key: `Authorization: Bearer sk_live_…` on the [`/v1` REST API](/reference/public-api) | These aren't mutually exclusive — a mobile app might log the user in with OAuth for live control *and* call `/v1` with the user's token for reads — but the question "am I logging a **user** in, or running **my own** automation?" decides your primary path. ## Transport map — which API for which job | I want to… | Transport | How | Auth | |---|---|---|---| | **Control a studio** + receive live state (scenes, sources, go-live, presence, chat control) | **WebSocket** | connect to the studio control plane (below) | user bearer | | **Publish** camera / mic / screen from the app | **WebRTC WHIP** | POST an SDP offer to the studio's WHIP endpoint — see [WHIP / WHEP](/reference/whip-whep) | media session token (Bearer) | | **Publish** a hardware / OBS / camera feed | **SRT** or **RTMP(S)** | [SRT streamid](/reference/srt-streamid) · [RTMP](/reference/rtmp) | the input's stream key, inside the publish URL | | **Stream to a live input** the app created itself | **SRT** or **RTMP(S)** | `POST …/sources` then `POST …/sources/{sourceId}/ingest` for the URL — see [Stream to a live input](/guides/stream-to-a-live-input) *(dev only today)* | `sk_live_` key (`sources:write` + `sources:ingest`) to provision, stream key in the URL to publish | | **Contribute a phone camera** into a studio as a participant | **SRT** | provision an ingest, then publish — see [Contribute from a phone](/guides/contribute-from-a-phone) | user bearer to provision, scoped stream key in streamid to publish | | **Watch** the program output (confidence monitor) | **WebRTC WHEP** | mint a read token + URL, then play — see [Monitor the program output](/guides/monitor-the-program) | media read token (Bearer) | | **Watch** the program or a source preview — **now** | **WebRTC WHEP** | POST an SDP offer to the WHEP endpoint — see [WHIP / WHEP](/reference/whip-whep) | media read token (Bearer) | | **Watch** at lowest latency — **later** | **MoQ** *(roadmap)* | not shipped yet; use WHEP today | — | | **List / read** studios, sources, destinations, stream status; **go live / offline** | **REST `/v1`** | the [Developer API](/reference/public-api) | `sk_live_` API key (or user bearer) | **Rule of thumb for a thick client:** one **WebSocket** to control a studio and stream its live state; **WebRTC** (WHIP up / WHEP down) for media; **REST** (`/v1`) for headless reads and automation. Stream **paths** (the `studios/{id}/…` names you publish and play) follow one grammar — see [Stream paths](/reference/path-grammar). ## Native-app authentication — OAuth 2.0 with PKCE A first-party app logs the user in against the TRaX identity provider using the **Authorization Code flow with PKCE** (the standard, secret-less flow for native and single-page apps). You get back a **user access token (JWT)** and a refresh token; the access token is what every TRaX surface accepts as `Authorization: Bearer `. **Identity provider (dev):** `https://auth-dev.traxstreaming.live` | Purpose | Endpoint | |---|---| | Authorize | `https://auth-dev.traxstreaming.live/oauth/v2/authorize` | | Token | `https://auth-dev.traxstreaming.live/oauth/v2/token` | | End session (logout) | `https://auth-dev.traxstreaming.live/oidc/v1/end_session` | Scopes: `openid profile email`. You'll register your app (to get a **client id** and your allowed **redirect URIs**) with the TRaX team — contact us for a native app registration. **The flow:** 1. **Generate a PKCE pair.** Create a random `code_verifier`, then `code_challenge = BASE64URL(SHA256(code_verifier))`. 2. **Open the authorize URL** in a system browser / `ASWebAuthenticationSession` (iOS) / Custom Tab (Android) — not an embedded webview: ``` https://auth-dev.traxstreaming.live/oauth/v2/authorize ?response_type=code &client_id= &redirect_uri= &scope=openid%20profile%20email &code_challenge= &code_challenge_method=S256 ``` Use a **custom URI scheme** (`com.yourapp://callback`) or a loopback redirect for the native round-trip. 3. **Handle the redirect** — you receive `?code=`. 4. **Exchange the code for tokens** (no client secret — PKCE stands in for it): ```bash curl -sS https://auth-dev.traxstreaming.live/oauth/v2/token \ -H "Content-Type: application/x-www-form-urlencoded" \ -d grant_type=authorization_code \ -d client_id= \ -d code= \ -d redirect_uri= \ -d code_verifier= ``` The response includes `access_token` (a JWT), `refresh_token`, and `expires_in`. Store them in the platform keystore (iOS Keychain / Android Keystore), and use `grant_type=refresh_token` with the same `client_id` to get a fresh access token before it expires. Send the resulting access token as `Authorization: Bearer ` on the WebSocket upgrade, on WebRTC WHIP/WHEP signaling, and on `/v1` REST calls. > This is the same OAuth-PKCE pattern this docs site itself uses to log in — a > native app differs only in the redirect (custom scheme / loopback instead of a > web `/callback` page) and in storing tokens in the OS keystore. ### Native vs. browser: the header advantage A **native app can set request headers everywhere**, which keeps auth uniform: - **WebSocket** — set `Authorization: Bearer ` on the upgrade request. (Browsers can't set headers on a WebSocket handshake, so the web client has to fall back to passing the token another way — a native app has no such limit and should use the header.) - **WebRTC WHIP / WHEP** — set `Authorization: Bearer ` on the signaling `POST`. - **REST `/v1`** — `Authorization: Bearer …` as usual. Preferring the header on native keeps the token out of URLs and logs. ## Controlling a studio (WebSocket) Real-time studio control — scenes, sources, going live, presence, chat control, and the live state stream back to your UI — runs over a **WebSocket** to the studio control plane: ``` wss://studio-api-dev.traxstreaming.live/api/v1/ws/studios/{studioId}/events ``` Open it with the user's bearer token (header, or a `?token=` query param where headers are impossible); the server resolves the user's access to that studio once at upgrade, then gates each operation by role. This socket is version-gated — add a `?client=` build stamp to the connect URL (or send it as an `X-Trax-Client` header). Frames are JSON — request/reply RPCs plus server-pushed realtime events. For a concrete walkthrough — connecting, seeding the source list + layout, and the RPCs that add / move / activate sources and switch presets and scenes — see [Drive the studio canvas](/guides/drive-the-canvas). The **full RPC + event catalog** for this surface is first-party/internal today. If you're a logged-in first-party developer, see the internal reference for the exhaustive method and event lists (WS RPCs, WS events, and the private transport map). Third-party apps reach the platform through the public `/v1` REST gateway and this WebSocket — never internal service endpoints directly. ## Where to go next 1. [Developer API (`/v1`)](/reference/public-api) — REST reference, endpoint table, API keys, and OpenAPI client generation. 2. [Authentication & session tokens](/reference/authentication) — token claims and how to verify a TRaX-issued token against our JWKS. 3. [WHIP / WHEP](/reference/whip-whep) — publish and play media over WebRTC. 4. [SRT streamid](/reference/srt-streamid) · [RTMP / RTMPS](/reference/rtmp) — push a hardware or OBS feed. 5. [Contribute from a phone](/guides/contribute-from-a-phone) — join a studio as a camera source over SRT, with reconnection and QR walk-up guests. [Stream to a live input](/guides/stream-to-a-live-input) — the `/v1` path for an app that creates its own input and publishes to it with an API key. 6. [Drive the studio canvas](/guides/drive-the-canvas) — control layout, sources, presets, and scenes over the studio WebSocket. 7. [Monitor the program output](/guides/monitor-the-program) — play the studio's program feed over WHEP as a confidence monitor. 8. [Stream paths](/reference/path-grammar) — the `studios/{id}/…` path grammar shared by every media transport. Tell us what you're building — the roadmap follows what the community wants to ship. # Quickstart: push a feed to TRaX with OBS or ffmpeg Ten minutes from an input to a live feed. You need one thing: the input's **publish URL**, which carries the host, the stream path and the stream key in one string. Two places hand it to you: - **From your own code:** create a `generic-input` source and call `POST /v1/studios/{id}/sources/{sourceId}/ingest`. The full walk-through, including the response shape, is [Stream to a live input from your app](/guides/stream-to-a-live-input). - **From the studio:** open the input's **Connection Information** panel. It shows the same URLs, already split into the Server / Stream Key pair OBS wants. Either way the credential is the input's **stream key**. It appears twice in an SRT streamid and twice in an RTMP query string, and it is the whole capability; there is no token to add. Treat the full URL as a password. ## Option A — SRT (recommended) ### ffmpeg ```bash ffmpeg -re -i input.mp4 -c:v libx264 -preset veryfast -b:v 4M \ -c:a aac -b:a 160k -f mpegts \ "srt://:8890?streamid=publish:::" ``` ### OBS 1. Settings → Stream → Service: **Custom…** 2. Server: `srt://:8890?streamid=publish:::` 3. Stream Key: leave **empty** (everything rides in the streamid) Do not URL-encode the streamid, and keep it under 512 bytes; see the [SRT contract](/reference/srt-streamid) for the failure modes. ## Option B — RTMP / RTMPS ### ffmpeg ```bash ffmpeg -re -i input.mp4 -c:v libx264 -preset veryfast -b:v 4M \ -c:a aac -b:a 160k -f flv \ "rtmp://:1935/?user=&pass=" ``` ### OBS OBS publishes to `/`, so the URL splits at its last `/`: 1. Settings → Stream → Service: **Custom…** 2. Server: `rtmp://:1935/inputs` 3. Stream Key: `?user=&pass=` Credentials ride in the **query string**, not in a `/live/` path and not in the URL's userinfo (`rtmp://user:pass@host/…` is not read and fails). For RTMPS use port 1936 and the `rtmps://` scheme; plain RTMP sends the key in clear text. See the [RTMP contract](/reference/rtmp). ## Verify it's flowing The input's tile in the studio's Sources panel goes live within a few seconds of the first packets, and back to offline within about five seconds of the encoder stopping. A healthy SRT contribution holds a stable round-trip and no reconnect churn; if the sender dies within the first second, check the streamid rules first. Publishing does not take the studio on air. A live input is something the operator places on the canvas; going live to destinations is a separate step ([details](/guides/stream-to-a-live-input#what-you-will-see-in-the-studio)). ## Recommended encoder settings | Setting | Value | |---|---| | Video codec | H.264, high profile | | Keyframe interval | 2 s | | Rate control | CBR, 4–8 Mbps for 1080p60 | | Audio | AAC 160 kbps, 48 kHz stereo | # Contribute a camera from a phone (SRT) A phone can join a TRaX studio as a **camera source** and publish its camera and mic over **SRT**. The phone is not compositing anything — the cloud encoder owns the live program, so the show keeps running even if the app backgrounds, the screen locks, or the signal drops for a moment. This is the ingest path the TRaX iOS app uses, and it is open to your own client. The model in one sentence: **a contributing phone is a participant whose device source has `transport = srt`.** Media rides SRT; identity and control ride the studio WebSocket. You reuse everything the platform already does with participants — the person grouping, the mixer strip, the canvas tile, mute and kick — instead of inventing a new kind of source. > **There are now two ways to do this, and this is the older one.** > [Go live from a phone over `/v1`](/guides/go-live-from-a-phone) provisions the > same input and hands back the same SRT credential in ONE call on the public > API, authenticated with an `sk_live_` key or an OIDC bearer like everything > else in `/v1`. It is the same server-side path — the same source row, the same > canvas tile, the same reconnection behaviour — so the two are > interchangeable rather than parallel implementations. > > Prefer `/v1` for a new client. Stay here when you also need the studio > WebSocket for identity and control, or the walk-up QR guest flow, neither of > which `/v1` carries. ## What you talk to Contribution is a first-party flow on the **studio control plane** (the studio API), not the `/v1` REST gateway. Three hosts are involved: | | Development | Production | |---|---|---| | Studio API (control plane) | `https://studio-api-dev.traxstreaming.live` | `https://studio-api.traxstreaming.live` | | Identity (OAuth login) | `https://auth-dev.traxstreaming.live` | `https://auth.traxstreaming.live` | | SRT ingest | `srt://ingest.traxstreaming.live:8890` | `srt://ingest.traxstreaming.live:8890` | Always publish to the exact `srtIngestUrl` the provisioning response hands you rather than hardcoding the ingest host. ## Authenticate as the user Every call below carries the user's access token: `Authorization: Bearer `, the OAuth 2.0 + PKCE token described in [Build a client](/guides/build-a-client#native-app-authentication--oauth-20-with-pkce). It is the same bearer you use for the studio WebSocket and for `/v1`. The one exception is the walk-up QR guest path, which uses a scoped greenroom session instead of an account (see [Walk-up guests](#walk-up-guests-qr) below). ## 1. Provision an SRT ingest Ask the studio for a scoped ingest, identifying the device that is about to publish: ``` POST /api/v1/studios/{studioId}/contribute/ingest Authorization: Bearer Content-Type: application/json { "deviceId": "9C1B...stable-per-install-id", "deviceLabel": "EJ's iPhone" } ``` - **`deviceId`** (required) is a stable id your app mints once and keeps in the Keychain / Keystore. It is what makes a reconnection resolve to the *same* camera tile instead of a new one. - **`deviceLabel`** is the human name shown in the studio ("EJ's iPhone"). You must be a member of the studio (any role). If contribution is not enabled for the studio, the endpoint returns `404`. The `201` response is the descriptor you publish with: ```json { "source": { "id": "mc-…", "name": "EJ's iPhone", "type": "mobile_contrib", "…": "…" }, "path": "inputs/mobile-contrib-mc-8f3c…", "streamId": "publish:inputs/mobile-contrib-mc-8f3c…::", "srtIngestUrl": "srt://ingest.traxstreaming.live:8890?streamid=publish:inputs/mobile-contrib-mc-8f3c…::", "autoSeat": true, "autoGoLive": true, "deviceId": "9C1B…" } ``` | Field | What it is | |---|---| | `srtIngestUrl` | The full SRT URL to publish to — streamid included. Use it verbatim. | | `streamId` | The SRT `streamid` on its own, if your SRT stack takes host and streamid separately. | | `path` | The stream path this contribution lives at (`inputs/mobile-contrib-`). | | `autoSeat` | Whether the source auto-seats on the canvas when it starts publishing (see below). | | `autoGoLive` | Deprecated alias of `autoSeat` — same value, removed in a future release. Read `autoSeat`. | | `source` | The participant device source the feed is bound to — it appears in the studio's Sources immediately, offline until SRT flows. | ## 2. Publish over SRT Open an SRT publisher (`libsrt` on iOS/Android, or `ffmpeg` from a workstation) to `srtIngestUrl`. The credential is entirely inside the streamid: ``` publish::: ``` The whole capability is the scoped per-path **stream key** — user and password positions are both the key. There is **no JWT in the streamid**, which keeps it well under libsrt's 512-byte cap. Do not URL-encode the streamid; libsrt wants the literal colons. See [SRT: the streamid contract](/reference/srt-streamid) for the field grammar and the failure modes. `ffmpeg` sanity check from a laptop: ```bash ffmpeg -re -i camera.mp4 -c:v libx264 -c:a aac -f mpegts \ "srt://ingest.traxstreaming.live:8890?streamid=publish:inputs/mobile-contrib-mc-8f3c…::" ``` ## What happens on the studio side - The feed **auto-seats as a participant device source** under the contributing person, inheriting their name, avatar, and color. No host click is needed. - The **cloud encoder is the authority.** If the phone backgrounds or loses signal, the source goes offline and the program falls back to a slate; when the phone comes back it re-seats the same tile. The broadcast itself never depended on the phone. - **`autoSeat` seats the source, it does not start the broadcast.** Bringing a camera onto the canvas is separate from going live to your destinations. The studio's master go-live control stays authoritative — a contributing phone can never force the program on air. ## Reconnection and the device registry Each phone is a durable device, not a per-connection source. List a user's devices and their history: ``` GET /api/v1/contribute/devices Authorization: Bearer → 200 { "devices": [ { "deviceId": "9C1B…", "label": "EJ's iPhone", "lastSeenAt": "…", "connectionCount": 12, "…": "…" } ] } ``` Because the source id is derived from the studio plus the durable device, the same `deviceId` reconnecting into the same studio re-provisions the **same path** and re-seats the **same tile** — layout and position survive drops and app restarts. ## 3. Release the input when you're done When the phone is finished contributing, hand its input back to the studio: ``` DELETE /v1/studios/{studioId}/contribute Authorization: Bearer Content-Type: application/json { "deviceId": "9C1B…" } ``` → **204 No Content** This is the teardown twin of the provision call. It stops the cloud encoder from re-arming a pull against a path the phone is no longer publishing to. Call it from `applicationWillTerminate` (or your platform's equivalent) so a clean exit gives the session back instead of leaving it to time out. **Keep-source.** Release **deactivates** the input; it does not delete it. The source row, its media path, and its **canvas placement survive**, so a later provision with the **same `deviceId`** re-seats the **same tile** exactly like a reconnection does. Use `DELETE /v1/studios/{studioId}/sources/{sourceId}` when you actually want the input removed from the studio. **Safe from a background teardown.** The call answers **204 on everything that is merely "nothing to do"** — an unknown `deviceId`, one already released, one that never published, or an empty body. A shutting-down app has no screen to show an error on and no chance to retry, so a teardown never fails on request framing it cannot fix. The only answers you must not ignore are **403** (the `deviceId` belongs to another user), **401** (no auth), and **5xx** (the release genuinely did not happen). Unlike provisioning, release is **not** gated by the deployment's contribute switch: that flag stops new sessions being created, but a session that already exists must always be releasable. ## Walk-up guests (QR) A host can let someone contribute without an account. The host mints a short-lived join code (admin role required): ``` POST /api/v1/studios/{studioId}/contribute/qr Authorization: Bearer → 201 { "studioId": "…", "token": "…", "deepLink": "traxstreaming://contribute?studio=…&token=…", "joinUrl": "https://studio-api.traxstreaming.live/m/contribute?studio=…&token=…", "expiresAt": "2026-08-11T20:15:00Z" } ``` Render the `token` (or the deep link) as a QR code. The token is the only way in — there is no enumeration — and it expires in 15 minutes with a small use cap; re-mint to rotate. A guest scans it, enters a name, and their app provisions an ingest against the greenroom session: ``` POST /api/v1/greenroom/contribute/ingest Content-Type: application/json Cookie: { "deviceId": "…", "deviceLabel": "Guest — Sam" } ``` The response is the same SRT descriptor as the authenticated path, and the guest auto-seats as a participant device source. Guest ingests are rate-limited, and the guest is kickable and forgotten on leave. ## Control the studio while you publish Contribution is only the camera leg. The same app holds a **WebSocket** to the studio for tally ("you're live"), mute, flip-camera, and stop — bidirectional control that has nothing to do with the media plane. That surface, and how to authenticate it, is in [Build a client → Controlling a studio](/guides/build-a-client#controlling-a-studio-websocket). ## Where to go next 1. [Build a client](/guides/build-a-client) — the transport map, native OAuth + PKCE, and the studio-control WebSocket. 2. [Stream to a live input](/guides/stream-to-a-live-input) — the `/v1` path when the feed should be a plain input rather than a participant, driven by an API key instead of a user login. 2. [Drive the studio canvas](/guides/drive-the-canvas) — control layout, sources, presets, and scenes over the studio WebSocket while you publish. 3. [Monitor the program output](/guides/monitor-the-program) — watch the composited program feed over WHEP. 4. [SRT: the streamid contract](/reference/srt-streamid) — the streamid grammar, the 512-byte cap, and how publishes fail. 3. [Stream paths](/reference/path-grammar) — the path grammar shared by every media transport. 4. [Authentication & session tokens](/reference/authentication) — the user-login (OIDC + PKCE) and media-token models in full. # Go live from a phone over /v1 A phone is the hardest client this API has. It gets used in a car park, on a rooftop, in a basement, at a festival where forty thousand other people are also holding phones. It is also the client with the least room to be clever: it has one radio, a battery, and a user who is about to point it at something that only happens once. So this guide is short on purpose. **Two calls put a phone on air**, and both of them are built to survive a bad link: 1. **`POST /v1/studios/{id}/contribute`** — provisions this device's input on the studio and hands back the credential to publish into it, in one round trip. 2. **`POST /v1/studios/{id}/monitor`** — the return feed, so whoever is holding the camera can see what the show is actually sending. The first one is the one that matters. If it succeeds you can push video; if it fails there is no stream, and nothing else on this API changes that. > **Which host.** `https://api.traxstreaming.live/v1` with a production key, or > `https://api-dev.traxstreaming.live/v1` with a dev key. Only the host differs. ## Before you start - An API key holding **`sources:ingest`**, **`sources:write`** and **`viewer:tokens`**. Create it under **Developer → API keys**, or use the user's own OIDC bearer if your app signs people in — see [Build a client](/guides/build-a-client#native-app-authentication--oauth-20-with-pkce). Add **`sources:read`** and **`studios:read`** if you also want to poll (you do; see [Polling on cellular](#polling-on-cellular)). - The **studio id** the phone is joining. - A **device id** your app mints once. Read the next section before you pick one. ### Why contribute needs three scopes Each one is a different power, and none of them implies another: | Scope | What it lets this call do | |---|---| | `sources:write` | Create an input on the studio | | `sources:ingest` | Hand back a live publish credential for it | | `viewer:tokens` | Mint the return feed (monitor only) | `sources:write` reconfigures what a studio listens for. `sources:ingest` occupies the wire and puts pixels on the broadcaster's program. Those are different enough that holding one has never granted the other, and `POST /contribute` genuinely does both, so it asks for both. The monitor's scope surprises people, so here is the reasoning rather than just the rule. It is **`viewer:tokens`**, not `stream:read`. `stream:read` is a *default* scope — every key created without an explicit list carries it forever — and this endpoint hands back a credential that **plays the program**. That is not a power a key should acquire by not thinking about it. `viewer:tokens` is the scope this platform already uses for minting media playback credentials, it is opt-in by name, and a key holding it can already mint a `video.play` token for the same studio through `POST /v1/viewer-tokens`. Requiring it here therefore grants nothing your key could not already do — it just refuses to give the program feed away to a key that never asked for it. ## The device id is the whole design `deviceId` is a value **your app mints once and keeps in the Keychain (iOS) or Keystore (Android)**. It is opaque to TRaX and never resolved to a person: send an identifier for the *device*, not for whoever is holding it. It is also the **idempotency key**, and that is what makes everything else in this guide safe. The same `deviceId` always resolves to the same input on the same studio — the same media path, the same credential, the same canvas tile. That has two consequences worth internalising: - **A phone that reconnects lands back on its own tile.** The operator's layout survives a lift ride. - **A `deviceId` your app regenerates each launch produces a new input each launch.** The operator watches their source list fill up with duplicates of one phone, and nothing in the API will stop you: from the server's side, a new device id *is* a new device. Mint it once. Persist it. Never derive it from something that changes — not the session, not the login, not the network. ## Step 1 — put the phone on the studio ```bash curl -sS https://api.traxstreaming.live/v1/studios/$STUDIO/contribute \ -H "Authorization: Bearer $TRAX_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "deviceId": "5C1B9E77-...-KEYCHAIN", "deviceLabel": "EJ'\''s iPhone" }' ``` ```json HTTP/1.1 201 Created { "sourceId": "mc-9f21c4a7e0b3d6...", "streamName": "inputs/mobile-contrib-mc-9f21c4a7e0b3d6...", "deviceId": "5C1B9E77-...-KEYCHAIN", "autoSeat": true, "ingest": { "srt": { "complete": "srt://ingest.traxstreaming.live:8890?streamid=publish:inputs/mobile-contrib-mc-9f21...:sk_stream_...:sk_stream_...&latency=200", "base": "srt://ingest.traxstreaming.live:8890" } }, "granted": ["srt"], "streamKey": "sk_stream_...", "srtStreamId": "publish:inputs/mobile-contrib-mc-9f21...:sk_stream_...:sk_stream_...", "ingestHost": "ingest.traxstreaming.live", "ingestMetro": "auto" } ``` Dial `ingest.srt.complete` **verbatim**. Do not rebuild it from `base` + `streamKey`: the credential form is the server's to decide and it has changed before, and a client that reassembles the URL from a remembered shape keeps sending the old one long after the server moved on. Everything in that response except `sourceId`, `streamName`, `deviceId` and `ingestHost` is a live publish credential. Keychain, not log line, not analytics event, not a crash report. ### `201` the first time, `200` every time after Send the same `deviceId` again and you get `200` with the **same** `sourceId`, the **same** `streamKey`, the **same** URLs. Nothing is created and nothing is rotated. That is the contract this whole guide rests on. **Retrying is safe, including after a timeout where you never saw the response.** Nothing on this path rotates a credential, so a retry cannot invalidate the key the timed-out attempt already issued. `created` is deliberately not in the body — the status code already says it, and two answers to one question is how a client ends up trusting the wrong one. If you want to *deliberately* invalidate a credential — it got pasted into a support chat, the phone walked off — that is `POST /v1/studios/{id}/sources/{sourceId}/ingest/rotate`, by name, on purpose. See [Stream to a live input](/guides/stream-to-a-live-input#rotate-the-key). ### `autoSeat` `autoSeat: true` means 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 to expect when they hit publish: a tile that appears on air, or a source the operator still has to place. ### `ingestHost` and `ingestMetro` `ingestHost` is there so you can show *"publishing to ingest.traxstreaming.live"* without parsing — or displaying — a string that is also a secret. `ingestMetro` is `"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 guessing 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. ### Asking for RTMP Send `protocols` to get more than the SRT default: ```json { "deviceId": "...", "protocols": ["srt", "rtmp"] } ``` `protocols` is a **request, not a guarantee**. Read `granted` off the response and configure from that. Ask for exactly what you will dial — every extra entry is another copy of the same live secret sitting in your app's memory, logs and crash reports. SRT is the right default for a phone. It recovers from packet loss without retransmitting the whole world, which is what a cellular uplink does to you. ## Step 2 — the return feed ```bash curl -sS -X POST https://api.traxstreaming.live/v1/studios/$STUDIO/monitor \ -H "Authorization: Bearer $TRAX_API_KEY" ``` No body required. ```json HTTP/1.1 200 OK { "whep": { "url": "https://media.traxstreaming.live/s/5b2e9d41-.../pp/whep", "token": "eyJhbGciOiJFUzI1NiIs..." }, "srt": null, "streamName": "s/5b2e9d41-.../pp", "expiresAt": "2026-08-23T16:05:00Z" } ``` POST your SDP offer to `whep.url` with `Authorization: Bearer `. That token 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 — which is what makes it safe to put on a signaling request your API key has no business appearing on. **Take the WHEP path.** WebRTC degrades on a lossy cellular uplink far better than anything else here: it drops frames and keeps going where a stricter transport would stall. ### The expiry is not what you think The credential lasts 300 seconds by default (900 maximum, clamped rather than refused if you ask for more). That sounds impossibly short for a two-hour show. It is not, because **the media server authorises a read once, at session establishment, and never re-checks**. A monitor that is already playing keeps playing long past `expiresAt`. You need a fresh mint to **(re)connect**, not to keep watching. So: mint when you open the monitor, mint again if it drops, and schedule from `expiresAt` — never from the TTL you asked for, which may have been clamped. ### When `srt` is `null` `srt` is an alternative for clients that cannot speak WHEP, and `null` is a real answer rather than 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 that cap, or no SRT endpoint is configured, the server returns `null` instead of a URL that cannot dial. Handle it by falling back to WHEP, which is the path you should be on anyway. ### A black monitor is a state, not an error The credential is always issued when you are allowed to 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. Render that; do not report it as a failure. ## Building for a bad connection Everything above is designed around the assumption that your requests will sometimes not arrive. Here is what to do about it. ### Timeouts **10 seconds** per request. Long enough to survive a slow radio handover, short enough that a user staring at a spinner gets an answer rather than a hang. Both endpoints here are single round trips by design, so a 10-second budget is a real budget rather than an aspiration. ### Retries Retry `contribute` freely. It is idempotent, it cannot double-provision, and it cannot rotate the credential a previous attempt issued. Retry `monitor` freely too — a spare monitor token costs nothing and expires on its own. Back off exponentially with jitter: **1s, 2s, 4s, 8s**, capped, with a random ±30%. The jitter matters more than it looks. Phones at an event all lose signal at the same moment and all come back at the same moment; without jitter they retry in lockstep and turn one outage into two. ### Honour `Retry-After` Two responses carry it, and both mean what they say: - **429** — you are over the credential-mint budget (roughly 5 per minute per caller, with a small burst). Wait the interval; do not guess. - **503 `unavailable`** — the studio service is briefly unreachable. Wait the interval and retry; this is the transient one. One 503 does **not** carry `Retry-After`, on purpose: - **503 `contribute_disabled`** — the device-contribute path is switched off on this deployment. There is no interval after which this succeeds. Surface it to the user; do not retry it. Branch on `error.code`, never on the status alone. Two 503s that mean opposite things is exactly why the codes exist. ### Polling on cellular Poll **every 15–30 seconds**, not faster. The answer you are usually waiting for — did my input come up, is the show live — is push-driven on the operator's side; your poll is a fallback, and a fallback that runs every two seconds is just a battery drain. The three reads a phone polls support conditional requests: - `GET /v1/studios/{id}/sources/{sourceId}` — *is my input publishing yet* - `GET /v1/studios/{id}/sources` — *did my input show up* - `GET /v1/studios/{id}/stream-status` — *is the show on air* Each returns an `ETag`. Send it back as `If-None-Match` and an unchanged answer costs you a **304 with no body**: ```bash # First poll curl -sS -D- https://api.traxstreaming.live/v1/studios/$STUDIO/sources/$SOURCE \ -H "Authorization: Bearer $TRAX_API_KEY" # → 200 OK # ETag: "kZ3n8Qw2r1vB7yTcXsLmPQ" # Every poll after curl -sS -D- https://api.traxstreaming.live/v1/studios/$STUDIO/sources/$SOURCE \ -H "Authorization: Bearer $TRAX_API_KEY" \ -H 'If-None-Match: "kZ3n8Qw2r1vB7yTcXsLmPQ"' # → 304 Not Modified (no body) ``` Keep the last `ETag` per URL and send it every time. The tag changes the moment the answer does, so a 304 means *nothing has happened*, and a 200 means *something has* — which is precisely the signal you were polling for. On a metered cellular link this is the difference between a poll that completes and a poll that times out. ### What to do when you are offline Nothing clever. A contributed input is **durable**: it survives your app being killed, the phone rebooting, and the network going away entirely. When you come back, call `contribute` with the same `deviceId` and you get your input back with the same credential. Do not cache the credential and skip the call — call it and let the `200` confirm what you already had. It is one round trip and it is the only way to learn that an operator deleted your input while you were away. ## What this guide does not cover - **Going live.** Contributing a camera never starts a broadcast. That is `POST /v1/studios/{id}/go-live`, it needs `stream:golive`, and it is the operator's decision — see [Getting started](/guides/getting-started). - **The audio mixer.** Your input arrives with a channel strip the operator can reach; you can too, with `audio:read` / `audio:write` — see [Control the mixer](/guides/control-the-mixer). - **The first-party SRT flow on the studio control plane.** If you are building against the studio API rather than `/v1`, see [Contribute a camera from a phone](/guides/contribute-from-a-phone), which covers the participant model, the studio WebSocket and the QR walk-up flow. ## Reference - [`POST /v1/studios/{id}/contribute`](/reference/api/v1#contribute) - [`POST /v1/studios/{id}/monitor`](/reference/api/v1#mintprogrammonitor) - [Stream to a live input from your app](/guides/stream-to-a-live-input) — the same credential shape, for an input you created yourself - [Monitor the program output (WHEP)](/guides/monitor-the-program) — the WHEP playback details ## Stop polling: the events stream Everything above is a request you make. This is the one you don't. A phone that polls `listSources` and `stream-status` every four seconds and `destinations` every eight is spending about **37 requests a minute** on this API while it is also pushing video up the same cellular uplink. Those requests compete with your own SRT stream for the scarce direction — and they do it hardest exactly when the link is worst and you need the video most. Open this instead, once: ```bash curl -N -H "Authorization: Bearer $TRAX_API_KEY" \ https://api.traxstreaming.live/v1/studios/$STUDIO_ID/events ``` ``` : connected id: 9f2c1a4b7e3d-0 event: snapshot data: {"sources":[…],"streamStatus":{"live":false,…},"destinations":[…],"canvas":{…},"audio":{…},"chatConnectors":[…]} id: 9f2c1a4b7e3d-1 event: source data: {"id":"…","name":"Phone","publishing":true,"publishedAt":"2026-08-23T22:04:11Z",…} : heartbeat 2026-08-23T22:04:26Z ``` **The first event is a `snapshot` of everything.** Opening this stream IS your initial read — you do not need to GET anything to prime yourself. **Every `data:` is the same JSON the equivalent GET returns.** A `source` event is a `Source`, exactly as `listSources` gives you one. Apply an event by replacing the row it names; there is no partial-update form to learn. | `event` | what to do | | --- | --- | | `snapshot` | replace your whole model — sources, stream status, destinations, the canvas, the audio mixer, and every chat connector, all at once | | `source` | replace (or insert) that source — this is where `publishing` flips | | `streamStatus` | replace the stream status | | `destination` | replace (or insert) that destination | | `canvas` | replace your whole canvas — layout mode, active preset/scene, and every tile's placement (a `CanvasState`, the same shape `GET …/canvas` returns) | | `audio` | replace your whole mixer — the master strip plus one strip per source (a `StudioAudio`, the same shape `GET …/audio` returns) | | `chatConnector` | replace (or insert) that platform's chat connector — its reconnect-pill state (a `ChatConnector`) | | `sourceRemoved` / `destinationRemoved` | drop the row named by `data.id` | | `chatConnectorRemoved` | a platform was unlinked; drop the connector named by `data.platform` | A `destination` event fires no matter where the change came from — a toggle in the web studio, another phone, or a `/v1` call of your own. The same is true for every row here: this stream is the studio's state, not your client's echo. The `canvas` and `audio` events are whole-object replacements — reconcile each one by swapping your entire canvas or mixer for what it carries, exactly as you do for `snapshot`. ### Reconnecting Cellular drops. That is the normal case, not the exception, and the stream is built for it. Keep the `id` of the last event you processed. On reconnect, send it back: ```bash curl -N -H "Authorization: Bearer $TRAX_API_KEY" \ -H "Last-Event-ID: 9f2c1a4b7e3d-1" \ https://api.traxstreaming.live/v1/studios/$STUDIO_ID/events ``` The server replays what you missed. If it cannot honour the id — too old, or a different server — it sends a fresh `snapshot` instead and carries on. **That is not an error.** So always send your last id, never reason about whether it will work, and never fall back to re-GETting the three collections. A browser's `EventSource` sends this header for you; if you cannot set headers, use `?lastEventId=`. Treat the id as opaque. Do not parse it, do not compare two of them. ### Keepalives Lines beginning with `:` are SSE comments, not events. On a quiet studio you will see `: heartbeat