guides

Set the program resolution and frame rate

Two calls: read what a studio is set to composite and encode at, and change it.

Which host. https://api.traxstreaming.live/v1, with a live key or a signed-in user's token. These endpoints are in production.

  1. GET /v1/studios/{id}/output — the format in three parts.
  2. PATCH /v1/studios/{id}/output — set the resolution and/or frame rate.

Read the GET first, and read all of it. The response deliberately does not collapse into one number, because one number would be wrong for most studios.

Three answers, not one

{
  "studioId": "…",
  "selected":  { "width": 2560, "height": 1440, "fpsNum": 60, "fpsDen": 1, "fps": 60 },
  "effective": { "width": 1920, "height": 1080, "fpsNum": 60, "fpsDen": 1, "fps": 60 },
  "caps": {
    "maxWidth": 1920,
    "maxHeight": 1080,
    "cappedBy": "twitch",
    "supported": [
      { "width": 1280, "height": 720,  "available": true,  "status": "available" },
      { "width": 1920, "height": 1080, "available": true,  "status": "available" },
      { "width": 2560, "height": 1440, "available": true,  "status": "available" },
      { "width": 3840, "height": 2160, "available": false, "status": "coming_soon" }
    ],
    "supportedFrameRates": [ { "num": 60000, "den": 1001, "fps": 59.94 }, "…" ]
  },
  "codec": "h264",
  "targetBitrateKbps": 6000,
  "appliesToNextGoLive": true
}

selected is what the operator picked. effective is what the next go-live will actually run. They differ here because Twitch ingests 1080p, and this studio has a Twitch destination enabled — so a 1440p selection airs at 1080p. caps.cappedBy names the platform responsible; it is an empty string when nothing is capping, which is the field to check rather than comparing maxWidth against the ladder top.

Show a user effective when answering "what am I streaming at". selected is their intent and can legitimately be higher than what airs today.

The cap is a fact about the studio's destination set right now, not a rule. Disable Twitch and the same selection airs at 1440p with no further write.

Frame rate is a rational

fpsNum / fpsDen, not a decimal. 59.94 is exactly 60000/1001, and rounding it to 59.94 or 60 produces drift the encoder cannot correct. The fps field is the decimal rendering for display; there is no way to write it, and sending one is a 400.

The accepted rates:

num / den reads as
24000 / 1001 23.976
24 / 1 24
25 / 1 25
30000 / 1001 29.97
30 / 1 30
50 / 1 50
60000 / 1001 59.94
60 / 1 60

Read them from caps.supportedFrameRates rather than hardcoding this table.

Setting it

curl -X PATCH https://api.traxstreaming.live/v1/studios/$STUDIO/output \
  -H "Authorization: Bearer $TRAX_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"width":1920,"height":1080,"fpsNum":60000,"fpsDen":1001}'

A PATCH: send only what you are changing. A resolution-only body leaves the frame rate exactly where it was, which is what lets a phone drop a studio to 720p without disturbing a 59.94 that someone chose deliberately.

The pairs are pairs. width and height move together and fpsNum and fpsDen move together; sending half of either is a 400 naming the field. Half a resolution is not a resolution, and a numerator with no denominator is not a frame rate.

The response is the whole re-resolved view — the same shape GET returns — so you never have to model the cap chain yourself to know what your change did.

It rejects; it does not clamp

An off-ladder resolution is a 400 that names the field and lists what is allowed:

{
  "error": {
    "code": "invalid_request",
    "message": "width: 1600x900 is not a supported program resolution; supported: 1280x720, 1920x1080, 2560x1440"
  }
}

This is the point of the endpoint. Offer caps.supported in your picker and you will never see this error; construct a resolution yourself and you will find out immediately rather than at the next go-live.

Coming soon resolutions

Each rung in caps.supported carries an available flag and a status string. A rung with "available": false ("status": "coming_soon") is part of the ladder but not offered yet — 4K (3840x2160) reads as coming soon today. Render it in your picker greyed out rather than hiding it, and do not let it be selected.

Selecting a coming-soon resolution is rejected the same way an off-ladder one is — a 400 that names the reason:

{
  "error": {
    "code": "invalid_request",
    "message": "width: 4K (3840x2160) is coming soon and not yet available"
  }
}

While 4K is coming soon it is excluded from the destination soft-cap too, so caps.maxWidth tops out at 2560x1440 even with only 4K-capable destinations (e.g. YouTube) enabled. When 4K ships, its rung flips to "available": true and the cap lifts with no change to your integration.

The one thing reported rather than enforced is the destination cap. Setting 1440p on a Twitch-only studio succeeds, and the response tells you it will air at 1080p. Refusing it would delete the operator's intent to punish a condition that lifts the moment they disable Twitch. (This is distinct from the coming-soon gate above, which IS enforced on write — a coming-soon resolution is a roadmap gap, not a per-destination condition.)

Codec and HDR

The output surface also describes the video codec and the color system (colorimetry). Read them the same way you read the resolution: from caps, never from assumptions.

Codec. codec on the response is what the next go-live encodes with. Today that is h264 for every studio, because no egress path can carry anything else to a destination — and caps.supportedCodecs says exactly that:

"caps": { "supportedCodecs": ["h264"] }

codec is writable in the PATCH body, validated against that same list. So today:

curl -X PATCH .../v1/studios/$STUDIO/output \
  -H "Authorization: Bearer $TRAX_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"codec":"h265"}'
{
  "error": {
    "code": "invalid_request",
    "message": "codec: h265 egress is not yet enabled — no egress path can carry HEVC to a destination today; supported: h264"
  }
}

That 400 is deliberate — it rejects rather than silently clamping to h264, for the same reason the resolution does. When HEVC egress ships, "h265" appears in caps.supportedCodecs and the identical PATCH starts succeeding, with no change to the request or response shape. Poll the caps, not a changelog. Writing "codec": "" clears the selection back to automatic (the resolution decides).

The response also carries selectedCodec — your persisted choice, "" when you never made one — separately from codec. They differ only if a codec you selected stops being deliverable; the platform then falls back to the automatic choice rather than sending dead video, and the split between the two fields is how you see that happened.

Colorimetry. selected.colorimetry, effective.colorimetry and caps.supportedColorimetry describe the color system:

"effective": {
  "width": 1920, "height": 1080, "fpsNum": 60, "fpsDen": 1, "fps": 60,
  "colorimetry": { "range": "sdr", "transfer": "bt709", "primaries": "bt709", "bitDepth": 8 }
},
"caps": {
  "supportedColorimetry": [
    { "range": "sdr", "transfer": "bt709", "primaries": "bt709", "bitDepth": 8 }
  ]
}

The colorimetry fields are read-only, and there is exactly one supported entry: SDR BT.709 at 8 bits. There is no colorimetry write path; when the pipeline can run more than one color system, caps.supportedColorimetry grows. As with codecs, the capability list is the announcement.

HDR intent. Separately from colorimetry, the PATCH body takes a boolean hdr: whether the next session should run an HDR canvas. It is validated against caps.supportsHdr: "hdr": true on a deployment where that is false is a 400, while an explicit "hdr": false is always accepted and stored. The response reports your stored choice as selectedHdr and the decision the next session will actually make as hdr. Writing hdr does not by itself turn on HDR where the deployment cannot run it.

Bitrate

The response carries targetBitrateKbps, the video bitrate the next session will target, and a bitratePolicy object that explains it. There is one program encode shared by every destination, not a separate encode per platform, so the target is the one value that satisfies every enabled destination at once.

bitratePolicy field What it is
mode auto (the platform picks) or manual (you set a number)
selectedKbps Your manual value. Present only in manual mode
targetKbps What the next session will target. 0 when valid is false
hard minKbps / maxKbps: the bounds the enabled destinations and the encoder actually allow
constraints One entry per destination platform, with its hard limits and recommended range
advisories Warnings that do not block the setting, each with a platform, code and message
valid / error Whether the saved policy can be honoured, and if not, why
appliesTo next_server_session

To set a manual bitrate, send both fields:

curl -X PATCH https://api.traxstreaming.live/v1/studios/$STUDIO/output \
  -H "Authorization: Bearer $TRAX_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"bitrateMode":"manual","bitrateKbps":8000}'

bitrateMode: "auto" clears the override and must be sent without bitrateKbps. Once a studio is in manual mode, a body carrying only bitrateKbps changes the number; the same body on a studio in auto mode is a 400. A value outside hard is a 400 that says so; it is never clamped.

targetBitrateKbps is a target, not a measurement of what is leaving the encoder, and it does not apply to a browser publishing its own camera.

Advanced encoder settings

Four more next-session settings ride the same PATCH. Each is optional, and omitting one keeps its current value:

Field Accepted values
keyframeIntervalSeconds 1, 2 or 4
audioBitrateKbps 128, 160, 192, 256 or 320 (a destination's limits can narrow this)
rateControl cbr or vbr (the hardware can report a downgrade)
encoderPreset quality, balanced or low_latency

advancedMode: "auto" clears all four at once and cannot be combined with any of them. To reset only some, name them in clearFields, e.g. {"clearFields":["rateControl"]}. Naming a field in clearFields and setting it in the same body, repeating a name, or naming an unknown field is a 400. The response reports the result as advancedPolicy, with the same valid / error / appliesTo shape as bitratePolicy.

For a studio that is live right now, currentAdmission reports what the running session was started with: its codec, format, bitrate target and HDR choice. It is absent when no session is known, and it never changes mid-session.

When it takes effect

appliesToNextGoLive is always true, and it is in the payload rather than only in this page because it is easy to get wrong. The encoder does not renegotiate resolution or frame rate mid-session: a studio that is live right now keeps the format it started with until it goes offline and live again.

Patching a live studio is safe — it persists and does not disturb the session on air — but it will not change what viewers are watching until the next go-live. If your UI can be open during a broadcast, say so there.

Scopes

studios:read to read, studios:write to change. Both are in the default scope set for a new key.

The format rides the studio scopes rather than having a pair of its own because changing it does not move what is on air — it takes effect at the next go-live, the same blast radius renaming a studio has. That is a different power from canvas:write, which puts a tile in front of every viewer the instant it lands.

  • Control the canvas from your app — the canvas read reports the same format as output, in the same shape. The canvas coordinate space is a fixed 1920x1080 for every studio and is not the broadcast resolution; scale your miniature by the canvas and label the stream by the output.