Skip to main content

Add a file with the API

Add a video or any file to your library with POST /v1/uploads: send it whole in one PUT or resumably over tus, from your server or straight from a browser, and follow it until it is ready.

You add a file to your organization’s library in two moves: open an upload with POST /v1/uploads, then send the bytes to the address it answers. The asset exists from the first move — you get its id at once — and becomes playable once the file has arrived and been processed.

Is the file already online, in your bucket or on your media server? Skip the second move: give us its link and we download it for you. Is it a video published on YouTube, Vimeo or Dailymotion? There is no file to add: POST /v1/assets creates an asset that plays it from its provider’s site.

Before you start

  • A key that may add files. In the Videas app, open Settings → API Access and create a key. Under What the key may do, tick Add files to the library through the API — it is unticked by default, so that a key that writes is always created on purpose. Tick Read the library through the API too: you need it to list workspaces, to follow the asset and to retry safely.
  • A workspace: the id of one of GET /v1/workspaces. The asset lands at its root.

Every call goes to https://api.videas.com/v1 with the key as a Bearer token:

Authorization: Bearer ak_…

Warning: the key stays on your server. /v1 sends no CORS headers, so a browser cannot call it anyway; a page that sends a file uses the upload’s own token instead — see From a browser.

Step 1 — open the upload

curl -X POST https://api.videas.com/v1/uploads \
  -H "Authorization: Bearer $VIDEAS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "workspace_id": "0192a6b1-7c3e-7d40-9a51-3f0e2b6c8d10",
    "filename": "lecture.mp4",
    "size_bytes": 734003200,
    "name": "Opening lecture",
    "external_ref": "course-42/lecture-1"
  }'
Field
workspace_id Required. Where the asset lands.
filename Required. The file’s name on your side, extension included — what the asset’s kind is first read from.
size_bytes Required. The file’s exact size in bytes, at most 10 GiB.
content_type Optional. Recorded, never trusted: the file is measured once it has arrived.
name, description, language Optional. The asset’s title (the file’s name when absent), its description, and its language as a BCP 47 tag (fr, en-GB). The title is at most 500 characters, the description 50,000.
external_ref Optional, and recommended: your own id for the asset, kept without the spaces around it and at most 500 characters. It is what makes a retry safe.

The answer is a 201:

{
  "id": "0192a6b2-4f1d-7e21-b3c8-6d2f0a9e1c57",
  "asset_id": "0192a6b2-4f1a-7b90-8e0d-51c3a7f2b046",
  "workspace_id": "0192a6b1-7c3e-7d40-9a51-3f0e2b6c8d10",
  "source": "client",
  "status": "pending",
  "size_bytes": 734003200,
  "received_bytes": 0,
  "upload_url": "https://api.videas.com/api/visitor/uploads/0192a6b2-4f19-7c02-a4e5-9b8d3c1f6e20",
  "max_put_bytes": 52428800,
  "expires_at": "2026-09-27T09:12:44Z",
  "source_url": null,
  "failure": null,
  "created_at": "2026-09-26T09:12:44Z",
  "updated_at": "2026-09-26T09:12:44Z",
  "finished_at": null,
  "upload_token": "…"
}
  • asset_id is the asset, which already exists, uploading.
  • id is the upload: keep it to follow it.
  • upload_url and upload_token are where and how to send the file. The token is answered once, here: no later read returns it. upload_url is opaque — use it exactly as given, never build one.

Step 2 — send the file

Every request to upload_url carries the token in X-Upload-Token — never your API key.

Up to max_put_bytes: one PUT

curl -T lecture.mp4 "$UPLOAD_URL" -H "X-Upload-Token: $UPLOAD_TOKEN"
# → 204 No Content
#   X-Checksum-Sha256: 5e8f…   (the SHA-256 of what was stored)

The body is the whole file, Content-Length its exact size — curl sets it — and any Content-Type will do. The upload completes in that request. If it breaks off, nothing is recorded: send the same PUT again. A 409 carrying Upload-Offset means the upload has already received bytes — when that offset equals the size, the file is already there.

Any size: tus, resumable

Above max_put_bytes, send the file to the same upload_url with tus 1.0 core: a HEAD says where to resume from, and each PATCH sends one chunk. Chunks are at most max_put_bytes, and at least 5 MiB except the last. A stock tus client does it all, given the address and the header — with tuspy, for instance:

from tusclient import client

tus = client.TusClient(upload["upload_url"], headers={"X-Upload-Token": upload["upload_token"]})
uploader = tus.uploader(file_path="lecture.mp4", url=upload["upload_url"], chunk_size=32 * 1024 * 1024)
uploader.upload()  # after an interruption, run it again: it resumes from the server's offset

The last chunk’s answer carries X-Checksum-Sha256 too.

From a browser

Your users can send their files straight from their browser to Videas, without the file passing through your server — and without your key leaving it. Your server opens the upload; the page only gets its address and its token.

1. Your server opens the upload and hands the page what it needs:

// server — POST /uploads { filename, size }
app.post('/uploads', async (req, res) => {
  const answer = await fetch('https://api.videas.com/v1/uploads', {
    method: 'POST',
    headers: {
      Authorization: `Bearer ${process.env.VIDEAS_API_KEY}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      workspace_id: process.env.VIDEAS_WORKSPACE_ID,
      filename: req.body.filename,
      size_bytes: req.body.size,
    }),
  })
  const upload = await answer.json()
  if (!answer.ok) return res.status(answer.status).json(upload)
  res.json({
    upload_url: upload.upload_url,
    upload_token: upload.upload_token,
    max_put_bytes: upload.max_put_bytes,
  })
})

2. The page sends the file — in one PUT when it is small enough, with tus-js-client otherwise:

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

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

  if (file.size <= upload.max_put_bytes) {
    const sent = await fetch(upload.upload_url, {
      method: 'PUT',
      headers: { 'X-Upload-Token': upload.upload_token },
      body: file,
    })
    if (sent.status !== 204) console.error('refused', sent.status)
  } else {
    new tus.Upload(file, {
      uploadUrl: upload.upload_url,   // the upload is open already: no creation request
      headers: { 'X-Upload-Token': upload.upload_token },
      chunkSize: 32 * 1024 * 1024,    // at most max_put_bytes, at least 5 MiB
      retryDelays: [0, 3000, 10000],
      onProgress: (sent, total) => console.log(`${Math.round((sent / total) * 100)}%`),
      onSuccess: () => console.log('stored'),
      onError: (error) => console.error(error),
    }).start()
  }
</script>

upload_url answers requests from any origin. The token admits whoever holds it to that one upload, and to nothing else, until the upload ends.

Follow it until it is ready

There is no notification yet: poll the upload.

curl https://api.videas.com/v1/uploads/$UPLOAD_ID \
  -H "Authorization: Bearer $VIDEAS_API_KEY"
status Meaning
pending Open, nothing received yet.
receiving Bytes are arriving — received_bytes says how many.
completed The file is stored. The asset goes on to processing, then ready.
cancelled Given up on: with DELETE /v1/uploads/{id}, or with the tus termination on upload_url.
expired The file did not complete before expires_at.

cancelled and expired are final, and the asset then ends error. Once the upload is completed, follow the asset itself with GET /v1/assets/{asset_id}: its status goes from uploading to processing, then ready — or error if the file cannot be used. Don’t play or embed it before ready.

To give an upload up, DELETE /v1/uploads/{id} answers 204; on a completed one it answers 409, the file being the asset’s.

Retrying safely

POST /v1/uploads is not idempotent: sent twice, it creates two assets. A request that timed out or answered a 5xx may nevertheless have created one. So:

  1. set external_ref on every upload;

  2. after a timeout or a 5xx, look it up before sending the request again — with a key that reads the library:

    curl "https://api.videas.com/v1/assets?external_ref=course-42%2Flecture-1" \
      -H "Authorization: Bearer $VIDEAS_API_KEY"
    # → { "data": [ … ], "next": null }
  3. nothing found: send it again. Found: the first request did create the asset — but its token was in the answer you lost, so its file cannot be sent. Open a new upload; the first asset ends error when its upload expires. An error asset with no file is such an abandoned attempt.

The requests that send the bytes need none of this: a PUT that failed recorded nothing, and tus resumes from where the server says.

Limits

Limit Value
File size 10 GiB
One PUT, one tus chunk max_put_bytes — 50 MiB
A tus chunk, except the last at least 5 MiB
Time to send the file until the upload’s expires_at

Errors

Every error has the same body — {"code": "…", "message": "…", "request_id": "…"}. Branch on code; message is for a person and may change.

Status code When opening an upload
400 validation Both size_bytes and source_url, or neither; no filename; a blank field; a name or an external_ref longer than 500 characters, or a description longer than 50,000; a malformed language. The message names the field.
401 unauthorized No key, or a key that is unknown, expired or revoked.
403 forbidden The key cannot add files to the library.
404 not_found No workspace of your organization has that id.
413 payload_too_large size_bytes is above 10 GiB.
429 rate_limited Too many requests: wait the seconds Retry-After gives.

Next steps