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:
- 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.
- 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.
embedHtmlis 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.tokenUrlis 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.
- Start at
minBackoffMs. - Double it after each failed attempt, and add random jitter.
- Cap it at
maxBackoffMs. - Never retry a
429sooner than itsRetry-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