Captik for Business
The Captik API
Send Captik a video from your own systems and get back word-timed captions in Indian languages and English, styled and edited videos, and finished exports up to 4K. The API draws on your business account's credit balance, the same one your team uses in the web editor and the Premiere Pro and DaVinci Resolve plugins.
Anyone can start: create a business account at captik.in/business, add credits (in India: rupees plus GST through Cashfree, with a GST invoice; elsewhere: US dollars through Dodo Payments), create an API key, and call the API. No sales call, no contract.
Overview
The API is a JSON REST API at https://captik.in/api/v1. A typical run:
- Upload your video (or give us a link to it) and create a project. Captik transcribes it and times every word.
- Read or edit the captions, pick a template and style, run an AI Edit, add sound, graphics or a voiceover.
- Start an export and download the finished video when the webhook says it is ready.
Anything that takes longer than a moment (transcription, AI Edits, exports) runs as a job: the call answers at once with the job, and you poll it or wait for the webhook.
Quickstart
- Sign in and create a business account at captik.in/business.
- Add credits there: India pays in rupees plus GST through Cashfree; everywhere else pays in US dollars through Dodo.
- Create an API key in the same dashboard, then:
# 1. Create a project from a video link (or upload one first: POST /uploads)
curl https://captik.in/api/v1/projects \
-H "Authorization: Bearer $CAPTIK_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: episode-0412" \
-d '{"video_url": "https://cdn.example.com/episode-0412.mp4", "language": "auto", "script": "roman"}'
# 2. Wait for the job (or the project.ready webhook)
curl https://captik.in/api/v1/jobs/JOB_ID -H "Authorization: Bearer $CAPTIK_API_KEY"
# 3. Download the captions
curl "https://captik.in/api/v1/projects/PROJECT_ID/captions?format=srt" -H "Authorization: Bearer $CAPTIK_API_KEY"
# 4. Export the video with a template, burned in
curl https://captik.in/api/v1/projects/PROJECT_ID/exports \
-H "Authorization: Bearer $CAPTIK_API_KEY" -H "Content-Type: application/json" \
-d '{"aspect": "16:9", "resolution": "1080p"}'Authentication
Every call carries one of your business's API keys:
Authorization: Bearer ck_live_...Owners and admins create and revoke keys in the business dashboard. A key is shown once, when it is made; Captik stores only a hash of it. Keep keys on your servers, never in an app or a web page. A revoked key stops working within a minute.
Credits and pricing
Your business account holds a prepaid credit balance: one credit is one rupee. Your team's use of the web editor and plugins and your API calls all draw on it. The standard rates, charged by the second of video (your agreement may differ; GET /me shows yours):
- ₹5.50 a minute of video captioned: transcription, scripts, translation to English, emphasis, styles and cut-outs on that video.
- ₹5.50 a minute to translate captions into another language.
- ₹7 a minute of video for each AI Edit.
- ₹3 a minute of video an AI video tool processes.
- ₹3.50 a minute of video exported, up to 1080p. A 4K minute counts as 2.5 minutes.
- ₹7 a minute of generated voiceover.
Adding credits. Owners and admins top up any time in the business dashboard, with a pack or any amount, or from your own systems with POST /credits/checkout, which returns a hosted payment link. In India you pay in rupees through Cashfree, with 18% GST added on top, and Captik issues a GST tax invoice for every top-up (GET /invoices). Elsewhere you pay in US dollars through Dodo Payments, at 65 credits per US dollar; Dodo adds your local sales tax or VAT and sends the invoice. Credits land the moment the payment does, and show in GET /usage at once. When your balance falls under your alert line, we email you and send a balance.low webhook, both with a link to top up.
A job that fails or that you cancel gives its credits back automatically. When a job needs more credits than your balance holds, it is refused with 402 insufficient_credits before any work starts, and uploads and new projects need at least ₹10 of credits. GET /usage lists every charge, refund and top-up, also as CSV.
Jobs
Long work answers 202 with a job:
{
"id": "6f1c...", "object": "job", "kind": "project.export",
"status": "running", // queued | running | succeeded | failed | canceled
"step": "rendering", "progress": 0.42,
"project_id": "b2e9...",
"output": null, // set when status is succeeded
"error": null, // { "code", "message" } when status is failed
"charged_credits": 40,
"created_at": "...", "started_at": "...", "finished_at": null
}Poll GET /jobs/:id (every few seconds is fine) or use webhooks. POST /jobs/:id/cancel stops a job and returns its credits. Jobs survive restarts on our side: one that is interrupted resumes from its last step.
Webhooks
Set an https endpoint in the business dashboard and Captik will POST these events to it:
project.ready,project.failed: a new project was transcribed (or could not be).export.ready: an export finished; the payload has the download link.job.succeeded,job.failed,job.canceled: any job ended.balance.low: your balance fell under your alert line.webhook.test: sent byPOST /webhooks/test.
Each call is signed with your webhook secret. Check the signature, and treat the Captik-Delivery id as idempotent: a delivery that does not get a 2xx answer within 10 seconds is retried, with backoff, 8 tries over about a day. Each retry goes to your webhook URL as it is then: change it and the events still owed follow; remove it and they stop.
No server yet? Set https://captik.in/api/webhook-test as your webhook and send a test event: it answers 200 to every call, so you see a delivery succeed. It stores nothing.
POST /your/endpoint
Captik-Event: export.ready
Captik-Delivery: 1d0c...
Captik-Signature: t=1731400000,v1=5f2b... // HMAC-SHA256 of "<t>.<raw body>"
{ "id": "1d0c...", "type": "export.ready", "created_at": "...", "data": { ... } }Verifying in Node.js:
import crypto from 'node:crypto';
function verify(rawBody, header, secret) {
const parts = Object.fromEntries(header.split(',').map((p) => p.split('=')));
if (Math.abs(Date.now() / 1000 - Number(parts.t)) > 300) return false; // older than 5 minutes
const want = crypto.createHmac('sha256', secret).update(`${parts.t}.${rawBody}`).digest('hex');
return crypto.timingSafeEqual(Buffer.from(want, 'hex'), Buffer.from(parts.v1, 'hex'));
}And in Python:
import hmac, hashlib, time
def verify(raw_body: bytes, header: str, secret: str) -> bool:
parts = dict(p.split("=", 1) for p in header.split(","))
if abs(time.time() - int(parts["t"])) > 300:
return False
want = hmac.new(secret.encode(), f"{parts['t']}.".encode() + raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(want, parts["v1"])Errors
Errors share one shape, with a request_id to quote to us:
{ "error": { "code": "insufficient_credits", "message": "...", "details": { "needed_credits": 120, "available_credits": 35 } }, "request_id": "req_..." }| 400 | invalid_request | The body or a parameter is not right; details lists what. |
| 401 | unauthorized | No key, or a key that is not valid. |
| 402 | insufficient_credits | Top up, then retry. |
| 403 | business_suspended | The account is paused. Contact us. |
| 404 | not_found | No such id in your business. |
| 409 | conflict / idempotency_conflict | A simultaneous change, or a reused Idempotency-Key. |
| 429 | rate_limited | Too many requests; wait Retry-After seconds. |
| 500 / 503 | internal_error / unavailable | Our side. Retry with backoff; it is logged. |
Limits and retries
- Up to 600 requests a minute per key. Over that,
429withRetry-After. - Send an
Idempotency-Keyheader (any unique string, up to 200 characters) on calls that start work. Repeating the call with the same key returns the first job instead of starting and charging a second one. - Videos up to 20 GB and 6 hours. Any common format; iPhone HEVC is converted for you before export.
- Download links last 24 hours; ask for the job or the project's exports again for a fresh one.
Endpoints
Account
Your business: balance, what you can spend now (with any credit line), your rates, seats and webhook settings.
{
"object": "business", "id": "…", "name": "…", "status": "active",
"balance_credits": 182450.5, "available_credits": 182450.5, "overdraft_credits": 0,
"low_balance_alert_credits": 1000,
"rates": { "currency": "INR", "credit_value_inr": 1, "caption_per_minute": 5.5, "export_per_minute": 3.5,
"export_4k_weight": 2.5, "voiceover_per_minute": 7, "ai_edit_per_minute": 7, "tools_per_minute": 3 },
"seats": { "used": 3, "limit": 10 },
"webhook": { "url": "https://…", "events": ["job.succeeded", "…"] }
}Every charge, refund and top-up in a date range, newest first, with the range's totals on the first page. Plain dates are midnight IST; "to" is not included. format=csv downloads the whole range as a spreadsheet.
Uploads
Send us the file directly, or skip this and give POST /projects a video_url instead.
Get somewhere to put your video. Up to 100 MB: one PUT to the url returned. Larger files (up to 20 GB) come back as a multipart upload: PUT each part to its url, then call /uploads/complete.
{ "filename": "episode-0412.mp4", "content_type": "video/mp4", "size_bytes": 2147483648 }// up to 100 MB
{ "upload": { "object": "upload", "key": "uploads/…/….mp4", "method": "PUT", "url": "https://…",
"headers": { "Content-Type": "video/mp4" }, "expires_at": "…" } }
// larger
{ "upload": { "key": "…", "method": "MULTIPART", "upload_id": "…", "part_size": 67108864,
"parts": [ { "part_number": 1, "url": "https://…" }, … ], "expires_at": "…", "complete_url": "…" } }Finish a multipart upload. Send the ETag each part PUT answered with (or leave parts out and we read them from storage).
{ "key": "uploads/…", "upload_id": "…", "parts": [ { "part_number": 1, "etag": "\"…\"" } ] }Projects
A project is one video and everything done to it.
Bring a video in and caption it: Captik downloads it (for a video_url), transcribes the speech, times every word and makes caption lines. Charged at the caption rate for the video's length. Send an Idempotency-Key so a retry never makes a second project.
{
"video_key": "uploads/…", // from POST /uploads, or:
"video_url": "https://cdn.example.com/ep.mp4",
"title": "Episode 412",
"language": "auto", // or a code from GET /languages, e.g. "hi-IN"
"script": "native", // native | roman | english
"template": "TEMPLATE_ID", // optional, from GET /templates
"metadata": { "show": "…", "episode": "412" }
}202 { "project": { "id": "…", "status": "processing", … }, "job": { "id": "…", "kind": "project.ingest", … } }
// job steps: downloading → extracting_audio → transcribing → aligning → captioning
// job output: { "project_id", "words", "lines", "duration_seconds", "language", "script", "warnings"? }Your projects, newest first: { object: "list", data, has_more, next_before }.
One project: status (processing | ready | failed), source details, language, script, template, a summary of the edit (including stock_credits), your metadata, and its latest job.
Rename it or change its metadata (keys you send are merged; null removes one).
{ "title": "Episode 412 (final)", "metadata": { "status": "approved" } }Delete a project. Its open jobs are cancelled.
Captions
The captions. SRT and VTT follow the caption lines with real word timings. Add download=1 to get a file. 409 project_not_ready while it is still processing.
{ "object": "captions", "project_id": "…", "language": "hi-IN", "script": "roman", "template": "…",
"words": [ { "id": "1", "text": "namaste", "start_ms": 820, "end_ms": 1240, "emphasis": false, "low_confidence": false } ],
"lines": [ { "start_ms": 820, "end_ms": 2310, "text": "namaste doston", "word_ids": ["1", "2"] } ] }Fix captions: change some words, replace them all, or regroup the lines. Words are in time order, without overlaps, and contain no spaces; lines cover every word in order.
{ "update": [ { "id": "14", "text": "Captik", "emphasis": true } ] }
// or { "words": [ { "text": "…", "start_ms": 0, "end_ms": 400 }, … ], "lines": [ { "word_ids": ["1","2","3"] } ] }
// or { "lines": [ { "word_ids": ["1","2"] }, { "word_ids": ["3","4","5"] } ] }Switch the script (native, roman, english: free) or translate the captions into another language (charged at the caption rate). Timings are carried over line by line.
{ "script": "roman" }
// or
{ "language": "ta-IN", "from": "original" }Catalogue
The ids the other calls take.
The spoken languages we transcribe (with their scripts) and the languages captions can be translated into.
Every caption template: { id, name, category, needs_cutout, tags }. Templates that draw behind the speaker make a cut-out automatically at export.
The AI Edits looks: { id, name, needs_cutout, bold }.
The music beds you can put under a video or an AI Edit.
The sound-effect library and the automatic sound-design styles.
Voiceover voices, reading styles and the speed range.
Style and edit
These change the project at once and answer with the new settings. Nothing is charged for them.
Template, colours, sizes, one or two lines, where the captions sit, emphasis, and per-caption changes by line index. GET returns the current style.
{
"template": "TEMPLATE_ID",
"colors": { "dominant": "#FFFFFF", "accent": "#FFD400", "text": "#FFFFFF" },
"font_scale": 1.1, "lines": "double",
"position": { "x": 50, "y": 78, "width": 80 }, // % of the frame
"captions": [ { "index": 3, "template": "OTHER_ID", "font": "Bebas Neue" } ],
"font": "FONT_ID" // your font's id, or a Google or built-in family; null = the template's
}Add one of your own fonts: a .ttf, .otf, .woff or .woff2 up to 5 MB, sent as a multipart "file" or as a link. Use its id (or its family) as "font" in /style. Free.
// multipart/form-data: file=@BrandSans.woff2, name=Brand Sans
// or
{ "url": "https://example.com/BrandSans.woff2", "name": "Brand Sans" }{ "id": "…", "object": "font", "name": "Brand Sans", "family": "BrandSans", "format": "woff2", "size_bytes": 48210, "created_at": "…" }Your fonts, and the Google and built-in families /style takes by name.
Remove one of your fonts. Captions that used it go back to their template's font at the next export.
Remove pauses, filler words ("um", "matlab") and repeated takes. Answers with the cuts made and the edited length.
{ "remove_pauses": { "min_ms": 600 }, "filler_words": true, "repeats": true }{ "cuts": [[12040, 12890], ...], "removed_ms": 48210, "source_duration_ms": 1500000, "edited_duration_ms": 1451790 }Automatic sound design, music bed and levels, or mute the original audio.
{ "auto_sound_effects": true, "sound_effects_level": 0.8, "music": "off", "music_level": 0.6, "mute_original": false }AI Edits, graphics, voiceover, cut-out
A full AI Edit in one of the looks: Captik watches the video, cuts it, and adds zooms, b-roll, sound and music. Every Customize switch of the editor is here. Send instructions to ask the director for a change, as in the editor. "broll": false takes the stock clips over the talk off; "stock_media": false keeps every stock photo and clip out of the edit, the look's own scenes included (use it for OTT, broadcast or anything that needs rights-cleared pictures; the scenes are drawn without pictures).
{
"look": "LOOK_ID",
"customize": { "hook": "3 mistakes new founders make", "zoom": "subtle", "broll": false,
"stock_media": false, "sounds": true, "cta": false, "accent": "#FF4D00" },
"music": "off",
"graphics": true,
"instructions": "calmer, fewer zooms"
}// job output
{ "planned": true, "scenes": 42, "sound_effects": 31, "hook": "...", "music": "off",
"stock_credits": [ { "kind": "video", "creator": "...", "url": "https://www.pexels.com/video/...",
"source": "pexels", "shown": [ { "start_ms": 81200, "end_ms": 83600 } ] } ],
"notes": [] }Motion graphics: auto plans them for the talk's moments, or place your own from the library with items. GET lists what is placed.
{ "auto": true }
// or
{ "items": [ { "template": "GRAPHIC_ID", "start_ms": 30000, "duration_ms": 4000, "params": { "title": "..." } } ], "replace": false }Generate a voiceover from text and place it on the video. Charged at the voiceover rate. GET lists voiceovers.
{ "text": "...", "voice": "warm", "style": "natural", "speed": 1, "start_ms": 0, "volume": 1 }Cut the speaker out of the background (for templates and looks that draw behind the person). Exports make one automatically when they need it; this lets you prepare it ahead. GET returns its status.
Tools
The editor's tools, one call each. A tool runs as a job and leaves its result on the project; the export draws it. Tools that work per clip take every clip of your video unless "clips" picks some: { "clip_ids": [...] } (GET /projects/:id/tools lists them) or { "from_ms", "to_ms" } (the video is split there first). Each job is charged once at the tools rate, for the seconds of video it works on. Undo is free. The sound tools take up to 5 minutes of video per call: do a longer one in stretches with from_ms / to_ms.
Every tool: its id, where it runs, whether it takes "clips", and its options as JSON Schema.
{ "object": "list", "data": [ { "id": "reduce-noise", "name": "Reduce noise", "group": "voice", "runs": "server",
"per_clip": true, "description": "…", "options": { "type": "object", "properties": { "strength": { "enum": ["light", "strong"] } } } }, … ] }The clips of the video, and what each tool has set (null when nothing).
{ "object": "tools", "project_id": "…",
"clips": [ { "id": "c_main", "own_video": true, "start_ms": 0, "end_ms": 61200 } ],
"tools": [ { "id": "stabilize", "name": "Stabilize", "runs": "server", "applied": { "clips": [ { "clip_id": "c_main", "level": "recommended" } ] } }, … ] }Run a tool. The body is its options, plus "clips" if you want only some of the video.
{ "strength": "strong", "clips": { "from_ms": 0, "to_ms": 30000 } }// job output
{ "object": "tool_result", "tool": "reduce-noise", "project_id": "…",
"clips": [ { "clip_id": "c_main", "audio_item_id": "a_…", "duration_ms": 30000, "tools": ["reduce_noise"] } ],
"applied": { … } }Undo: take off what that tool set. Answers { removed, applied }.
Takes out hiss, hum, fans and room noise, keeping the voice.
{ "strength": "light | strong" }Studio voice: noise out, rumble cut, clearer, levelled and loud.
{ "intensity": 25 | 50 | 75 | 100 } // 75 by defaultBrings the sound to a set loudness: default (-23 LUFS), tiktok (-12) or youtube (-14).
{ "target": "default | tiktok | youtube" }Changes the voice. The sound tools stack: each works on the last one's result. Undoing any of them gives the clip its original sound back.
{ "voice": "chipmunk | high | low | giant | robot | radio | megaphone | echo | hall | phone" }Finds the beats of the music in the sound. It returns them only; nothing on the video changes.
// job output
{ "clips": [ { "clip_id": "c_main", "bpm": 118.5, "beats": [ { "source_ms": 512, "edit_ms": 512 }, … ] } ] }Makes the colours better with a tone curve that follows the footage.
{ "intensity": 80 } // 0 to 100Gives the clips the colours of a reference: another clip, a moment or a stretch of the same video, or a picture.
{ "reference": { "at_ms": 41000 }, "intensity": 80 }
// or { "reference": { "clip_id": "c_…" } }, { "reference": { "from_ms": 0, "to_ms": 5000 } },
// { "reference": { "image_url": "https://example.com/look.jpg" } }Smooths grain and low-light noise, keeping edges.
{ "strength": 50 } // 1 to 100Finds where the footage cuts to another shot and splits the video into clips there.
{ "mode": "scene | shot" } // scene: only big changes, at least 2 s apartSmooths out shaky footage. More stable crops in a little more.
{ "level": "recommended | minimum | most" }Evens out lights that flicker or brightness that pumps. Use weak if the picture changes fast, strong if it changes slowly.
{ "mode": "timelapse | flash", "level": "weak | rec | strong" }Retouches faces. Values 0-100; leave them out for the automatic retouch. The faces are found once per stretch of video and reused by the next Retouch or Relight. If no face is found, the job still succeeds and its output says so.
{ "clear": 50, "smooth": 45, "whitening": 15, "blemishes": 0, "dewrinkle": 0, "even": 0, "smile_lines": 0, "dark_circles": 35, "bright_eyes": 25, "teeth": 35 }Adds light: soft light on the face, coloured backlights, moving creative light. Makes the cut-out first when the project has none, so the light falls on the person.
{ "preset": "soft_face | warm_atmosphere | water_light | morning_sun | neon_bicolor | yellow_purple | teal_backlight | blue_backlight | green_side | red_purple_stroke | mysterious_purple | dark_night_green | rainbow_spot | color_swipe | multicolor_switch | metallic_laser | color_rotation | multicolor_flicker" }Changes the clips' shape and keeps the person in frame as they move. Replaces Stabilize, Camera tracking and AI movement on those clips. Another ratio on the same clips needs no second look.
{ "ratio": "9:16 | 16:9 | 1:1 | 4:3 | 3:4 | 2.35:1 | 2:1 | 1.85:1 | 5.8-inch", "stability": "flexible | normal | stable", "speed": "fast | normal | slow" }Follows a face, body, hand or an area you pick, and holds it where it was on the picked frame. Use box to pick which one (fractions of the picture); "custom" needs a box.
{ "target": "face | body | hand | custom", "at_ms": 2000, "box": { "x": 0.4, "y": 0.1, "w": 0.2, "h": 0.25 },
"fit_to_canvas": false, "lock_size": false, "blur_canvas": false }Camera moves on the beats and gestures (dynamic, soft, zoom, shake) or follows the speech, pushing in on each sentence and punching in on emphasised words. Another preset on the same clips needs no second look.
{ "preset": "dynamic | soft | zoom | shake | speech", "scale": 50, "rotation": 50, "distance": 50, "strength": 50 }Measures how the camera moves, so an AI edit's scenes and a room look's words stay in place on handheld footage. Makes the cut-out first when needed. A still or unreliable camera keeps everything where it was planned. An AI edit export measures this itself when it has not been done.
{ "again": false }// job output
{ "camera_moves": true, "trusted": true, "measured_seconds": 30, "samples": 300, "person_grid": true }Tracks where the person is in every frame, from the cut-out. AI edits and rebuilt looks use it to place their scenes; a depth look uses it to blur and dim the background behind the person. Exports also do this themselves when it is missing.
{ "aspect": "original | 9:16 | 16:9 | 1:1 | 4:5" } // the export frame the depth grids are for; default the project'sExports
Render the finished video with everything above. Charged at the export rate for the edited length (4K counts 2.5x). The export.ready webhook carries the link.
{ "aspect": "original | 9:16 | 16:9 | 1:1 | 4:5", "resolution": "720p | 1080p | 4k", "quality": "best | small", "burn_captions": true }// job output
{ "render_job_id": "...", "url": "https://...", "expires_at": "...", "width": 1920, "height": 1080,
"duration_seconds": 1451.8, "stock_credits": [ ... ], "notes": [] }This project's exports, with fresh 24-hour download links.
Jobs
Your jobs, newest first.
One job: status, step, progress, output or error, credits charged.
Stop a queued or running job (an export it started is cancelled too) and return its credits.
Webhooks
Send a signed webhook.test event to your endpoint, to check your receiver.