Documentation Index
Fetch the complete documentation index at: https://docs.roughy.ai/llms.txt
Use this file to discover all available pages before exploring further.
Roughy speaks the TUS 1.0.0 wire protocol for
uploads. Your client streams the file in chunks; Roughy proxies
them to durable storage with the resumability and recovery
semantics built into TUS. You should not upload via raw PUT —
TUS handles every part of “what if the connection dies at 80%”
automatically when you use a TUS-aware client.
The TUS endpoint is:
https://api.roughy.ai/api/v1/assets/upload-resumable
Tus-Max-Size is 50 GiB per upload. Per-chunk cap is 64 MiB —
TUS clients respect this automatically.
Authentication
Every TUS verb requires your Roughy API key as a Bearer token,
exactly like the rest of the API:
Authorization: Bearer rk_live_…
The same key authorizes the initial POST and every subsequent
PATCH/HEAD/DELETE. TUS clients let you set headers once;
they’re attached to every request automatically.
The Upload-Metadata header carries the asset shape, encoded per
the TUS spec (<key> <base64-of-utf8-value> pairs separated by
commas). Two keys matter:
| Key | Required | Value |
|---|
type | ✅ | "video" or "audio" — the Roughy asset type. |
content_type | | The media MIME type (e.g. "video/mp4", "audio/wav"). Helps the server set the storage object’s Content-Type. |
filename | | Original filename, for your audit trail. Optional. |
TUS client libraries take a plain object/dict and do the
base64-encoding for you.
JavaScript / Browser
The most-installed TUS client is
tus-js-client. 5 KB
gzipped, handles chunking and resume natively. Install:
npm install tus-js-client
Drop-in for a typical <input type="file"> upload:
import * as tus from "tus-js-client"
const upload = new tus.Upload(file, {
endpoint: "https://api.roughy.ai/api/v1/assets/upload-resumable",
headers: {
Authorization: `Bearer ${ROUGHY_API_KEY}`,
},
metadata: {
type: "video",
content_type: file.type,
filename: file.name,
},
chunkSize: 50 * 1024 * 1024, // 50 MiB; under the 64 MiB cap
retryDelays: [0, 3000, 5000, 10000, 20000],
onProgress: (sent, total) => {
console.log(`${((sent / total) * 100).toFixed(1)}%`)
},
onSuccess: () => {
// Asset id is in the JSON body of the initial POST.
// tus-js-client exposes it as upload.url, but the cleanest
// way is to read the JSON response from your own server's
// logic — or extract from the Location URL path.
const assetId = upload.url?.split("/").pop()
console.log("uploaded session:", assetId)
},
onError: (err) => {
console.error("upload failed:", err)
},
})
upload.start()
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).
from tusclient import client
uploader = client.TusClient(
"https://api.roughy.ai/api/v1/assets/upload-resumable",
headers={"Authorization": f"Bearer {ROUGHY_API_KEY}"},
).uploader(
"/path/to/video.mp4",
chunk_size=50 * 1024 * 1024, # 50 MiB
metadata={
"type": "video",
"content_type": "video/mp4",
"filename": "episode-42.mp4",
},
)
uploader.upload() # blocks; resumes automatically on retry
After upload() returns, the asset is READY. Read
uploader.url to recover the session URL — the path segment
after /upload-resumable/ is the upload session id; the asset id
itself is on the JSON body of the initial POST (the client
exposes it as uploader._url then a separate fetch, or you can
parse it from the first response — see the
tus-py-client docs).
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 a PATCH with the running offset.
TOKEN="rk_live_…"
API="https://api.roughy.ai"
FILE="/path/to/video.mp4"
CHUNK_SIZE=$((64 * 1024 * 1024)) # 64 MiB
SIZE=$(stat -f%z "$FILE") # macOS; on Linux: stat -c%s "$FILE"
b64() { printf '%s' "$1" | base64 | tr -d '\n'; }
# 1. Initiate the session
INIT=$(curl -sf -X POST "$API/api/v1/assets/upload-resumable" \
-H "Authorization: Bearer $TOKEN" \
-H "Tus-Resumable: 1.0.0" \
-H "Upload-Length: $SIZE" \
-H "Upload-Metadata: type $(b64 'video'),content_type $(b64 'video/mp4'),filename $(b64 "$(basename "$FILE")")")
SESSION_ID=$(echo "$INIT" | jq -r '.session_id')
ASSET_ID=$(echo "$INIT" | jq -r '.asset.id')
LOCATION="$API/api/v1/assets/upload-resumable/$SESSION_ID"
# 2. PATCH chunks
OFFSET=0
while [ $OFFSET -lt $SIZE ]; do
REMAINING=$((SIZE - OFFSET))
THIS=$((REMAINING < CHUNK_SIZE ? REMAINING : CHUNK_SIZE))
tail -c +$((OFFSET + 1)) "$FILE" | head -c "$THIS" | \
curl -sf -X PATCH "$LOCATION" \
-H "Authorization: Bearer $TOKEN" \
-H "Tus-Resumable: 1.0.0" \
-H "Upload-Offset: $OFFSET" \
-H "Content-Type: application/offset+octet-stream" \
-H "Content-Length: $THIS" \
--data-binary @-
OFFSET=$((OFFSET + THIS))
done
echo "Uploaded $OFFSET / $SIZE bytes; asset_id=$ASSET_ID"
To resume after an aborted run, ask the server for the current
offset with HEAD and continue from there:
LOCATION="https://api.roughy.ai/api/v1/assets/upload-resumable/<session_id>"
OFFSET=$(curl -sf -I -X HEAD "$LOCATION" \
-H "Authorization: Bearer $TOKEN" \
-H "Tus-Resumable: 1.0.0" \
| awk -F': ' 'tolower($1) == "upload-offset" { gsub(/\r/, "", $2); print $2 }')
echo "Resume from offset $OFFSET"
# → resume the PATCH loop with $OFFSET as the starting point
Cancelling an in-flight upload
Sending DELETE to the session URL cancels the upload and
soft-deletes the underlying asset row:
curl -X DELETE "$LOCATION" \
-H "Authorization: Bearer $TOKEN" \
-H "Tus-Resumable: 1.0.0"
Idempotent — a DELETE of a session that’s already gone is also
a 204.
Verifying the upload
After the final chunk, the asset is in state READY:
curl "$API/api/v1/assets/$ASSET_ID" \
-H "Authorization: Bearer $TOKEN"
{
"id": "019deafa-…",
"type": "video",
"state": "ready",
"size_bytes": 14956800000,
"content_type": "video/mp4",
"duration_seconds": "3782.413",
"download_url": "https://…/sign/assets/uploads/…?token=…"
}
duration_seconds is filled by an asynchronous ffprobe pass after
the upload completes; expect it to appear within a few seconds.
What’s next