Skip to main content

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.

Upload metadata

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:
KeyRequiredValue
type"video" or "audio" — the Roughy asset type.
content_typeThe media MIME type (e.g. "video/mp4", "audio/wav"). Helps the server set the storage object’s Content-Type.
filenameOriginal 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