Bunny Stream
"Bunny.net Stream: video hosting, HLS delivery, direct uploads, video collections, thumbnail generation, token authentication, webhook events"
Bunny.net Stream is a cost-effective video hosting and delivery platform built on Bunny.net's global CDN. It handles transcoding, adaptive bitrate streaming, and delivery through 100+ PoPs worldwide. The mental model is simple: you create a Video Library, upload videos to it, and Bunny handles everything else — transcoding into multiple resolutions, generating HLS manifests, creating thumbnails, and delivering via CDN. All interactions go through a REST API authenticated with a library-level API key. Use direct uploads via TUS protocol for client-side ingest, organize content with collections, and rely on webhook callbacks for processing status updates. ## Key Points - Use TUS protocol for client-side uploads to get resumable, chunked uploads out of the box. - Organize videos into collections for logical grouping and easier management. - Enable token authentication on your video library for any content that requires access control. - Store the video GUID in your database and construct CDN URLs dynamically using your hostname. - Use the webhook endpoint to track transcoding completion rather than polling the status API. - Set appropriate cache headers and leverage Bunny's CDN for thumbnail delivery. - Do not poll the video status endpoint in a loop; configure a webhook URL in your library settings. - Do not hardcode CDN hostnames; they can change if you reconfigure your library's CDN settings. - Do not upload large files through your server; use TUS direct uploads from the client. - Do not skip token authentication for paid or private content; unsigned URLs are publicly accessible. - Do not forget to handle status code 5 (error) in webhooks; transcoding can fail for corrupt or unsupported inputs. ## Quick Example ``` BUNNY_API_KEY=your-library-api-key BUNNY_LIBRARY_ID=your-library-id BUNNY_CDN_HOSTNAME=vz-abcdef-123.b-cdn.net ```
skilldb get video-services-skills/Bunny StreamFull skill: 256 linesBunny Stream
Core Philosophy
Bunny.net Stream is a cost-effective video hosting and delivery platform built on Bunny.net's global CDN. It handles transcoding, adaptive bitrate streaming, and delivery through 100+ PoPs worldwide. The mental model is simple: you create a Video Library, upload videos to it, and Bunny handles everything else — transcoding into multiple resolutions, generating HLS manifests, creating thumbnails, and delivering via CDN. All interactions go through a REST API authenticated with a library-level API key. Use direct uploads via TUS protocol for client-side ingest, organize content with collections, and rely on webhook callbacks for processing status updates.
Setup
Bunny Stream uses a straightforward REST API. There is no official SDK — use fetch or any HTTP client:
const BUNNY_API_KEY = process.env.BUNNY_API_KEY!;
const BUNNY_LIBRARY_ID = process.env.BUNNY_LIBRARY_ID!;
const bunnyApi = {
baseUrl: `https://video.bunnycdn.com/library/${BUNNY_LIBRARY_ID}`,
async request<T>(path: string, options: RequestInit = {}): Promise<T> {
const res = await fetch(`${this.baseUrl}${path}`, {
...options,
headers: {
AccessKey: BUNNY_API_KEY,
"Content-Type": "application/json",
...options.headers,
},
});
if (!res.ok) throw new Error(`Bunny API error: ${res.status}`);
return res.json();
},
};
Environment variables required:
BUNNY_API_KEY=your-library-api-key
BUNNY_LIBRARY_ID=your-library-id
BUNNY_CDN_HOSTNAME=vz-abcdef-123.b-cdn.net
Key Techniques
Creating and Uploading Videos
interface BunnyVideo {
guid: string;
title: string;
status: number; // 0=created, 1=uploaded, 2=processing, 3=transcoding, 4=finished, 5=error
thumbnailFileName: string;
}
async function createAndUploadVideo(title: string, fileBuffer: Buffer): Promise<BunnyVideo> {
// Step 1: Create the video object
const video = await bunnyApi.request<BunnyVideo>("/videos", {
method: "POST",
body: JSON.stringify({ title }),
});
// Step 2: Upload the file content
await fetch(
`https://video.bunnycdn.com/library/${BUNNY_LIBRARY_ID}/videos/${video.guid}`,
{
method: "PUT",
headers: { AccessKey: BUNNY_API_KEY },
body: fileBuffer,
}
);
return video;
}
TUS Direct Upload (Client-Side)
import * as tus from "tus-js-client";
// Server: create video and return upload credentials
async function createUploadSession(title: string) {
const video = await bunnyApi.request<BunnyVideo>("/videos", {
method: "POST",
body: JSON.stringify({ title }),
});
// Generate a SHA256 signature for TUS auth
const expiration = Math.floor(Date.now() / 1000) + 3600;
const signaturePayload = `${BUNNY_LIBRARY_ID}${BUNNY_API_KEY}${expiration}${video.guid}`;
const signature = await sha256Hex(signaturePayload);
return {
videoId: video.guid,
libraryId: BUNNY_LIBRARY_ID,
expiration,
signature,
};
}
// Client: upload using TUS protocol
function uploadWithTus(
file: File,
params: { videoId: string; libraryId: string; expiration: number; signature: string }
) {
const upload = new tus.Upload(file, {
endpoint: "https://video.bunnycdn.com/tusupload",
retryDelays: [0, 3000, 5000, 10000],
metadata: {
filetype: file.type,
title: file.name,
},
headers: {
AuthorizationSignature: params.signature,
AuthorizationExpire: String(params.expiration),
VideoId: params.videoId,
LibraryId: params.libraryId,
},
onProgress(bytesUploaded, bytesTotal) {
console.log(`${((bytesUploaded / bytesTotal) * 100).toFixed(1)}%`);
},
onSuccess() {
console.log("Upload complete");
},
});
upload.start();
}
Playback and Thumbnails
function getPlaybackUrls(videoId: string, cdnHostname: string) {
return {
hls: `https://${cdnHostname}/${videoId}/playlist.m3u8`,
thumbnail: `https://${cdnHostname}/${videoId}/thumbnail.jpg`,
preview: `https://${cdnHostname}/${videoId}/preview.webp`,
// Thumbnail at specific time (seconds)
thumbnailAt30s: `https://${cdnHostname}/${videoId}/thumbnail.jpg?v=30`,
};
}
// Embed with iframe
function getEmbedUrl(videoId: string, libraryId: string): string {
return `https://iframe.mediadelivery.net/embed/${libraryId}/${videoId}`;
}
Token-Authenticated Playback
import crypto from "crypto";
function generateSignedUrl(
videoId: string,
cdnHostname: string,
securityKey: string,
expiresInSeconds = 3600
): string {
const expiration = Math.floor(Date.now() / 1000) + expiresInSeconds;
const path = `/${videoId}/playlist.m3u8`;
const hashable = securityKey + path + expiration;
const token = crypto
.createHash("sha256")
.update(hashable)
.digest("hex")
.replace(/\+/g, "-")
.replace(/\//g, "_")
.replace(/=+$/, "");
return `https://${cdnHostname}${path}?token=${token}&expires=${expiration}`;
}
Managing Collections
interface BunnyCollection {
guid: string;
name: string;
videoCount: number;
}
async function createCollection(name: string): Promise<BunnyCollection> {
return bunnyApi.request<BunnyCollection>("/collections", {
method: "POST",
body: JSON.stringify({ name }),
});
}
async function addVideoToCollection(videoId: string, collectionId: string) {
return bunnyApi.request(`/videos/${videoId}`, {
method: "POST",
body: JSON.stringify({ collectionId }),
});
}
async function listCollectionVideos(collectionId: string): Promise<BunnyVideo[]> {
const result = await bunnyApi.request<{ items: BunnyVideo[] }>(
`/videos?collection=${collectionId}&page=1&itemsPerPage=100`
);
return result.items;
}
Webhook Handling
interface BunnyWebhookPayload {
VideoGuid: string;
VideoLibraryId: number;
Status: number; // matches BunnyVideo status codes
}
async function handleBunnyWebhook(req: Request): Promise<Response> {
const body: BunnyWebhookPayload = await req.json();
switch (body.Status) {
case 4: // Finished
await db.video.update({
where: { bunnyVideoId: body.VideoGuid },
data: { status: "ready" },
});
break;
case 5: // Error
await db.video.update({
where: { bunnyVideoId: body.VideoGuid },
data: { status: "error" },
});
break;
}
return new Response("OK", { status: 200 });
}
Best Practices
- Use TUS protocol for client-side uploads to get resumable, chunked uploads out of the box.
- Organize videos into collections for logical grouping and easier management.
- Enable token authentication on your video library for any content that requires access control.
- Store the video GUID in your database and construct CDN URLs dynamically using your hostname.
- Use the webhook endpoint to track transcoding completion rather than polling the status API.
- Set appropriate cache headers and leverage Bunny's CDN for thumbnail delivery.
Anti-Patterns
- Do not poll the video status endpoint in a loop; configure a webhook URL in your library settings.
- Do not hardcode CDN hostnames; they can change if you reconfigure your library's CDN settings.
- Do not upload large files through your server; use TUS direct uploads from the client.
- Do not skip token authentication for paid or private content; unsigned URLs are publicly accessible.
- Do not forget to handle status code 5 (error) in webhooks; transcoding can fail for corrupt or unsupported inputs.
Install this skill directly: skilldb add video-services-skills
Related Skills
Amazon IVS
"Amazon Interactive Video Service: low-latency live streaming, real-time stages, chat, stream recording, channel management, viewer analytics"
Api.video
"api.video: video hosting API, upload, live streaming, player customization, analytics, webhooks"
Cloudflare Stream
"Cloudflare Stream: video hosting, adaptive streaming, direct upload, watermarks, signed URLs, Workers integration"
LiveKit
"LiveKit: real-time video/audio, WebRTC, rooms, tracks, screen sharing, recording, React components"
Mux Video
"Mux: video hosting, HLS streaming, upload API, playback URLs, thumbnails, analytics, Mux Player, Next.js integration"
Vimeo OTT
"Vimeo OTT API: video-on-demand platform, subscription management, customer auth, content libraries, embed players, analytics, webhook events"