Api.video
"api.video: video hosting API, upload, live streaming, player customization, analytics, webhooks"
api.video provides a developer-centric video infrastructure API covering both on-demand and live streaming. The platform handles encoding, storage, and CDN delivery through a clean REST API with official SDKs. The mental model is straightforward: create a video object, upload content to it, and get an embeddable player and streaming URLs. For live streaming, create a live stream object and receive an RTMP ingest URL. api.video differentiates with its player customization system (themes, branding, captions) and a progressive upload feature for large files. Design your integration around video object IDs, use delegated upload tokens for client-side uploads, and rely on webhooks for processing status. The API follows REST conventions consistently, making it predictable to work with.
## Key Points
- Use delegated upload tokens for client-side uploads; never expose your API key in browser code.
- Enable `mp4Support` when you need a downloadable version of the video alongside HLS streaming.
- Use progressive upload for files over 200 MB to get resumable, chunked uploads.
- Create player themes once and assign them to videos rather than customizing per video.
- Store the `videoId` in your database as the primary reference; derive URLs from it.
- Use the `record: true` option on live streams to automatically create VOD assets after broadcast.
- Set meaningful `tags` and `metadata` on videos to enable filtering and search in your application.
- Handle the `video.encoding.quality.completed` webhook to know when each quality tier is ready.
- Do not upload files directly with your API key from the browser; use delegated tokens instead.
- Do not upload entire large files in a single request; use progressive upload for reliability.
- Do not poll for encoding status; use webhooks to be notified when encoding completes.
- Do not create a new player theme for every video; reuse themes to maintain consistent branding.
## Quick Example
```typescript
import ApiVideoClient from "@api.video/nodejs-client";
const apiVideo = new ApiVideoClient({
apiKey: process.env.API_VIDEO_API_KEY!,
});
```
```typescript
// Server-side: "@api.video/nodejs-client": "^2.0.0"
// Client-side: "@api.video/video-uploader": "^1.0.0"
```skilldb get video-services-skills/Api.videoFull skill: 381 linesapi.video
Core Philosophy
api.video provides a developer-centric video infrastructure API covering both on-demand and live streaming. The platform handles encoding, storage, and CDN delivery through a clean REST API with official SDKs. The mental model is straightforward: create a video object, upload content to it, and get an embeddable player and streaming URLs. For live streaming, create a live stream object and receive an RTMP ingest URL. api.video differentiates with its player customization system (themes, branding, captions) and a progressive upload feature for large files. Design your integration around video object IDs, use delegated upload tokens for client-side uploads, and rely on webhooks for processing status. The API follows REST conventions consistently, making it predictable to work with.
Setup
Install the TypeScript client and configure authentication:
import ApiVideoClient from "@api.video/nodejs-client";
const apiVideo = new ApiVideoClient({
apiKey: process.env.API_VIDEO_API_KEY!,
});
For client-side uploads, install the uploader:
// Server-side: "@api.video/nodejs-client": "^2.0.0"
// Client-side: "@api.video/video-uploader": "^1.0.0"
Environment variables required:
API_VIDEO_API_KEY=your-api-key
API_VIDEO_WEBHOOK_SECRET=your-webhook-verification-secret
Key Techniques
Creating and Uploading Videos
async function createAndUploadVideo(
title: string,
filePath: string,
tags: string[] = []
) {
// Step 1: Create the video object
const video = await apiVideo.videos.create({
title,
description: `Uploaded on ${new Date().toISOString()}`,
tags,
metadata: [
{ key: "uploadedBy", value: "api-service" },
],
mp4Support: true, // Enable mp4 download URL
public: true,
});
// Step 2: Upload the file
const uploadedVideo = await apiVideo.videos.upload(video.videoId, filePath);
return {
videoId: uploadedVideo.videoId,
playerUrl: uploadedVideo.assets?.player,
hlsUrl: uploadedVideo.assets?.hls,
thumbnailUrl: uploadedVideo.assets?.thumbnail,
mp4Url: uploadedVideo.assets?.mp4,
iframeEmbed: uploadedVideo.assets?.iframe,
};
}
Progressive Upload for Large Files
import fs from "fs";
import path from "path";
async function progressiveUpload(videoId: string, filePath: string) {
const CHUNK_SIZE = 50 * 1024 * 1024; // 50 MB
const fileSize = fs.statSync(filePath).size;
const totalParts = Math.ceil(fileSize / CHUNK_SIZE);
const session = apiVideo.videos.createUploadProgressiveSession(videoId);
for (let part = 0; part < totalParts; part++) {
const start = part * CHUNK_SIZE;
const end = Math.min(start + CHUNK_SIZE, fileSize);
const chunk = fs.createReadStream(filePath, { start, end: end - 1 });
const tempPath = path.join("/tmp", `chunk-${part}.mp4`);
const writeStream = fs.createWriteStream(tempPath);
await new Promise<void>((resolve) => {
chunk.pipe(writeStream).on("finish", resolve);
});
if (part < totalParts - 1) {
await session.uploadPart(tempPath);
} else {
await session.uploadLastPart(tempPath);
}
fs.unlinkSync(tempPath);
}
}
Delegated Upload Tokens (Client-Side)
// Server: generate a delegated token
async function createDelegatedToken(ttlSeconds = 3600) {
const token = await apiVideo.uploadTokens.createToken({
ttl: ttlSeconds,
});
return { token: token.token, ttl: token.ttl };
}
// Client: use the token with the uploader
import { VideoUploader } from "@api.video/video-uploader";
async function uploadFromBrowser(file: File, token: string) {
const uploader = new VideoUploader({
file,
uploadToken: token,
videoName: file.name,
retries: 3,
});
uploader.onProgress((event) => {
const percent = Math.round((event.uploadedBytes / event.totalBytes) * 100);
console.log(`Upload progress: ${percent}%`);
});
const video = await uploader.upload();
return video;
}
Live Streaming
async function createLiveStream(name: string) {
const liveStream = await apiVideo.liveStreams.create({
name,
record: true, // Record the stream for VOD
public: true,
playerId: "pl4f4ferf5jf5ahq9f2a",
});
return {
liveStreamId: liveStream.liveStreamId,
streamKey: liveStream.streamKey,
rtmpUrl: `rtmp://broadcast.api.video/s/${liveStream.streamKey}`,
playerUrl: liveStream.assets?.player,
hlsUrl: liveStream.assets?.hls,
iframeEmbed: liveStream.assets?.iframe,
};
}
// Update live stream thumbnail
async function setLiveStreamThumbnail(liveStreamId: string, imagePath: string) {
await apiVideo.liveStreams.uploadThumbnail(liveStreamId, imagePath);
}
Player Customization
async function createCustomPlayer() {
const player = await apiVideo.playerThemes.create({
name: "brand-player",
text: "#ffffff",
link: "#3498db",
linkHover: "#2980b9",
trackPlayed: "#3498db",
trackUnplayed: "#95a5a6",
trackBackground: "#2c3e50",
backgroundTop: "#1a1a2e",
backgroundBottom: "#16213e",
backgroundText: "#e0e0e0",
enableApi: true,
enableControls: true,
forceAutoplay: false,
hideTitle: false,
forceLoop: false,
});
return player.playerId;
}
// Assign the player theme to a video
async function assignPlayer(videoId: string, playerId: string) {
await apiVideo.videos.update(videoId, {
playerId,
});
}
// Upload a logo for the player
async function uploadPlayerLogo(playerId: string, logoPath: string, link: string) {
await apiVideo.playerThemes.uploadLogo(playerId, logoPath, link);
}
Analytics
async function getVideoAnalytics(videoId: string) {
// Get play count metrics
const metrics = await apiVideo.analytics.getMetricsBreakdown({
metric: "play",
breakdown: "country",
from: new Date(Date.now() - 30 * 24 * 60 * 60 * 1000),
to: new Date(),
filterBy: { videoId },
});
// Get aggregate metrics over time
const overTime = await apiVideo.analytics.getMetricsOverTime({
metric: "play",
from: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000),
to: new Date(),
interval: "day",
filterBy: { videoId },
});
return { breakdown: metrics.data, overTime: overTime.data };
}
Captions and Subtitles
async function manageCaptions(videoId: string) {
// Upload captions
await apiVideo.captions.upload(videoId, "en", "captions-en.vtt");
await apiVideo.captions.upload(videoId, "fr", "captions-fr.vtt");
// Set default caption language
await apiVideo.captions.update(videoId, "en", {
default: true,
});
// List all captions
const captions = await apiVideo.captions.list(videoId);
return captions.data;
}
Webhook Handling
import type { NextApiRequest, NextApiResponse } from "next";
interface ApiVideoWebhookPayload {
type: string;
emittedAt: string;
videoId?: string;
liveStreamId?: string;
encoding?: {
quality: string;
status: string;
};
}
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method !== "POST") {
return res.status(405).json({ error: "Method not allowed" });
}
const payload = req.body as ApiVideoWebhookPayload;
switch (payload.type) {
case "video.encoding.quality.completed":
await handleEncodingComplete(payload);
break;
case "live-stream.broadcast.started":
await handleLiveStarted(payload);
break;
case "live-stream.broadcast.ended":
await handleLiveEnded(payload);
break;
}
res.status(200).json({ received: true });
}
async function handleEncodingComplete(payload: ApiVideoWebhookPayload) {
if (!payload.videoId) return;
const video = await apiVideo.videos.get(payload.videoId);
await db.video.update({
where: { apiVideoId: payload.videoId },
data: {
status: "ready",
hlsUrl: video.assets?.hls ?? null,
mp4Url: video.assets?.mp4 ?? null,
thumbnailUrl: video.assets?.thumbnail ?? null,
duration: video.duration ?? 0,
},
});
}
async function handleLiveStarted(payload: ApiVideoWebhookPayload) {
if (!payload.liveStreamId) return;
await db.liveStream.update({
where: { apiVideoLiveId: payload.liveStreamId },
data: { status: "live", startedAt: new Date(payload.emittedAt) },
});
}
async function handleLiveEnded(payload: ApiVideoWebhookPayload) {
if (!payload.liveStreamId) return;
await db.liveStream.update({
where: { apiVideoLiveId: payload.liveStreamId },
data: { status: "ended", endedAt: new Date(payload.emittedAt) },
});
}
Video Management
async function manageVideos() {
// List videos with filters
const videos = await apiVideo.videos.list({
sortBy: "createdAt",
sortOrder: "desc",
currentPage: 1,
pageSize: 25,
tags: ["tutorial"],
});
// Update video metadata
await apiVideo.videos.update("vi4k0jvEUuaTdRAEjQ4Prklg", {
title: "Updated Title",
description: "New description",
tags: ["updated", "tutorial"],
metadata: [{ key: "category", value: "education" }],
});
// Upload a custom thumbnail
await apiVideo.videos.uploadThumbnail("vi4k0jvEUuaTdRAEjQ4Prklg", "thumbnail.jpg");
// Pick a thumbnail from the video timeline
await apiVideo.videos.pickThumbnail("vi4k0jvEUuaTdRAEjQ4Prklg", {
timecode: "00:01:05.000",
});
// Delete a video
await apiVideo.videos.delete("vi4k0jvEUuaTdRAEjQ4Prklg");
}
Best Practices
- Use delegated upload tokens for client-side uploads; never expose your API key in browser code.
- Enable
mp4Supportwhen you need a downloadable version of the video alongside HLS streaming. - Use progressive upload for files over 200 MB to get resumable, chunked uploads.
- Create player themes once and assign them to videos rather than customizing per video.
- Store the
videoIdin your database as the primary reference; derive URLs from it. - Use the
record: trueoption on live streams to automatically create VOD assets after broadcast. - Set meaningful
tagsandmetadataon videos to enable filtering and search in your application. - Handle the
video.encoding.quality.completedwebhook to know when each quality tier is ready.
Anti-Patterns
- Do not upload files directly with your API key from the browser; use delegated tokens instead.
- Do not upload entire large files in a single request; use progressive upload for reliability.
- Do not poll for encoding status; use webhooks to be notified when encoding completes.
- Do not create a new player theme for every video; reuse themes to maintain consistent branding.
- Do not ignore the difference between video creation and video readiness; a created video is not yet playable.
- Do not skip setting
public: falsefor premium content; public videos are accessible to anyone with the URL. - Do not hardcode player embed URLs; use the
assets.iframeorassets.playerfields from the API response. - Do not forget to clean up delegated tokens that are no longer needed; they consume quota until they expire.
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"
Bunny Stream
"Bunny.net Stream: video hosting, HLS delivery, direct uploads, video collections, thumbnail generation, token authentication, webhook events"
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"