guides

Stream to a live input from your app

Your app holds an sk_live_ key and an encoder (a phone, a capture box, OBS on a laptop). This guide takes you from that key to video arriving in a studio: create an input, ask /v1 where to publish, hand the URL to the encoder.

Which host. Creating an input and reading its publish URLs are live in production: use https://api.traxstreaming.live/v1 with a production key, or https://api-dev.traxstreaming.live/v1 with a dev key. Only the host differs. Rotating a key is the exception — that endpoint is on dev only for now; this page will say so when it reaches production.

Two calls, in order:

  1. POST /v1/studios/{id}/sources with type: "generic-input" creates the input. It is a durable object in the studio; it exists whether or not anything is publishing to it.
  2. POST /v1/studios/{id}/sources/{sourceId}/ingest returns the publish URLs for that input. Nothing is created; you are reading a credential that already exists.

The thing to internalise: the input is the durable object and you point a device at it. Calling the second endpoint twice hands two devices the same endpoint. It never makes a second input.

Before you start

  • An API key holding sources:write (to create the input) and sources:ingest (to read its publish URLs). sources:ingest is its own scope because the response is a live credential: sources:write does not imply it, and no default read set includes it. Create the key under Developer → API keys in your account.
  • The studio id the input should live in.
  • A studio on an account with an active subscription. Without one, creating the input returns 402 subscription_required.

Step 1 — create the input

curl -sS https://api.traxstreaming.live/v1/studios/$STUDIO/sources \
  -H "Authorization: Bearer $TRAX_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "name": "Field camera 1", "type": "generic-input" }'
HTTP/1.1 201 Created
{
  "id": "8f3c2a10-6d1e-4b7a-9c0f-2e5d7a1b4c93",
  "studioId": "5b2e9d41-07c3-4f8a-b6e1-9a4d2c7f0e15",
  "name": "Field camera 1",
  "type": "generic-input",
  "active": false,
  "status": "OK"
}

Keep id. It is the {sourceId} for everything that follows, and it is stable for the life of the input. The source appears in the studio's Sources panel immediately, offline, waiting for media.

Step 2 — get the publish URLs

curl -sS https://api.traxstreaming.live/v1/studios/$STUDIO/sources/$SOURCE/ingest \
  -H "Authorization: Bearer $TRAX_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "protocols": ["srt", "rtmp"] }'

The body is optional. Send nothing and you get ["srt"]. Ask for exactly what your encoder will dial: every extra protocol is another copy of the same secret in your app's memory, logs and crash reports.

HTTP/1.1 200 OK
{
  "streamName": "inputs/generic-input-8f3c2a10-6d1e-4b7a-9c0f-2e5d7a1b4c93",
  "ingestUrls": {
    "srt": {
      "complete": "srt://east01.ingest.traxstreaming.live:8890?streamid=publish:inputs/generic-input-8f3c2a10-…:<KEY>:<KEY>",
      "base": "srt://east01.ingest.traxstreaming.live:8890"
    },
    "rtmp": {
      "complete": "rtmp://east01.ingest.traxstreaming.live:1935/inputs/generic-input-8f3c2a10-…?user=<KEY>&pass=<KEY>",
      "base": "rtmp://east01.ingest.traxstreaming.live:1935/inputs/generic-input-8f3c2a10-…"
    }
  },
  "granted": ["srt", "rtmp"],
  "streamKey": "<KEY>"
}

<KEY> is the input's stream key. It is the entire credential: the same value sits in both credential positions of the SRT streamid and in both RTMP query parameters. There is no token to refresh and nothing else to mint.

Field What it is
streamName The media path this input publishes to. The one field here that is safe to log; use it to correlate a device with what the studio shows.
ingestUrls.<proto>.complete The URL to hand the encoder, credential included. This is the secret. Keychain, not log line, not analytics event.
ingestUrls.<proto>.base The same endpoint with the credential removed. Safe to display. It does not authenticate on its own; never present it as something to publish to.
granted The protocols you actually got. Configure from this list, never from what you asked for.
streamKey The key on its own, for encoders whose UI has a separate key field. Always present — on a mint and on a rotate alike, and on a brand-new input as much as an established one. It adds no exposure beyond complete, which already contains it.

granted is the answer, not protocols

srt, rtmp and rtmps are the protocols that can be granted today. The request also accepts webrtc, rtsp and hls, and none of them is ever granted:

  • webrtc: the WHIP URL for a plain input carries no publish credential, so it would fail at the handshake. Browser and WebRTC publishing use a different mechanism; see WHIP / WHEP.
  • rtsp: pull-only. The URL is a hint for an IP camera the platform dials out to, not a place a device publishes.
  • hls: has no publish form.

Asking for one of those is not an error; it is simply absent from granted and from ingestUrls. A name outside that list of six is a 400, because a silently dropped typo leaves a client waiting for an endpoint that is never coming.

The URL shapes

Two forms, one credential. The SRT and RTMP reference pages have the full grammar.

srt://<host>:8890?streamid=publish:<streamName>:<KEY>:<KEY>
rtmp://<host>:1935/<streamName>?user=<KEY>&pass=<KEY>
rtmps://<host>:1936/<streamName>?user=<KEY>&pass=<KEY>

RTMP credentials go in the query string. The older rtmp://user:pass@host/… userinfo form is not read by the ingest server and fails authentication; if you have it in a config file from before, replace it. An SRT complete URL may also carry &latency=… and &oheadbw=… when the input has SRT tuning set in the studio; pass the URL through unchanged and those apply.

Step 3 — publish

Use the host the response gives you. Do not hardcode the ingest hostname; it is the platform's to change.

ffmpeg over SRT

ffmpeg -re -i camera.mp4 \
  -c:v libx264 -preset veryfast -b:v 4M -g 60 \
  -c:a aac -b:a 160k -f mpegts \
  "srt://east01.ingest.traxstreaming.live:8890?streamid=publish:inputs/generic-input-…:<KEY>:<KEY>"

Quote the URL. Do not percent-encode the streamid: the SRT library wants the literal colons, and an encoded one fails authentication.

OBS over SRT

  1. Settings → Stream → Service: Custom…
  2. Server: the SRT complete URL, whole.
  3. Stream Key: leave empty. Everything rides in the streamid.

libsrt on iOS / Android

Open a caller socket to base (host:port) and set SRTO_STREAMID to the streamid portion of complete, the part after streamid=. It is publish:<streamName>:<KEY>:<KEY> and must stay under libsrt's 512-byte cap, which it does by a wide margin: the key replaces what used to be a token.

ffmpeg over RTMP

ffmpeg -re -i camera.mp4 \
  -c:v libx264 -preset veryfast -b:v 4M -g 60 \
  -c:a aac -b:a 160k -f flv \
  "rtmp://east01.ingest.traxstreaming.live:1935/inputs/generic-input-…?user=<KEY>&pass=<KEY>"

OBS over RTMP / RTMPS

OBS publishes to <Server>/<Stream Key>, so split the complete URL at its last /. This is the same split the studio's own Connection Information panel shows:

  1. Settings → Stream → Service: Custom…
  2. Server: rtmp://east01.ingest.traxstreaming.live:1935/inputs
  3. Stream Key: generic-input-…?user=<KEY>&pass=<KEY>

For RTMPS, same split on the rtmps entry (port 1936). Plain RTMP sends the key in clear text on the wire; prefer RTMPS or SRT when the encoder supports them.

Encoder settings that work

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

Step 4 — confirm you are live

Your encoder says it connected. That is your side of the handshake. To hear it from ours, read the input back:

curl -s https://api-dev.traxstreaming.live/v1/studios/$STUDIO/sources/$SOURCE \
  -H "Authorization: Bearer $TRAX_API_KEY"
{
  "id": "8c1f…",
  "studioId": "a6a5…",
  "name": "Camera A",
  "type": "generic-input",
  "active": false,
  "status": "OK",
  "publishing": true,
  "publishedAt": "2026-08-22T18:14:07Z"
}

Read publishing, not status. status is the control-plane provisioning state — whether this input still has a backing ingest stream — and it says OK for a perfectly configured input nobody is publishing to. It has never meant "live". publishing is media-plane truth, the same signal the studio's own source LEDs burn, so the API and the operator's screen cannot disagree.

publishedAt is when this publish began, not when we last noticed it. It holds still while your encoder stays connected, so you can render "live for 4m" from it — and a new value means your publisher dropped and reconnected, which is worth surfacing to whoever is holding the camera.

Poll it about every 2 seconds while you are waiting, then stop. Liveness normally appears within a couple of seconds of the handshake, and it clears within roughly 15 seconds of the publisher going away — so a source that has genuinely stopped reads false after a short tail, not instantly.

publishing can be null, and null is a third answer. It means the liveness tracker could not be reached on that request: unknown, not offline. Keep polling. Treating null as "my stream is down" is exactly the wrong move during a blip, and it is why the field is nullable rather than a bare boolean.

active is still a different question again — it is whether the operator has put this input on the canvas. Publishing without being active is normal and correct: you are feeding an input the operator can cut to when they want it. See What you will see in the studio.

The same three fields ride on every GET /v1/studios/{id}/sources row, so a dashboard showing every input can read them in one request. The single-source read exists because a poll should not have to pull the whole studio.

What you will see in the studio

  • The input's tile in the Sources panel goes from offline to live within a few seconds of the first packets. When the encoder stops, it reads offline again within about five seconds.
  • Publishing does not put the studio on air. A live input is a source the operator can place on the canvas; the broadcast to destinations is a separate decision. The one exception is deliberate: if the studio's auto-go-live master switch is on and this specific input has been flagged as a trigger in the studio, the first packets start the broadcast. Neither flag is something your API key can set on the input; the operator does that in the studio.
  • One publisher per input. A second encoder connecting with the same key replaces the first on the same ingest node. Two field units sharing one input will fight; give each its own.

Stopping and reconnecting

Stopping is closing the connection. There is no API call to make; the input reads offline and keeps existing.

Reconnecting is dialling the same complete URL again. The credential does not expire and the input does not move, so a client that cached the response at provisioning time reconnects without another /v1 call. Re-fetch the ingest endpoints on a fresh install or when you have deliberately thrown the cached URL away, not on every reconnect: the endpoint is rate-limited at about 5 requests per minute per caller, separately from the rest of /v1, and a client fetching it in a loop is indistinguishable from a script harvesting publish credentials. A 429 carries a Retry-After header; honour it.

When the input is no longer needed, DELETE /v1/studios/{id}/sources/{sourceId} removes it, and the key with it.

When it does not work

What you see Almost always
Create returns 402 subscription_required The studio's account has no active subscription. Nothing to fix in your code.
Ingest returns 403 insufficient_scope naming sources:ingest Your key has sources:write but not sources:ingest. They are separate on purpose; mint a key that holds both.
Ingest returns 404 studio not found Wrong studio id, wrong source id, or the key's owner does not own that studio. Deliberately the same answer for all three.
Ingest returns 400 A protocol name outside srt, rtmp, rtmps, webrtc, rtsp, hls, or a body field the endpoint does not know. The message names it.
GET …/ingest returns 405 The endpoint is POST only, so the credential never lands in a URL line, a proxy log or a Referer.
429 with Retry-After You fetched the endpoints more than about 5 times in a minute. Cache the response; back off for the header's value.
503 write_disabled The deployment's write gate is off. Not something you can retry through.
SRT connects, then drops in under a second The streamid was percent-encoded, or edited. Pass complete through byte for byte.
RTMP is refused at connect Credentials in userinfo (rtmp://user:pass@…) instead of the query string, or a mangled key. The server closes the connection on a bad key.
Media flows but the studio stays off air Expected. See What you will see in the studio.

Rotate the key

A stream key that got out (pasted into a support chat, on a laptop that walked off, held by a contributor who no longer works with you) is replaced with one call:

curl -sS -X POST \
  "https://api.traxstreaming.live/v1/studios/$STUDIO/sources/$SOURCE/ingest/rotate" \
  -H "Authorization: Bearer $TRAX_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "protocols": ["srt"], "kickPublishers": true }'

Both body fields are optional; an empty body is valid. The response is 200, not 201 (nothing was created; the secret attached to the input changed), and it is byte-for-byte the shape POST …/ingest returns: streamName, ingestUrls, granted, streamKey. A client that can configure an encoder from a mint result needs no second code path for a rotate result.

There is no overlap window. The old URL and key stop authenticating the moment the call returns. That is deliberate: a grace period in which both keys work is exactly the interval in which the leaked one is still useful. Every device still configured with the old URL fails its next handshake until you re-point it. Rotate when you can re-provision, not in the sixty seconds before a show. Put another way: rotate and re-provision in the same maintenance step. A rotation with no plan for the devices holding the old URL is an outage you scheduled for yourself.

kickPublishers decides what happens to a session publishing right now, and it defaults to false. The media server authorises a publish once, at the handshake, and never re-checks, so an encoder 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: the in-flight session is cut. Leave it false for a planned hand-over: the broadcast in progress finishes on the old key while every new connection needs the new one.

Three smaller rules:

  • 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. The grant rules are the mint's: srt, rtmp and rtmps are grantable; webrtc, rtsp and hls are not; read granted off the response.
  • A typo does not cost you a key. An unrecognised protocol name is a 400 and nothing is rotated; the names are checked before the write.
  • Same scope, same budget, same refusals as the mint. sources:ingest (rotation is a credential mint, not a source edit), the separate ~5-per-minute bucket with Retry-After on 429, and the same 400 / 401 / 403 / 404 / 503 write_disabled answers as Step 2.

The studio sees it. Open studio tabs re-render the Connection Information panel with the new URLs, so an app rotating over /v1 does not leave the operator holding dead details. It works the other way too: an operator can rotate from the studio, which invalidates your app's cached URL. Treat a refused SRT or RTMP handshake on a URL that used to work as "re-fetch the ingest endpoints", not as "my API key is wrong".

Where to go next

  1. Developer API (/v1) — scopes, the endpoint table, and the OpenAPI document to generate a client from.
  2. SRT: the streamid contract and RTMP / RTMPS — the credential grammar in full and how each transport fails.
  3. Contribute a camera from a phone — the other ingest path, for a phone that should appear as a participant with a name and avatar rather than a plain input.
  4. Build a client — the transport map for a thick client that also controls the studio and watches the program.