Let users upload video files from your own app straight into Mindstamp, with resumable transfers and no separate hosting.
Let your users upload video files from your own application straight into Mindstamp. The file travels directly to Mindstamp's video platform, transfers resume after an interruption, and you never need to host the file anywhere else first.
This guide is for developers who already create videos through the REST API and want to add file upload. If you are new to the API, read Getting Started first.
How it works
An upload takes three calls and one file transfer:
- Create the video and get an upload ticket.
POST /videos/uploadscreates the video record and returns short-lived credentials for one upload slot. - Send the file with TUS. Your application uploads the bytes to the endpoint in the ticket using any TUS 1.0 client. TUS is an open protocol for resumable uploads, so large files and dropped connections are handled for you.
- Wait for processing.
GET /videos/{id}/statusreports when the video is encoded and ready.
After that, the video behaves like any other Mindstamp video. Add interactions, share the link, or embed it.
The API token never leaves your server. The upload ticket is scoped to one video, expires after 60 minutes, and is safe to hand to a browser.
Before you start
- A Mindstamp plan with API access (Pro and higher) and an API token.
- A TUS client. The examples use tus-js-client, which runs in browsers and in Node.js. Clients exist for Python, Go, Java, .NET, iOS, and Android.
- A video file. MP4 with H.264 video and AAC audio gives the fastest processing. Your plan's video length limit applies.
Step 1: Create the video and get a ticket
Call POST /videos/uploads from your server with the file name and a title.
curl -X POST "https://api.mindstamp.com/api/v1/videos/uploads" \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"filename": "quarterly-webinar.mp4",
"title": "Quarterly Webinar",
"size": 734003200,
"published": false
}'
| Field | Required | Notes |
|---|---|---|
filename | Yes | The file name with its extension. |
title | Yes | The video title shown to viewers. |
size | No | File size in bytes. |
duration | No | Length in seconds, if you know it. |
published | No | Defaults to true. Send false to keep the video private until you have added interactions. |
transcribe | No | Defaults to true. A transcript is created after processing. |
group_id, folder_id | No | Place the video in a group or folder. |
message | No | The video message. |
The response is the video object plus an upload block:
{
"id": "f62c16e9-fa42-484c-acb9-4d4b449a74b1",
"token": "sAHUZiyZMUMa",
"title": "Quarterly Webinar",
"link": "https://share.mindstamp.com/w/sAHUZiyZMUMa",
"status": "waiting_for_upload",
"upload": {
"protocol": "tus",
"tus_version": "1.0.0",
"endpoint": "https://video.bunnycdn.com/tusupload",
"expires_at": "2026-10-20T20:46:25Z",
"headers": {
"AuthorizationSignature": "5f3c…",
"AuthorizationExpire": "1792536385",
"LibraryId": "128514",
"VideoId": "d64ca9c1-f0d1-48e9-b259-f99e5bc58954"
}
}
}
Store the id and token. Pass the upload block to whatever will send the file.
One ticket per file
Each call creates a new video. Call it once per file, and keep the returned
idso a retry in your application does not create a duplicate.
Step 2: Upload the file with TUS
Every TUS request must carry the four headers from the ticket. Two metadata fields are also required: filetype and title.
In the browser
This example takes a File from an <input type="file"> and the upload block from Step 1.
import * as tus from "tus-js-client";
function uploadToMindstamp(file, ticket, { onProgress, onSuccess, onError }) {
const upload = new tus.Upload(file, {
endpoint: ticket.endpoint,
headers: ticket.headers,
metadata: {
filetype: file.type,
title: file.name,
},
chunkSize: 50 * 1024 * 1024,
retryDelays: [0, 1000, 3000, 5000, 10000],
onProgress: (sent, total) => onProgress(Math.round((sent / total) * 100)),
onSuccess,
onError,
});
upload.start();
return upload; // call upload.abort() to cancel
}
From a server
The same client works in Node.js with a file stream. Use this when the file is already on your servers.
import fs from "node:fs";
import * as tus from "tus-js-client";
const path = "./quarterly-webinar.mp4";
const { size } = fs.statSync(path);
const upload = new tus.Upload(fs.createReadStream(path), {
endpoint: ticket.endpoint,
headers: ticket.headers,
uploadSize: size,
metadata: { filetype: "video/mp4", title: "quarterly-webinar.mp4" },
chunkSize: 50 * 1024 * 1024,
retryDelays: [0, 1000, 3000, 5000, 10000],
onSuccess: () => console.log("Upload complete"),
onError: (error) => console.error(error),
});
upload.start();
With curl
Useful for a quick test. TUS creates the upload first, then sends the bytes.
# 1. Create the upload. The Location header is the upload URL.
curl -i -X POST "https://video.bunnycdn.com/tusupload" \
-H "Tus-Resumable: 1.0.0" \
-H "Upload-Length: $(stat -f%z quarterly-webinar.mp4)" \
-H "Upload-Metadata: filetype $(printf video/mp4 | base64),title $(printf quarterly-webinar.mp4 | base64)" \
-H "AuthorizationSignature: …" -H "AuthorizationExpire: …" \
-H "LibraryId: …" -H "VideoId: …"
# 2. Send the bytes. Location is a path; prefix it with https://video.bunnycdn.com
curl -X PATCH "https://video.bunnycdn.com/tusupload/UPLOAD_ID" \
-H "Tus-Resumable: 1.0.0" \
-H "Upload-Offset: 0" \
-H "Content-Type: application/offset+octet-stream" \
-H "AuthorizationSignature: …" -H "AuthorizationExpire: …" \
-H "LibraryId: …" -H "VideoId: …" \
--data-binary @quarterly-webinar.mp4
Long transfers
A ticket is valid for 60 minutes. If a transfer might run longer, or a user comes back the next day to finish, request a new ticket for the same video:
curl -X POST "https://api.mindstamp.com/api/v1/videos/{id}/uploads" \
-H "Authorization: Bearer YOUR_API_TOKEN"
The new ticket points at the same upload slot, so the TUS client resumes from the last byte it sent. Once bytes have arrived and processing has begun, this call returns 409 Conflict.
Step 3: Wait for processing
Poll GET /videos/{id}/status until state is ready. A few seconds between polls is enough. Encoding usually finishes within a few minutes of the upload completing, depending on file length.
curl "https://api.mindstamp.com/api/v1/videos/{id}/status" \
-H "Authorization: Bearer YOUR_API_TOKEN"
{
"id": "f62c16e9-fa42-484c-acb9-4d4b449a74b1",
"token": "sAHUZiyZMUMa",
"published": false,
"source_type": "upload",
"state": "ready",
"duration": 1843.2,
"authoring_ready": true,
"transcript_state": null
}
state | Meaning |
|---|---|
waiting_for_upload | The slot exists and no bytes have arrived. |
uploading | Bytes are arriving. |
processing | The upload is complete and the video is being encoded. |
ready | The video plays. duration is set and authoring_ready is true. |
failed | The file could not be encoded. Create a new video and upload again. |
Wait for authoring_ready before you add interactions that depend on the timeline, such as questions at a given second.
Step 4: Build the interactive video
With the video ready, use the endpoints you already know:
- Add questions, buttons, and other interactions with the Interactions endpoints.
- Apply branding with the video design endpoint.
- Publish with
PATCH /videos/{id}/settingsandpublished: trueif you created the video unpublished. - Share
linkfrom the video object, or useembed_codefor your own pages.
Errors
| Status | Cause | What to do |
|---|---|---|
400 | filename or title is missing. | Send both fields. |
403 | File uploads are off for the account. | Contact support, or create the video from a URL. |
409 | The upload already started, or the video has media from a URL. | Create a new video for a new file. |
422 | The duration is over the plan limit. | Shorten the file or upgrade the plan. |
429 | The account's video quota is reached. | Delete unused videos or raise the quota. |
502 | The video platform did not create an upload slot. No video was created. | Retry in a minute. |
Every error body contains error, message, code, and a resolution hint.
Good practices
- Keep the API token on your server. Create the ticket server-side and pass only the
uploadblock to the browser. - Show progress from the TUS client.
onProgressreports bytes sent, which is what users want to see for a large file. - Save the video
idbefore the upload starts. If the page reloads, your application can request a new ticket and resume instead of starting over. - Create the video unpublished and publish when it is done. Send
published: false, add your interactions afterready, then publish. Viewers never see a half-built video. - Handle
failedby creating a new video. A failed encode clears the upload slot. Ask the user for the file again rather than retrying the same slot.