The TUS resumable upload endpoint allows resumable and presigned uploads of video files. This enables end-users to upload directly to Bunny Stream and greatly improves reliability on poor networks and mobile connections.
The endpoint uses the open tus protocol for resumable file uploads. Before a video can be uploaded through the TUS endpoint, a video object must be created through the Create Video API call to obtain the video ID.
TUS endpoint
https://video.bunnycdn.com/tusupload
How it works
Create a video object using the Create Video API to get a videoId.
Generate a presigned signature on your server using SHA256.
Upload the file from the client using a TUS client library with the presigned credentials.
This approach allows secure direct uploads from end-users without exposing your API key.
Authentication
To authenticate a TUS upload request, the following headers must be included:
Header Description AuthorizationSignatureSHA256 signature for request validation AuthorizationExpireUNIX timestamp (in seconds) when the upload expires LibraryIdThe ID of the video library VideoIdThe GUID of the previously created video object
Video metadata parameters
The following metadata can be passed with the TUS upload:
Parameter Required Description filetypeYes The MIME type of the uploaded video (e.g., video/mp4) titleYes The title of the video collectionNo The GUID of the collection to upload to thumbnailTimeNo Time in milliseconds to extract the main video thumbnail
Generating the signature
The authorization signature is generated by hashing the concatenation of several values using SHA256:
SHA256(library_id + api_key + expiration_time + video_id)
The signature must be generated on your server to keep your API key secure.
Never expose your API key in client-side code.
import crypto from "crypto" ;
const libraryId = "your-library-id" ;
const apiKey = "your-api-key" ;
const videoId = "video-guid-from-create-call" ;
const expirationTime = Math . floor ( Date . now () / 1000 ) + 86400 ; // 24 hours from now
const signatureString = ` ${ libraryId }${ apiKey }${ expirationTime }${ videoId } ` ;
const signature = crypto
. createHash ( "sha256" )
. update ( signatureString )
. digest ( "hex" );
Examples
Once you have the presigned credentials from your server, use a TUS client library to upload the file. The official tus-js-client is recommended for browser uploads.
Install the TUS client:
npm install tus-js-client
Basic client-side upload
import * as tus from "tus-js-client" ;
// These values come from your server after creating the video
const { videoId , libraryId , expirationTime , signature } =
await fetchUploadCredentials ();
const file = document . getElementById ( "fileInput" ). files [ 0 ];
const upload = new tus . Upload ( file , {
endpoint: "https://video.bunnycdn.com/tusupload" ,
retryDelays: [ 0 , 3000 , 5000 , 10000 , 20000 , 60000 , 60000 ],
headers: {
AuthorizationSignature: signature ,
AuthorizationExpire: expirationTime ,
VideoId: videoId ,
LibraryId: libraryId ,
},
metadata: {
filetype: file . type ,
title: file . name ,
},
onError : function ( error ) {
console . error ( "Upload failed:" , error );
},
onProgress : function ( bytesUploaded , bytesTotal ) {
const percentage = (( bytesUploaded / bytesTotal ) * 100 ). toFixed ( 2 );
console . log ( `Upload progress: ${ percentage } %` );
},
onSuccess : function () {
console . log ( "Upload complete!" );
},
});
// Check for previous uploads to resume
upload . findPreviousUploads (). then ( function ( previousUploads ) {
if ( previousUploads . length ) {
upload . resumeFromPreviousUpload ( previousUploads [ 0 ]);
}
upload . start ();
});
Next.js App Router
This example shows a complete implementation using Next.js with the App Router.
app/api/upload/route.ts
components/VideoUploader.tsx
import { type NextRequest , NextResponse } from "next/server" ;
import { createHash } from "node:crypto" ;
interface CreateVideoResponse {
guid : string ;
title : string ;
libraryId : number ;
}
interface UploadCredentials {
videoId : string ;
libraryId : string ;
expirationTime : number ;
signature : string ;
embedUrl : string ;
}
export async function POST ( request : NextRequest ) {
const BUNNY_API_KEY = process . env . BUNNY_STREAM_API_KEY ;
const BUNNY_LIBRARY_ID = process . env . BUNNY_STREAM_LIBRARY_ID ;
if ( ! BUNNY_API_KEY || ! BUNNY_LIBRARY_ID ) {
return NextResponse . json (
{ error: "Bunny Stream not configured" },
{ status: 500 },
);
}
const { title } = ( await request . json ()) as { title ?: string };
// Step 1: Create a video object in Bunny Stream
const createResponse = await fetch (
`https://video.bunnycdn.com/library/ ${ BUNNY_LIBRARY_ID } /videos` ,
{
method: "POST" ,
headers: {
Accept: "application/json" ,
"Content-Type" : "application/json" ,
AccessKey: BUNNY_API_KEY ,
},
body: JSON . stringify ({
title: title ?? "Untitled Video" ,
}),
},
);
if ( ! createResponse . ok ) {
const error = await createResponse . text ();
console . error ( "Failed to create Bunny video:" , error );
return NextResponse . json (
{ error: "Failed to create video" },
{ status: 500 },
);
}
const video = ( await createResponse . json ()) as CreateVideoResponse ;
// Step 2: Generate TUS upload credentials
const expirationTime = Math . floor ( Date . now () / 1000 ) + 86400 ; // 24 hours
const signature = createHash ( "sha256" )
. update ( ` ${ BUNNY_LIBRARY_ID }${ BUNNY_API_KEY }${ expirationTime }${ video . guid } ` )
. digest ( "hex" );
return NextResponse . json ({
videoId: video . guid ,
libraryId: BUNNY_LIBRARY_ID ,
expirationTime ,
signature ,
embedUrl: `https://iframe.mediadelivery.net/embed/ ${ BUNNY_LIBRARY_ID } / ${ video . guid } ` ,
} satisfies UploadCredentials );
}
React with Express backend
Express server (server.js):
const express = require ( "express" );
const crypto = require ( "crypto" );
const cors = require ( "cors" );
const app = express ();
app . use ( cors ());
app . use ( express . json ());
const BUNNY_API_KEY = process . env . BUNNY_STREAM_API_KEY ;
const BUNNY_LIBRARY_ID = process . env . BUNNY_STREAM_LIBRARY_ID ;
app . post ( "/api/create-upload" , async ( req , res ) => {
const { title } = req . body ;
// Create video object
const response = await fetch (
`https://video.bunnycdn.com/library/ ${ BUNNY_LIBRARY_ID } /videos` ,
{
method: "POST" ,
headers: {
Accept: "application/json" ,
"Content-Type" : "application/json" ,
AccessKey: BUNNY_API_KEY ,
},
body: JSON . stringify ({ title }),
},
);
const video = await response . json ();
const videoId = video . guid ;
const expirationTime = Math . floor ( Date . now () / 1000 ) + 86400 ;
const signature = crypto
. createHash ( "sha256" )
. update ( ` ${ BUNNY_LIBRARY_ID }${ BUNNY_API_KEY }${ expirationTime }${ videoId } ` )
. digest ( "hex" );
res . json ({
videoId ,
libraryId: BUNNY_LIBRARY_ID ,
expirationTime ,
signature ,
});
});
app . listen ( 3001 );
Resuming uploads
One of the key benefits of TUS is the ability to resume interrupted uploads. The tus-js-client library handles this automatically:
upload . findPreviousUploads (). then ( function ( previousUploads ) {
// If there are previous uploads, resume from the first one
if ( previousUploads . length ) {
upload . resumeFromPreviousUpload ( previousUploads [ 0 ]);
}
upload . start ();
});
The client stores upload progress in the browser’s local storage by default. If an upload is interrupted (due to network issues, browser refresh, etc.), it can be resumed from where it left off.
Error handling
Implement proper error handling to provide a good user experience:
const upload = new tus . Upload ( file , {
// ... other options
onError : function ( error ) {
if ( error . originalRequest ) {
// Network or server error
console . error ( "Server error:" , error . message );
} else {
// Client-side error
console . error ( "Upload error:" , error . message );
}
},
onShouldRetry : function ( error , retryAttempt , options ) {
// Retry on network errors or 5xx server errors
const status = error . originalResponse ?. getStatus ();
if ( status >= 500 && status < 600 ) {
return true ;
}
return true ; // Retry by default
},
});