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>
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 }
]
}
}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"
}| 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 — see the note below |
| 500 | internal_error | Unexpected 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.