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. Create an API key
Go to Settings → API Keysin your dashboard and generate a key. Copy it — it's shown only once.
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. 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>
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.
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
| Field | Type | Default |
|---|---|---|
| logoUrl | string | null | Account's configured logo; null removes it for this embed |
| logoPosition | top-left | top-right | bottom-left | bottom-right | Account setting (default: top-right) |
| logoSize | small | medium | large | Account setting (default: medium) |
| logoOpacity | 0–100 | Account setting (default: 80) |
| accentColor | hex string | null | Account 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.
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
| Pattern | Matches |
|---|---|
| example.com | Exact domain — example.com only |
| *.example.com | All immediate subdomains — abc.example.com, xyz.example.com |
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"
}| HTTP | Error Code | Meaning |
|---|---|---|
| 401 | unauthorized | Missing or invalid API key |
| 402 | storage_quota_exceeded | Upload would exceed your storage limit |
| 403 | plan_restriction | Feature not available on your current plan |
| 404 | not_found | Video or resource does not exist |
| 429 | rate_limited | Too many requests — slow down |
| 500 | internal_error | Unexpected server error |