REST_API · V1

API Documentation

VideoKavach provides a REST API for programmatic video management, plus a signed embed token system for secure playback in any frontend or LMS.

Quick Start

  1. 1. Create an API key

    Go to Settings → API Keysin your dashboard and generate a key. Copy it — it's shown only once.

  2. 2. Upload a video

    # Step 1: Create a video row + get a presigned upload URL
    curl -X POST https://videokavach.com/api/v1/videos/presign \
      -H "Authorization: Bearer kvch_live_..." \
      -H "Content-Type: application/json" \
      -d '{"title":"My Lecture","filename":"lecture.mp4","size":52428800}'
    
    # Response
    {
      "videoId": "vid_abc123",
      "uploadUrl": "https://r2.example.com/..."
    }
    
    # Step 2: PUT the file to the presigned URL (browser or server)
    curl -X PUT "<uploadUrl>" \
      -H "Content-Type: video/mp4" \
      --data-binary @lecture.mp4
    
    # Step 3: Trigger transcoding
    curl -X POST https://videokavach.com/api/v1/videos/vid_abc123/finalize \
      -H "Authorization: Bearer kvch_live_..."
  3. 3. Embed the video

    Once the webhook fires video.ready, sign an embed token server-side and render an iframe:

    // Node.js — sign embed token with your API key as the HMAC secret
    import { SignJWT } from "jose";
    import { createSecretKey } from "crypto";
    
    const secret = createSecretKey(Buffer.from("kvch_live_..."));
    
    const token = await new SignJWT({
      vid: "vid_abc123",
      viewer_id: "user_42",
      viewer_email: "[email protected]",
      viewer_phone: "+919876543210", // optional
    })
      .setProtectedHeader({ alg: "HS256" })
      .setIssuedAt()
      .setExpirationTime("2h")
      .sign(secret);
    
    // Render in your HTML/JSX:
    // <iframe src="https://videokavach.com/embed/vid_abc123?t={token}" />

Authentication

All /api/v1/* endpoints require a Bearer token in the Authorization header.

Authorization: Bearer kvch_live_<your_api_key>
Keep your API key secret. It is both your REST API credential and the HMAC secret for signing embed JWTs. Never expose it in client-side code.

Embed JWT

The embed player at /embed/{videoId} requires a signed JWT passed as ?t=. Sign it on your backend — never in the browser.

Algorithm & Secret

Algorithm: HS256. Secret: your API key plaintext (e.g. kvch_live_...).

JWT Payload

{
  "vid":          "vid_abc123",          // required — must match the video in the URL
  "viewer_id":    "user_42",             // required — your internal user ID
  "viewer_email": "[email protected]", // optional — shown in watermark
  "viewer_phone": "+919876543210",       // optional — shown in watermark
  "iat":          1716000000,            // issued-at (unix seconds)
  "exp":          1716007200             // expiry   (unix seconds) — max 24h recommended
}

iframe embed

<iframe
  src="https://videokavach.com/embed/vid_abc123?t=<jwt>&autoplay=1"
  width="720" height="405"
  allowfullscreen
  frameborder="0"
></iframe>

PHP example (WordPress plugin / Moodle)

<?php
function kavach_sign_token(string $videoId, $viewer): string {
    $apiKey = KAVACH_API_KEY; // your API key
    $header  = rtrim(strtr(base64_encode(json_encode(['alg'=>'HS256','typ'=>'JWT'])), '+/', '-_'), '=');
    $payload = rtrim(strtr(base64_encode(json_encode([
        'vid'          => $videoId,
        'viewer_id'    => (string) $viewer->ID,
        'viewer_email' => $viewer->user_email,
        'iat'          => time(),
        'exp'          => time() + 7200,
    ])), '+/', '-_'), '=');
    $sig = rtrim(strtr(base64_encode(hash_hmac('sha256', "$header.$payload", $apiKey, true)), '+/', '-_'), '=');
    return "$header.$payload.$sig";
}

Branding Override

By default the player shows the logo and accent colour configured in your account's Settings → Player Branding. You can override these per-embed by including a branding field in the JWT payload.

Typical use case — multi-tenant platforms. If you host video on behalf of multiple organisations (e.g. a SaaS that licenses content to coaching centres), each org can embed the same video with their own logo simply by injecting their logo URL into the token at sign time.

JWT payload with branding override

{
  "vid":          "vid_abc123",
  "viewer_id":    "user_42",
  "viewer_email": "[email protected]",
  "iat":          1716000000,
  "exp":          1716007200,

  "branding": {
    "logoUrl":      "https://your-cdn.com/tenant-logo.png",
    "logoPosition": "top-right",   // top-left | top-right | bottom-left | bottom-right
    "logoSize":     "small",       // small | medium | large
    "logoOpacity":  80,            // 0–100
    "accentColor":  "#4f46e5"
  }
}

All branding fields are optional

FieldTypeDefault
logoUrlstring | nullAccount's configured logo; null removes it for this embed
logoPositiontop-left | top-right | bottom-left | bottom-rightAccount setting (default: top-right)
logoSizesmall | medium | largeAccount setting (default: medium)
logoOpacity0–100Account setting (default: 80)
accentColorhex string | nullAccount setting (default: null → system blue)

Remove logo for a specific embed

"branding": { "logoUrl": null }

Videos

GET/api/v1/videos

List all videos for your tenant. Supports ?status=ready|transcoding|failed and ?q=search.

POST/api/v1/videos/presign

Create a video row and return a presigned S3/R2 upload URL. Body: { title, filename, mimeType, size }.

POST/api/v1/videos/:id/finalize

Trigger transcoding after the source file has been uploaded to the presigned URL.

GET/api/v1/videos/:id

Get video details including status, duration, variants, and storage size.

DELETE/api/v1/videos/:id

Delete a video and all associated storage (HLS segments, thumbnail, captions).

GET/api/v1/videos/:id/domains

Get domain restrictions for this video. Returns inherit flag and effective domain list.

PUT/api/v1/videos/:id/domains

Set domain restrictions. Pass null for domains to inherit account defaults, or an array to override.

GET/api/v1/videos/:id/chapters

Get chapters (time-coded markers) for a video.

PUT/api/v1/videos/:id/chapters

Replace all chapters. Body: [{ title, startSec }].

List videos — example response

GET /api/v1/videos

{
  "videos": [
    {
      "id": "vid_abc123",
      "title": "Module 1 — Introduction",
      "status": "ready",
      "durationSec": 1842,
      "width": 1920,
      "height": 1080,
      "sourceBytes": 524288000,
      "createdAt": "2026-05-01T10:00:00Z"
    }
  ]
}

Automated Domain Management

Platforms that license content to multiple organisations typically need to add a licensee's domain to every video in a course when a subscription activates, and remove it on expiry or revocation.

How domain enforcement works. If a video's effective domain list is non-empty, the embed page checks the Referer header. Playback is blocked if the origin doesn't match. An empty list means no restriction — the signed token is the only gate.

Pattern — add domain on license activation

async function addLicenseeDomain(kavachBaseUrl, kavachApiKey, videoId, newDomain) {
  const res = await fetch(`${kavachBaseUrl}/api/v1/videos/${videoId}/domains`, {
    headers: { Authorization: `Bearer ${kavachApiKey}` },
  });
  const { effective } = await res.json();
  const updated = [...new Set([...effective, newDomain])];
  await fetch(`${kavachBaseUrl}/api/v1/videos/${videoId}/domains`, {
    method:  "PUT",
    headers: { Authorization: `Bearer ${kavachApiKey}`, "Content-Type": "application/json" },
    body:    JSON.stringify({ domains: updated }),
  });
}

Domain pattern formats

PatternMatches
example.comExact domain — example.com only
*.example.comAll immediate subdomains — abc.example.com, xyz.example.com
Concurrent updates. The domain API is read-modify-write. For high-throughput automation, serialise calls per video or use account-level defaults where possible.

Captions & Audio Tracks

GET/api/v1/videos/:id/captions

List subtitle tracks for a video.

POST/api/v1/videos/:id/captions

Upload a caption track (VTT or SRT). Multipart form: file + label + language.

DELETE/api/v1/videos/:id/captions/:captionId

Remove a caption track.

GET/api/v1/videos/:id/audio

List alternate audio tracks (dubbed versions).

POST/api/v1/videos/:id/audio

Upload a new audio track. Multipart form: file + label + language.

DELETE/api/v1/videos/:id/audio/:trackId

Remove an audio track.

Account Settings

GET/api/v1/settings/webhook

Get the current webhook URL configured for this account.

PUT/api/v1/settings/webhook

Set the webhook URL. Body: { webhookUrl }.

GET/api/v1/settings/domains

Get the default allowed domains for this account.

PUT/api/v1/settings/domains

Set default allowed domains. Body: { domains: ["example.com"] }. Empty array = allow all.

GET/api/v1/tenant/usage

Get storage and bandwidth usage for the current billing period.

Webhooks

VideoKavach sends an HTTP POST to your webhook URL when video processing completes or fails. Configure your webhook URL in Settings or via PUT /api/v1/settings/webhook.

Request headers

X-Kavach-Event:     video.ready
X-Kavach-Signature: <hmac-sha256-hex>
Content-Type:       application/json

Signature verification

// Node.js
import { createHmac, timingSafeEqual } from "crypto";

function verifyKavachSignature(body: string, sig: string, apiKey: string): boolean {
  const expected = createHmac("sha256", apiKey).update(body).digest("hex");
  return timingSafeEqual(Buffer.from(expected), Buffer.from(sig));
}

video.ready payload

{
  "event": "video.ready",
  "video": {
    "id":           "vid_abc123",
    "title":        "Module 1 — Introduction",
    "duration_sec": 1842,
    "variants": [
      { "label": "360p",  "height": 360,  "bandwidth_bps": 896000 },
      { "label": "720p",  "height": 720,  "bandwidth_bps": 2928000 },
      { "label": "1080p", "height": 1080, "bandwidth_bps": 5192000 }
    ]
  }
}

Errors

All errors return JSON with error and message fields.

{
  "error":   "storage_quota_exceeded",
  "message": "Storage quota exceeded for your plan"
}
HTTPError CodeMeaning
401unauthorizedMissing or invalid API key
402storage_quota_exceededUpload would exceed your storage limit
403plan_restrictionFeature not available on your current plan
404not_foundVideo or resource does not exist
429rate_limitedToo many requests — slow down
500internal_errorUnexpected server error