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
idof one ofGET /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.
/v1sends 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_idis the asset, which already exists,uploading.idis the upload: keep it to follow it.upload_urlandupload_tokenare where and how to send the file. The token is answered once, here: no later read returns it.upload_urlis 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:
-
set
external_refon every upload; -
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 } -
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
errorwhen its upload expires. Anerrorasset 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
- Add a file from a link — when the file is already online.
- Add a video hosted elsewhere — YouTube, Vimeo or Dailymotion.
- Errors, limits and versions — every refusal, and what to retry.
- The API reference, generated from the API’s contract.