- Create the asset with a plain REST
POST /v1/assets— the response body is the new asset (inpending_upload), and the TUS transfer URL comes back in theLocationheader. - Stream the bytes to that URL using the TUS 1.0.0 wire protocol — chunked, resumable, connection-drop-safe.
POST /v1/projects and reuse its id for every asset in that workflow.
Tus-Max-Size is 50 GiB per upload. There is no per-chunk cap; the server streams every chunk straight through to durable storage, so any chunk size your TUS client uses is accepted (typical clients default to 5–50 MiB). The minimum upload size is 64 bytes; the server validates the file format from the head of the first PATCH and rejects anything smaller.
Asset type detection runs on the first chunk, not at finalize: if the bytes don’t match a supported media format (audio or video), the upload is rejected after that first PATCH with 409 Conflict, and the upstream resumable session is cancelled. A bogus upload therefore pays at most a single chunk’s bandwidth — not the full Upload-Length. The first PATCH must carry at least 64 bytes for the format check to run; TUS clients with default chunk sizes (MB-class) sail through this without configuration.
Authentication
Every request requires your Roughy API key as a Bearer token:POST and every subsequent PATCH/HEAD/DELETE. TUS clients let you set headers once; they’re attached to every request automatically.
Step 1 — Create the asset
size_bytes is required — pass the exact byte count of the file you’re about to upload. language is optional and immutable once set: it hints the spoken language of the media. (The cut’s reference script is not an asset field — you supply it per POST /v1/cuts; see Cut.)
The response is 201 Created and the body is the asset (pending_upload); the byte-derived fields (type, content_type, extension, size_bytes) stay null until the first chunk identifies the media:
Location value directly to your TUS client as the upload URL — no further URL construction needed. (Lost it? See Recovering a lost upload.)
Step 2 — Stream chunks via TUS
Point your TUS client at theLocation URL (not at the create endpoint). The TUS server implements TUS 1.0.0 Transfer + Termination + Expiration extensions:
OPTIONS /v1/uploads— capability discovery (no auth, no body). Response advertisesTus-Resumable: 1.0.0,Tus-Version: 1.0.0,Tus-Extension: termination,expiration,Tus-Max-Size: <50 GiB>.HEAD /v1/uploads/{upload_id}— returns the canonicalUpload-Offsetfor resume. Always carriesCache-Control: no-store.PATCH /v1/uploads/{upload_id}— uploads one chunk. RequiresContent-Type: application/offset+octet-stream,Upload-Offsetmatching the server’s current offset (mismatch →409 Conflict; HEAD to recover), andTus-Resumable: 1.0.0. Response is204 No Contentwith the newUpload-Offset.DELETE /v1/uploads/{upload_id}— Termination extension. Cancels the upload and removes the asset row. Idempotent — repeat calls also return204.
POST /v1/assets — the transfer endpoint only streams chunks, so there’s no TUS creation request to make here.
The Upload-Expires response header uses RFC 9110 HTTP-date format (e.g. Wed, 25 Jun 2014 16:00:00 GMT) per the TUS spec. Each successful PATCH refreshes the expiry by 48 h; an upload left idle past that window is reaped.
Upload metadata
Asset type (audio vs video), MIME type, and file extension are detected from the uploaded bytes on the first chunk; you don’t declare them. The only create-time fields are project_id, size_bytes, and the optional language.
JavaScript / Browser
The most-installed TUS client istus-js-client. 5 KB
gzipped, handles chunking and resume natively. Install:
<input type="file"> upload:
tus-js-client automatically PATCHes chunks, advances on
Upload-Offset from the server response, and resumes from the
last acknowledged offset on connection drop.
Python
tus-py-client is the
official Python client (pip install tuspy). It doesn’t natively support
pre-allocated upload URLs, so the cleanest approach is to create the asset
via httpx / requests, then hand the Location URL to the
uploader for the PATCH loop:
Raw cURL (debug / one-off)
When you don’t have a TUS client handy — debug sessions, CI checks, shell scripts — the protocol is small enough to drive by hand. Each chunk is aPATCH with the running offset.
HEAD and continue from there:
Recovering a lost upload
If you didn’t keep theLocation URL from step 1, you don’t have to start
over — re-fetch the asset and read its upload_url. While the asset is
pending_upload, its body carries the same TUS transfer URL (the asset_id
is in the POST /v1/assets response body):
HEAD the upload_url for the canonical offset, then resume the PATCH
loop. upload_url is null once the upload finalises. If you lost the
asset id too, GET /v1/assets?state=pending_upload lists every asset still
awaiting its bytes.
Cancelling an in-flight upload
SendingDELETE to the upload URL cancels the upload and
deletes the underlying asset:
DELETE of an upload that’s already gone is also
a 204.
Verifying the upload
After the final chunk the asset moves toprocessing while Roughy
prepares it. Poll GET /v1/assets/{id} until state is ready:
duration_seconds is measured while the asset is processing, so it’s
present by the time the asset is ready. Reaching ready settles the
per-minute charge — then the asset is ready to cut.
What’s next
- Cut — turn the uploaded asset into a kept-segment plan.
- Asset concept — types, states, and lifecycle details.
- TUS protocol details: tus.io and the implementations index for clients in other languages (Go, Rust, Ruby, PHP, iOS, Android).