REST API reference

Everything the VideoKavach API exposes: uploading videos, signing embed tokens, managing captions and chapters, reading analytics, and receiving webhooks. All endpoints are versioned under /api/v1.

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 }
    ]
  }
}

Analytics

The same watch data the dashboard shows, for pulling into your own reporting. Both endpoints are read-only.

GET/api/v1/videos/:id/analytics

Totals for one video: unique viewers, average percent watched, total watch time, and the timeline heatmap.

GET/api/v1/videos/:id/analytics/viewers

Per-viewer rows: viewer id and email, percent watched, session count, and last-watched timestamp.

GET/api/v1/tenant/usage

Account-level storage and bandwidth totals against your plan limits.

Watch data is sampled from heartbeats

The player posts the ranges it has played, which are merged into a per-viewer progress record. A viewer who watches the same minute three times still counts that minute once toward percent watched; the heatmap is where repeated views show up.

Devices

Used by native apps that support offline download. Devices are counted per (tenant, viewer_id) pair, so a student who buys from two different institutes gets a separate device budget from each.

POST/api/v1/devices

Register or touch a device. Body: { embedToken, label?, platform? }

GET/api/v1/devices?embedToken=...

List the devices registered for this (tenant, viewer) pair.

DELETE/api/v1/devices/:id

Remove a device, freeing its slot. Also revokes any offline licences held on it.

Going over the cap is rejected, not rotated

Unlike streaming sessions, a new device beyond the cap is refused outright rather than evicting the oldest one. Silently de-registering a device would destroy the downloads stored on it, so freeing a slot is left to the student.

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 — see the note below
500internal_errorUnexpected server error

Where rate limits apply

Limits are per-endpoint rather than account-wide. The one you are most likely to meet is on upload presigning: 20 requests per minute per account. Bulk imports should upload sequentially, which is also what the dashboard does.