Skip to main content
Technology & EngineeringVideo Services381 lines

Api.video

"api.video: video hosting API, upload, live streaming, player customization, analytics, webhooks"

Quick Summary33 lines
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 lines
Paste into your CLAUDE.md or agent config

api.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 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.

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: false for premium content; public videos are accessible to anyone with the URL.
  • Do not hardcode player embed URLs; use the assets.iframe or assets.player fields 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

Get CLI access →