Use the media library from your app
Your app has a file — a sting, a lower-third still, a music bed, a pre-recorded segment — and it needs to end up on air. This guide is the whole path: get the bytes into TRaX, watch them become usable, put them on a studio's canvas, and take them away again.
Which host. These endpoints are on dev only for now:
https://api-dev.traxstreaming.live/v1with a dev key. This page will say so when they reach production.
The shape to hold in your head is three steps, not one:
POST /v1/media— tell us what is coming. You get back an asset id and a plan for pushing the bytes.- You upload the bytes — straight to storage, not through this API.
POST /v1/media/{assetId}/complete— tell us they landed.
Step 3 is the one that gets skipped, and skipping it is the most expensive
mistake on this page. An upload that stops after step 2 has produced nothing:
the asset sits in UPLOADING forever, it counts against your user's storage,
and it can be attached to nothing. If you take one thing from this guide, take
that.
Before you start
- An API key holding
media:readto browse andmedia:writeto upload or delete.media:writeincludes read. Create one under Developer → API keys in your account. Neither scope is in the default set — tick them when you create the key.media:readhands out a signed download URL for the owner's files, so a key created without choosing scopes does not carry it. - To put a file on a studio, you also need
sources:write.
Set them up once:
export TRAX_API_KEY=sk_live_...
export TRAX_API=https://api-dev.traxstreaming.live/v1
Uploading a small file
curl -s -X POST $TRAX_API/media \
-H "Authorization: Bearer $TRAX_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "filename": "sting.mp4", "contentType": "video/mp4", "byteSize": 4210688 }'
{
"assetId": "0f8fad5b-d9cb-469f-a165-70867728950e",
"mode": "single",
"uploadUrl": "https://s3.../video/0f8fad5b...?X-Amz-Signature=...",
"objectKey": "video/0f8fad5b-d9cb-469f-a165-70867728950e",
"kind": "video"
}
byteSize must be the real size. It is checked against the remaining storage
allowance before a URL is issued, which is why an upload that cannot fit is
refused here rather than after you have spent a user's data plan on it.
PUT the file at uploadUrl, then complete:
curl -s -X PUT "$UPLOAD_URL" \
-H "Content-Type: video/mp4" \
--data-binary @sting.mp4
curl -s -X POST $TRAX_API/media/$ASSET_ID/complete \
-H "Authorization: Bearer $TRAX_API_KEY"
No body on /complete for a single-file upload. The response is the asset.
uploadUrl is a credential, not an address: anyone holding it can write to
that object until it expires. Keychain, not log file, not crash report.
Uploading a big file
Read mode before anything else. Over a threshold, POST /v1/media returns
no uploadUrl at all — it returns a plan:
{
"assetId": "1c2d...",
"mode": "multipart",
"partBytes": 67108864,
"parts": [
{ "partNumber": 1, "offset": 0, "byteSize": 67108864, "url": "https://s3.../?partNumber=1&..." },
{ "partNumber": 2, "offset": 67108864, "byteSize": 67108864, "url": "https://s3.../?partNumber=2&..." },
{ "partNumber": 3, "offset": 134217728, "byteSize": 22020096, "url": "https://s3.../?partNumber=3&..." }
],
"kind": "video"
}
Why it exists: every request to our storage host crosses a CDN edge that caps a
single request body at 100 MB, and it enforces that at the edge. A 150 MB
PUT does not slow down — it dies with a 413 after about two megabytes, from a
server that is not ours. Parts are sized to stay under that cap, so what limits
a file is your user's storage allowance, not the network.
Then:
- PUT each part's slice at its own URL. Slice by the plan's
offsetandbyteSize— never by a part size of your own. The server may have grown the part size to keep the plan under the storage layer's part-count ceiling, so the plan is the only correct description of how the file is cut. - Upload parts in any order, a few at a time. Three in flight is a good number; more mostly buys congestion. Give each part two or three retries — a failed part is a failed part, not a failed upload.
- Keep the
ETagresponse header each PUT returns. Send them back on/complete.
curl -s -X POST $TRAX_API/media/$ASSET_ID/complete \
-H "Authorization: Bearer $TRAX_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "parts": [
{ "partNumber": 1, "etag": "\"d41d8cd98f00b204e9800998ecf8427e\"" },
{ "partNumber": 2, "etag": "\"a1b2c3d4e5f60718293a4b5c6d7e8f90\"" },
{ "partNumber": 3, "etag": "\"0f1e2d3c4b5a69788796a5b4c3d2e1f0\"" }
] }'
Order does not matter — the server sorts, because parts finish out of order when several are in flight. Completeness does. A partial list is an error, not a partial completion.
If you cannot read the ETag header, which is the normal case in a browser unless the storage host exposes it through CORS, send no body at all. The server recovers every ETag from storage itself. That fallback is why a missing ETag is an inconvenience rather than a dead end.
/complete is idempotent. If the response never arrives, call it again —
never re-upload.
Watching it become usable
curl -s $TRAX_API/media/$ASSET_ID -H "Authorization: Bearer $TRAX_API_KEY"
{
"assetId": "0f8fad5b-d9cb-469f-a165-70867728950e",
"kind": "video",
"filename": "sting.mp4",
"status": "READY",
"sizeBytes": 4210688,
"durationSeconds": 8.4,
"width": 1920,
"height": 1080,
"thumbnailUrl": "https://s3.../thumb/...",
"downloadUrl": "https://s3.../video/...",
"createdAt": "2026-08-23T00:12:00Z",
"updatedAt": "2026-08-23T00:12:09Z"
}
Poll this until status reads READY. A large file is assembled and probed
after /complete returns, so READY can lag it by a moment. Every second or two
is plenty, and then stop.
sizeBytes is what actually landed in storage, not what you declared. The
duration and dimensions come from probing the file; treat them as picker
metadata rather than a guarantee, and expect 0 for a still image or a file we
could not read.
thumbnailUrl and downloadUrl expire. Fetch them when you render and
throw them away. A URL you saved in your database works all through testing and
403s a day later — this is the single most common way this API gets used wrong.
thumbnailUrl is also best-effort even on a READY asset: audio files and most
images do not have one, which is a kind icon on your side, not a failure.
Browsing the library
curl -s "$TRAX_API/media?limit=50" -H "Authorization: Bearer $TRAX_API_KEY"
{
"data": [
{ "id": "0f8fad5b...", "filename": "sting.mp4", "kind": "video", "byteSize": 4210688,
"status": "READY", "durationSeconds": 8.4, "width": 1920, "height": 1080,
"thumbnailUrl": "https://s3.../thumb/..." }
],
"nextCursor": "MGY4ZmFkNWI",
"quotaUsedBytes": 418447360,
"quotaLimitBytes": 2147483648
}
The library belongs to the user, not to a studio, so nothing here names one. Rows carry a thumbnail so you can draw a grid, and deliberately carry no download URL — reading a file is a different thing from knowing it exists, and list responses are what end up in logs. Read one asset when you need the file.
quotaUsedBytes / quotaLimitBytes ride along so you can show usage and refuse
an over-large pick on the device, instead of discovering the ceiling when an
upload comes back 402.
Putting it on a studio
Create a source and pass the asset id. Do not pass downloadUrl.
curl -s -X POST $TRAX_API/studios/$STUDIO/sources \
-H "Authorization: Bearer $TRAX_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "name": "Opening sting", "type": "video-file", "mediaAssetId": "'$ASSET_ID'" }'
Use the type that matches the file: video-file, image, audio-file, or the
generic media.
The reason to pass an id rather than a link: the URL your phone can fetch and
the URL the encoder can fetch are different signatures of the same object,
and both expire. Pass the id and the server resolves the one the encoder needs
and re-mints it whenever it goes stale. An asset-backed source keeps working
next month; a source built from a pasted downloadUrl stops working tomorrow.
url and mediaAssetId are mutually exclusive. Sending both is a 400 rather
than a precedence rule you would have to remember.
Cancelling and deleting
Two different operations, and they are not interchangeable.
Cancel an upload in flight — the user backed out, or it failed past retrying:
curl -s -X POST $TRAX_API/media/$ASSET_ID/abort \
-H "Authorization: Bearer $TRAX_API_KEY"
Do this. An abandoned upload does not show up in GET /v1/media — that lists
finished assets — and it still occupies the storage allowance, so an app that
abandons uploads without aborting them slowly locks its own user out of
uploading anything at all. There is a server-side sweeper, but it works in
hours, not minutes.
/abort is idempotent, and it refuses a finished asset with 409. That is
deliberate: a mistimed cancel must not become data loss.
Delete finished content:
curl -s -X DELETE $TRAX_API/media/$ASSET_ID \
-H "Authorization: Bearer $TRAX_API_KEY"
Permanent — the file, its thumbnail, and the storage it held.
It is refused with 409 while any studio source still uses the asset, and
the message says how many. Take that seriously: a source pointing at a deleted
file shows a permanent "unavailable" card, and if that studio is live, the
encoder's next read on the file fails and the input drops to a placeholder in
the middle of the show. Delete the sources first.
What we accept
| Kind | Content types |
|---|---|
| video | video/mp4, video/webm, video/quicktime |
| image | image/jpeg, image/png, image/webp, image/gif |
| audio | audio/mpeg, audio/wav, audio/ogg, audio/aac |
contentType decides the asset's kind, which decides how it behaves on a
canvas — a looping video, a still, an audio-only bed. Send the wrong one and
you get an asset that misbehaves in a way nothing later will explain.
When it does not work
| Response | What happened |
|---|---|
400 invalid_request |
An unsupported contentType, a byteSize of zero or less, a partial part list, or a mediaAssetId that is not a READY asset of yours. The message says which |
402 storage_quota_exceeded |
The library is full, or this file is larger than any single upload this account may hold. Nothing is wrong with the request — free space or change the plan. The message names which ceiling |
403 insufficient_scope |
The key lacks media:read (to browse) or media:write (to upload or delete). The requiredScope field names it |
404 not_found |
No such asset, or it belongs to someone else. The two are deliberately indistinguishable |
409 conflict |
You aborted an upload that already finished, or deleted an asset a studio source still uses |
503 unavailable |
Writes are disabled on this deployment, or the service is briefly unreachable. Retry |
Where to go next
- Drive the canvas — arrange the source you just added.
- Control the canvas over /v1 — put a library asset on the canvas, then move it.
- Control the mixer from your app — for an audio bed, the level is the next thing you want.
- The /v1 reference — every endpoint and scope.