Skip to main content

Resumable uploads with a TUS client

A battle-tested TUS client (tus-js-client, tus-py-client) on the Videas API, for uploads that resume — including from the browser, with no API key exposed.

The SDK’s uploadFile is the easy path, but it doesn’t resume across calls (an interrupted upload restarts from the first chunk). When you need robust resume — flaky networks, multi-GB files, uploads that survive a page reload — drive the transfer with a proven TUS client instead. This is a short tutorial for both the browser and the server.

The one thing to understand: two auth levels

The Videas upload flow has two steps with different security:

Step Endpoint Needs the API key? Where
1. Create the session POST /upload/ (Write Assets) Yes — secret sk_ key Server only
2. Transfer the bytes PATCH/HEAD on the returned Location No — the Location is a secret capability URL Server or browser

So the Location you get back is an unguessable, one-shot upload URL: anyone holding it can send the bytes, no API key required. That’s what makes safe browser uploads possible — your server mints the session, the browser only ever sees the capability URL.

Never put your sk_ key in the browser. Instead:

1. Your server mints the session (holds the key) and returns the Location. See Upload with the REST API → step 1; with the SDK it’s videas.upload.createSession(...). Expose a tiny endpoint:

// server — POST /api/upload-session { filename, size }
app.post('/api/upload-session', async (req, res) => {
  const created = await videas.upload.createSession({
    headers: {
      'Tus-Resumable': '1.0.0',
      'Upload-Length': String(req.body.size),
      // key <base64(value)> pairs; filename + workspace_uid are required
      'Upload-Metadata': `filename ${btoa(req.body.filename)},workspace_uid ${btoa('ws_123')}`,
    },
  })
  res.json({ location: created.location }) // the capability URL — browser-safe
})

2. The browser transfers to that URL with tus-js-client — chunked, resumable, no key:

<script type="module">
  import * as tus from 'https://cdn.jsdelivr.net/npm/tus-js-client@4/+esm'

  const file = input.files[0]
  const { location } = await (await fetch('/api/upload-session', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ filename: file.name, size: file.size }),
  })).json()

  const upload = new tus.Upload(file, {
    uploadUrl: location,            // resume an already-created session
    chunkSize: 32 * 1024 * 1024,    // ≤ 50 MiB (server per-chunk limit)
    retryDelays: [0, 3000, 10000],  // automatic retry with back-off
    onProgress: (sent, total) => console.log(`${Math.round((sent / total) * 100)}%`),
    onSuccess: () => console.log('done'),
    onError: (err) => console.error(err),
  })
  // Resume if this file was partially uploaded before, else start.
  upload.findPreviousUploads().then((prev) => {
    if (prev.length) upload.resumeFromPreviousUpload(prev[0])
    upload.start()
  })
</script>

tus-js-client does a HEAD on the Location to learn the current offset, then PATCHes only the missing bytes — so a dropped connection (or a page reload) resumes instead of restarting.

Server uploads (Node)

On the server the key is safe, so you can let tus-js-client do both steps — point it at the create endpoint with your key and the required metadata:

import * as tus from 'tus-js-client'
import { createReadStream, statSync } from 'node:fs'

const path = 'movie.mp4'
const upload = new tus.Upload(createReadStream(path), {
  endpoint: 'https://api.videas.com/api/external/v1/upload/',
  headers: { Authorization: `Bearer ${process.env.VIDEAS_API_KEY}` },
  uploadSize: statSync(path).size,
  metadata: { filename: 'movie.mp4', workspace_uid: 'ws_123', content_type: 'video/mp4' },
  chunkSize: 32 * 1024 * 1024,
  retryDelays: [0, 3000, 10000],
  onSuccess: () => console.log('done'),
})
upload.start()

Server uploads (Python)

With tus-py-client (pip install tuspy):

import os
from tusclient import client

tus = client.TusClient(
    "https://api.videas.com/api/external/v1/upload/",
    headers={"Authorization": f"Bearer {os.environ['VIDEAS_API_KEY']}"},
)
uploader = tus.uploader(
    "movie.mp4",
    chunk_size=32 * 1024 * 1024,  # ≤ 50 MiB
    metadata={"filename": "movie.mp4", "workspace_uid": "ws_123"},
)
uploader.upload()  # resumable: re-run to continue an interrupted upload

After the upload

Same as any upload: you get an asset_uid, then the video is processed asynchronously — poll GET /assets/{uid}/ until status: ready. See How uploads work.

Which should I use?

  • Just works, small/medium files, server-side → the SDK’s uploadFile (Upload with the SDK).
  • Browser uploads → server mints the session, browser uses tus-js-client with uploadUrl (above).
  • Robust resume / very large files → a TUS client (tus-js-client, tuspy).

Next steps