Integrating Video Uploads into Your Application

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:

  1. Create the video and get an upload ticket. POST /videos/uploads creates the video record and returns short-lived credentials for one upload slot.
  2. 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.
  3. Wait for processing. GET /videos/{id}/status reports 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
  }'
FieldRequiredNotes
filenameYesThe file name with its extension.
titleYesThe video title shown to viewers.
sizeNoFile size in bytes.
durationNoLength in seconds, if you know it.
publishedNoDefaults to true. Send false to keep the video private until you have added interactions.
transcribeNoDefaults to true. A transcript is created after processing.
group_id, folder_idNoPlace the video in a group or folder.
messageNoThe 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 id so 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
}
stateMeaning
waiting_for_uploadThe slot exists and no bytes have arrived.
uploadingBytes are arriving.
processingThe upload is complete and the video is being encoded.
readyThe video plays. duration is set and authoring_ready is true.
failedThe 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}/settings and published: true if you created the video unpublished.
  • Share link from the video object, or use embed_code for your own pages.

Errors

StatusCauseWhat to do
400filename or title is missing.Send both fields.
403File uploads are off for the account.Contact support, or create the video from a URL.
409The upload already started, or the video has media from a URL.Create a new video for a new file.
422The duration is over the plan limit.Shorten the file or upgrade the plan.
429The account's video quota is reached.Delete unused videos or raise the quota.
502The 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 upload block to the browser.
  • Show progress from the TUS client. onProgress reports bytes sent, which is what users want to see for a large file.
  • Save the video id before 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 after ready, then publish. Viewers never see a half-built video.
  • Handle failed by creating a new video. A failed encode clears the upload slot. Ask the user for the file again rather than retrying the same slot.

Reference