guides

Embed a player on your site

You can publish one of your sources for public playback and put a player for it on your own website. Your viewers don't sign in, don't need a TRaX account and never see a key. The player asks TRaX for a short-lived read token, then plays the source over WebRTC (WHEP).

There are two halves:

  1. You (the owner) turn public playback on for a source, once, with an API key or a signed-in user token. You get back a public playback id, a watch URL and an embed snippet.
  2. Each viewer's player exchanges that playback id for a two-minute read token. This call takes no authentication at all.

Step 1: turn on public playback

Requires the playback:write scope and edit access to the studio. Neither playback:write nor playback:read is in the default key scopes. Ask for them by name when you create the key.

curl -X POST "https://api.traxstreaming.live/v1/studios/$STUDIO/sources/$SOURCE/playback" \
  -H "Authorization: Bearer $TRAX_KEY" \
  -H "Content-Type: application/json" \
  -d '{"visibility":"unlisted"}'
{
  "playbackId": "Qm9vdHN0cmFwUGxheWJhY2s",
  "studioId": "…",
  "sourceId": "…",
  "visibility": "unlisted",
  "enabled": true,
  "createdAt": "2026-10-06T16:00:00Z",
  "updatedAt": "2026-10-06T16:00:00Z",
  "watchUrl": "https://watch.traxstreaming.live/p/Qm9vdHN0cmFwUGxheWJhY2s",
  "embedHtml": "<iframe src=\"https://watch.traxstreaming.live/p/Qm9vdHN0cmFwUGxheWJhY2s?embed=1\" width=\"640\" height=\"360\" frameborder=\"0\" allow=\"autoplay; fullscreen; picture-in-picture\" allowfullscreen></iframe>",
  "player": {
    "tokenUrl": "https://api.traxstreaming.live/v1/playback/Qm9vdHN0cmFwUGxheWJhY2s/token"
  }
}

The first call answers 201. The call is idempotent: calling it again for the same source returns the same playbackId with 200, so a retry never breaks an embed you already pasted. If you send a different visibility, it is updated in place.

  • embedHtml is an iframe pointing at the hosted watch page. Paste it into your page. The hosted watch page is being rolled out separately; until it is live, build your own player with Step 2.
  • player.tokenUrl is what a custom player calls in Step 2.

Read the current state with GET /v1/studios/{id}/sources/{sourceId}/playback (playback:read, which playback:write implies). It answers 404 when public playback is off for the source.

Visibility

Value Meaning
unlisted (default) Anyone with the playback id can watch. TRaX does not list or index the watch page.
public The same, and TRaX's own surfaces may list or index the watch page.

Token minting is identical for both. Neither keeps a published id secret. Once a playback id is in an embed on a public page, assume anyone can find it.

Turn it off

curl -X DELETE "https://api.traxstreaming.live/v1/studios/$STUDIO/sources/$SOURCE/playback" \
  -H "Authorization: Bearer $TRAX_KEY"

DELETE /v1/studios/{id}/sources/{sourceId}/playback answers 204 whether or not playback was on. From then on the token endpoint answers 404 for that playback id, so no new viewer can connect. Viewers already watching are not cut off: the media server checks the token once, when the session starts. A disable is complete within two minutes plus the length of the longest session already open.

Turning playback on again issues a new playback id. That is deliberate: switching playback off is how you revoke every embed already shared, and reusing the old id would quietly bring them all back.

Deleting the source also deletes its playback.

Step 2: get a token for a viewer (no authentication)

The player calls POST /v1/playback/{playbackId}/token with no Authorization header and no body:

const res = await fetch(
  'https://api.traxstreaming.live/v1/playback/Qm9vdHN0cmFwUGxheWJhY2s/token',
  { method: 'POST' },
)

The endpoint answers CORS with Access-Control-Allow-Origin: *, so a player on any website can call it straight from the browser. It is the only /v1 endpoint that does. It takes no credential, so there is nothing for a hostile page to borrow.

{
  "whep": {
    "url": "https://ingest.traxstreaming.live:…/inputs/…/whep",
    "token": "eyJhbGciOiJFUzI1NiIs…"
  },
  "iceServers": [{ "urls": ["stun:…"] }],
  "expiresAt": "2026-10-06T16:02:00Z",
  "retryPolicy": { "minBackoffMs": 1000, "maxBackoffMs": 60000 }
}

Play it over WHEP

iceServers lists STUN servers only. Anonymous playback never hands out TURN relay credentials, because anything in this response is visible to every viewer of your page. Most networks connect over STUN; a viewer behind a strict symmetric NAT may not.

POST your SDP offer to whep.url and send the token in the Authorization header:

const pc = new RTCPeerConnection({ iceServers: body.iceServers })
pc.addTransceiver('video', { direction: 'recvonly' })
pc.addTransceiver('audio', { direction: 'recvonly' })
pc.ontrack = (e) => { video.srcObject = e.streams[0] }

await pc.setLocalDescription(await pc.createOffer())
const answer = await fetch(body.whep.url, {
  method: 'POST',
  headers: {
    'Content-Type': 'application/sdp',
    Authorization: `Bearer ${body.whep.token}`,
  },
  body: pc.localDescription.sdp,
})
await pc.setRemoteDescription({ type: 'answer', sdp: await answer.text() })

Never put the token in the URL. Don't use a query parameter, even where a player library offers one. URLs end up in server logs, browser history and Referer headers.

The token lives two minutes

The token expires two minutes after it is minted (expiresAt), and the lifetime is fixed. That is shorter than it sounds:

  • A session that is already playing keeps playing past expiresAt. The media server checks the token only when the session starts.
  • Mint a fresh token every time the player connects or reconnects. Don't cache tokens and don't share one between viewers. A reconnect after a network drop is a new connection and needs a new token.

The token reads exactly one source. It is not your API key and it carries no identity of yours.

Errors, retries and limits

Answer Meaning What the player does
200 A token. Play.
404 not_found The playback id is unknown, or its owner turned playback off. You get the same answer for both on purpose. Do not retry. Show "this stream isn't available". The answer won't change until the owner turns playback on again, and that issues a different id.
429 rate_limited Too many token requests. Wait at least Retry-After seconds, then retry with backoff.
503 Temporarily unavailable. Retry with backoff (honour Retry-After when present).

Every 429 carries a Retry-After header in seconds. It is never less than 1, and it is computed from the rate limiter's own refill time. The same number is in the body as error.retryAfterSeconds, for players that read the body more easily than headers:

// HTTP 429, header: Retry-After: 6
{
  "error": {
    "code": "rate_limited",
    "message": "too many playback token requests from this address; retry after the Retry-After interval",
    "requestId": "req_…",
    "retryAfterSeconds": 6
  }
}

Use the server's retry policy

Every successful token response includes retryPolicy. Use it for reconnects and for retrying a 429 or 503. Never use it for a 404.

  1. Start at minBackoffMs.
  2. Double it after each failed attempt, and add random jitter.
  3. Cap it at maxBackoffMs.
  4. Never retry a 429 sooner than its Retry-After.

TRaX sets these numbers on the server and may change them, for example to slow a very large audience reconnecting at once, without you shipping a new player. Read them from every response instead of hard-coding them. Defaults today are 1000 and 60000 ms.

Limits

The token endpoint has its own rate limits, separate from the per-key limits on the rest of /v1 (see Rate limits). A request must fit within both:

  • Per viewer address: 20 requests in a burst, refilling at 10 per minute.
  • Per playback id: 200 requests in a burst, refilling at 50 per second, shared by everyone watching that playback.

These are defaults and may change. Drive your player off Retry-After and retryPolicy rather than these numbers.

Reference

  • POST /v1/studios/{id}/sources/{sourceId}/playback: turn public playback on (playback:write)
  • GET /v1/studios/{id}/sources/{sourceId}/playback: read it (playback:read)
  • DELETE /v1/studios/{id}/sources/{sourceId}/playback: turn it off (playback:write)
  • POST /v1/playback/{playbackId}/token: a viewer's read token (no authentication)
  • WHIP / WHEP: the play protocol