Turn long videos into scored, reframed, captioned 9:16 clips from your own code — the same pipeline as the dashboard. Available as a REST API and as an MCP server, so any backend, workflow tool, or AI agent (Claude, Cursor, …) can clip a video with one call. Usage draws from your plan’s monthly video quota — no per-minute surcharges, no enterprise-only gating.
Katto is an AI video clipping tool that turns long videos into scored, captioned 9:16 clips. It exposes a public REST API, a CLI (npm katto-cli) and an official hosted MCP server (mcp.katto.tech, 29 tools, OAuth 2.1). All three are available on every plan, including free, and draw from the same monthly video quota as the dashboard.
New to the API? See what the video clipping API is for.
Submit a video, then poll until the clips are ready.
# 1. submit
curl -X POST https://katto.tech/api/v1/jobs \
-H "Authorization: Bearer sk_live_..." \
-H "Content-Type: application/json" \
-d '{"url":"https://www.youtube.com/watch?v=dQw4w9WgXcQ"}'
# -> { "id": "JOB_ID", "status": "queued", "status_url": "..." }
# 2. poll (every few seconds)
curl https://katto.tech/api/v1/jobs/JOB_ID \
-H "Authorization: Bearer sk_live_..."
# -> { "status": "completed", "hd_ready": true,
# "clips": [ { "url": "...", "captions_url": "...", "title": "...", "score": 88, "clip_index": 0, "hd": true } ] }Three common flows, end to end.
Podcast → short clips
curl -X POST https://katto.tech/api/v1/jobs \
-H "Authorization: Bearer sk_live_..." \
-H "Content-Type: application/json" \
-d '{"url":"https://www.youtube.com/watch?v=...","config":{"genre":"podcast","clipLength":"30_60"}}'
# poll GET /api/v1/jobs/JOB_ID until status "completed", then read clips[]Twitch VOD → clips
curl -X POST https://katto.tech/api/v1/jobs \
-H "Authorization: Bearer sk_live_..." \
-H "Content-Type: application/json" \
-d '{"url":"https://www.twitch.tv/videos/123456789","config":{"genre":"gaming"}}'Re-render or dub a finished clip
POST /api/v1/jobs/{id}/clips/{index}/rerender with a new layout_mode, caption_style or dub languages — free, no quota. Also available as MCP tools (katto_rerender_clip, katto_dub_clip); see the tools reference.
Create a key in Dashboard → API and pass it as a bearer token. A key is shown once; we store only a hash. Revoke anytime.
Authorization: Bearer sk_live_xxxxxxxxxxxxxxxxxxxxxxxx
POST /api/v1/jobs — accepts YouTube, Twitch, Kick, Vimeo, Zoom, Google Drive, Dropbox, Loom, LinkedIn, Facebook, Rumble, Dailymotion and Medal.tv links, or a direct https link to a video file. For a file on disk, see Upload a file.
// Node (fetch)
const r = await fetch("https://katto.tech/api/v1/jobs", {
method: "POST",
headers: {
Authorization: "Bearer sk_live_...",
"Content-Type": "application/json",
},
body: JSON.stringify({
url: "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
config: { genre: "podcast", clipLength: "30_60" },
}),
});
const { id, status_url } = await r.json();# Python (requests)
import requests
r = requests.post(
"https://katto.tech/api/v1/jobs",
headers={"Authorization": "Bearer sk_live_..."},
json={"url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ"},
)
job = r.json() # { "id": ..., "status": "queued", "status_url": ... }config is optional: genre, clipLength (lt30 / 30_60 / 60_90 / 90_180), customPrompt, topics.
Pass an Idempotency-Key header on POST /api/v1/jobs to make retries safe. A repeat with the same key returns the original job (200, with idempotent_replay: true) instead of creating — and quota-charging — a second one. While the first request is still in flight, a duplicate returns 409; retry shortly.
curl -X POST https://katto.tech/api/v1/jobs \
-H "Authorization: Bearer sk_live_..." \
-H "Idempotency-Key: 8f3a1c-your-unique-id" \
-H "Content-Type: application/json" \
-d '{"url":"https://www.youtube.com/watch?v=dQw4w9WgXcQ"}'Use a fresh unique key per distinct submission (a UUID works well), and reuse it when retrying that same submission.
GET /api/v1/jobs/{id} — poll until status is completed.
{
"id": "JOB_ID",
"status": "completed",
"progress": { "step": "done", "pct": 100 },
"hd_ready": true,
"clips": [
{ "url": "https://.../hq_0.mp4", "captions_url": "https://.../clip_0.srt", "title": "The hook that stops the scroll", "score": 88, "clip_index": 0, "hd": true },
{ "url": "https://.../hq_1.mp4", "captions_url": "https://.../clip_1.srt", "title": "Second-best moment", "score": 83, "clip_index": 1, "hd": true }
],
"error": null
}Clips are ranked by virality score (highest first; ties break on clip_index). hd is true once a clip's 1080p render has landed; the job-level hd_ready turns true once every clip is hd, i.e. the URLs are final. status reaches completed a little before that, so wait for hd_ready before caching a clip URL.
Statuses: queued → processing → scoring → completed (or failed). Clips finish asynchronously, and how long that takes scales with the length of the source. Measured across 422 completed jobs, from submit to finished clips: a median of 9 minutes overall, 10 minutes for sources under 30 minutes, 17 minutes for 30 to 60 minutes, and 28 minutes for 60 to 90 minutes, where the slowest tenth ran to 52. Poll the job rather than assuming a deadline.
GET /api/v1/jobs — your jobs, newest first. Keyset pagination: pass the previous next_cursor as ?cursor=. Optional ?limit= (1–100, default 20) and ?status=.
curl "https://katto.tech/api/v1/jobs?limit=20&status=completed" \
-H "Authorization: Bearer sk_live_..."
# -> { "jobs": [ { "id": "...", "status": "completed", "source": "youtube",
# "created_at": "...", "completed_at": "..." } ], "next_cursor": "..." }GET /api/v1/jobs/{id}/transcript — the completed job’s timestamped transcript. Returns 404 while still processing.
curl https://katto.tech/api/v1/jobs/JOB_ID/transcript \
-H "Authorization: Bearer sk_live_..."
# -> { "job_id": "...", "lang": "en",
# "segments": [ { "start": 26.78, "end": 27.16, "text": "Good morning." } ] }Three calls: reserve, PUT the bytes straight to storage, then start. Nothing is charged until the last step, which checks the file landed and spends one video of quota.
# 1. reserve (size in bytes; mp4, mov, webm or mkv)
curl -X POST https://katto.tech/api/v1/uploads \
-H "Authorization: Bearer sk_live_..." \
-H "Content-Type: application/json" \
-d '{"filename":"episode-12.mp4","content_type":"video/mp4","size":734003200}'
# -> { "job_id": "JOB_ID", "upload_url": "https://...", "method": "PUT",
# "headers": { "Content-Type": "video/mp4" }, "expires_in": 7200, "complete_url": "..." }
# 2. upload the file to upload_url (same Content-Type)
curl -X PUT -H "Content-Type: video/mp4" --upload-file ./episode-12.mp4 "UPLOAD_URL"
# 3. start clipping (optional "config", same fields as POST /api/v1/jobs)
curl -X POST https://katto.tech/api/v1/uploads/JOB_ID/complete \
-H "Authorization: Bearer sk_live_..."
# -> { "id": "JOB_ID", "status": "queued", "status_url": "..." }, then poll GET /api/v1/jobs/JOB_IDThe editor’s actions on a finished job, over the API. None of them uses video quota. Each needs a key with the write scope and returns 404 for a job that is not yours.
More clips from the same video
POST /api/v1/jobs/{id}/more renders the next best moments Katto scored but did not render (count 1–10, default 5). POST /api/v1/jobs/{id}/clips makes a clip from any range you choose, in seconds of the source (3–180 s, up to 10 per video, within 7 days of the job). New clips are appended to the job: poll GET /api/v1/jobs/{id}/clips.
curl -X POST https://katto.tech/api/v1/jobs/JOB_ID/clips \
-H "Authorization: Bearer sk_live_..." \
-H "Content-Type: application/json" \
-d '{"start_seconds":312.4,"end_seconds":356.0,"aspect":"9:16"}'
# -> 202 { "status": "queued", "expected_clip_index": 8, "title": "...",
# "has_captions": true, "status_url": ".../api/v1/jobs/JOB_ID/clips" }Translate captions, retitle
POST /api/v1/jobs/{id}/clips/{index}/translate-captions with {"language":"es"} re-renders the clip with its captions translated (105 languages, Creator plan; poll the returned status_url). POST /api/v1/jobs/{id}/clips/{index}/retitle writes a new AI title (optional style: energetic, descriptive, fun, shocking; counts against the plan’s monthly title allowance) or saves the title you pass.
Trim or extend, add a hook, rate
POST /api/v1/jobs/{id}/clips/{index}/adjust with start_offset_seconds and/or end_offset_seconds moves the clip’s edges relative to its original bounds (positive extends, negative trims; 3–180 s, inside the source; extending only within 7 days of the job) and re-renders it. POST /api/v1/jobs/{id}/clips/{index}/hook burns a hook over the first seconds (text, or omit it and the AI writes one; position top or center, duration 1–10 s). Both return 202 with a rerender_id to poll. POST /api/v1/jobs/{id}/clips/{index}/feedback with {"rating":"good"} or bad (optional reason) records the user’s verdict, like the thumbs on the results page.
curl -X POST https://katto.tech/api/v1/jobs/JOB_ID/clips/2/adjust \
-H "Authorization: Bearer sk_live_..." \
-H "Content-Type: application/json" \
-d '{"start_offset_seconds":-1.5,"end_offset_seconds":4}'
# -> 202 { "rerender_id": "...", "status": "queued", "start_seconds": 314.2,
# "end_seconds": 362.0, "duration_seconds": 47.8, "extended": true, "status_url": "..." }POST /api/v1/jobs/{id}/clips/{index}/publish posts a clip to a social account connected in the dashboard — YouTube, TikTok, LinkedIn, Instagram, Bluesky, Mastodon or Threads — now, or later with schedule_at (at most 7 days ahead). Paid plans; scheduling is on Creator. A platform that is not connected returns 409 with code: "platform_not_connected". TikTok requires a privacy_level chosen by the creator. Every call is a new public post: an agent should confirm with the user first.
Before publishing, GET /api/v1/connections lists the 7 platforms with connected, the account’s display_name and reconnect_required (tokens are never returned; read scope). POST /api/v1/jobs/{id}/clips/{index}/post-text (optional platform) returns AI-written title, caption and hashtags per platform, within each platform’s limits; nothing is posted. Scheduled posts are listed by GET /api/v1/scheduled-posts (optional status: pending, posting, posted, failed; paginated like jobs) and cancelled, while not yet published, by DELETE /api/v1/scheduled-posts/{id}.
curl -X POST https://katto.tech/api/v1/jobs/JOB_ID/clips/0/publish \
-H "Authorization: Bearer sk_live_..." \
-H "Content-Type: application/json" \
-d '{"platform":"linkedin","caption":"The one thing we got wrong about pricing"}'
# -> 200 { "status": "published", "platform": "linkedin", "result": { ... } }
# with "schedule_at": "2026-10-20T15:00:00Z"
# -> 201 { "status": "scheduled", "scheduled_post_id": "...", "scheduled_at": "..." }GET /api/v1/usage — your plan and monthly video quota.
curl https://katto.tech/api/v1/usage \
-H "Authorization: Bearer sk_live_..."
# -> { "plan": "pro", "videos_used": 6, "videos_limit": 25, "videos_remaining": 19 }Add webhook_url to a job to get a signed POST on completion instead of polling. It must be a public https URL.
curl -X POST https://katto.tech/api/v1/jobs \
-H "Authorization: Bearer sk_live_..." \
-H "Content-Type: application/json" \
-d '{
"url": "https://www.youtube.com/watch?v=...",
"webhook_url": "https://your-app.com/hooks/katto"
}'On completion Katto POSTs:
POST https://your-app.com/hooks/katto
X-Katto-Timestamp: 1786943456
X-Katto-Signature: sha256=<hmac>
{
"event": "job.completed",
"job_id": "...",
"status": "completed",
"hd_ready": true,
"clips": [ { "url": "...", "captions_url": "...", "title": "...", "score": 88, "clip_index": 0, "hd": true } ],
"timestamp": "..."
}Verify: HMAC-SHA256(secret, timestamp + "." + rawBody) compared to X-Katto-Signature (use a constant-time comparison). The timestamp is part of the signed payload, so to stop replays, also reject any request whose X-Katto-Timestamp is more than ~5 minutes old. Your signing secret is in Dashboard → API.
Over-limit responses return 429 with Retry-After and X-RateLimit-* headers.
| Scope | Limit |
|---|---|
| Per IP | 60 / min |
| Create job (per key) | 20 / min |
| Get job (per key) | 60 / min |
All errors are JSON: { "error": "message" }.
| 400 | Invalid or missing URL / oversized input |
| 401 | Invalid or missing API key |
| 403 | Monthly video quota reached, or a feature your plan does not include |
| 404 | Job not found (or not yours) |
| 409 | Not possible in the current state (still processing, a clip already in preparation, platform not connected) |
| 405 | Wrong method |
| 413 | Request body too large |
| 429 | Rate limit exceeded |
| 500 | Server error |
The full machine-readable contract is published as OpenAPI 3.1 at /openapi.json — import it into Postman, an SDK generator, or an agent.
curl https://katto.tech/openapi.json
katto-mcp exposes this API as MCP tools, so agents (Claude, Cursor, ChatGPT, VS Code, Windsurf) can clip videos as a tool call. Connect it two ways.
Hosted (recommended) — nothing to install
{
"mcpServers": {
"katto": { "url": "https://mcp.katto.tech/mcp" }
}
}Streamable HTTP at https://mcp.katto.tech/mcp. Sign in with your Katto account via OAuth 2.1 (dynamic client registration) — the key never touches the browser. Scripts can instead send Authorization: Bearer sk_live_… to the same URL (dual auth).
Local — run it yourself
{
"mcpServers": {
"katto": {
"command": "npx",
"args": ["-y", "katto-mcp"],
"env": { "KATTO_API_KEY": "sk_live_..." }
}
}
}29 tools, each carrying MCP annotations (read-only vs quota-spending vs destructive): katto_create_clip_job, katto_get_job, katto_list_jobs, katto_get_clips, katto_get_transcript, katto_get_usage, katto_get_account, katto_cancel_job, katto_list_sources, katto_list_clip_lengths, katto_rerender_clip, katto_dub_clip, katto_get_rerender, katto_get_brand_kit, katto_get_webhook_secret and katto_list_caption_styles. Then just ask your agent: “Clip the best moments from this podcast.”
Per-client setup: Claude, Cursor / VS Code / Windsurf, ChatGPT, Gemini. Full tool reference with input schemas: /mcp/tools. Published on npm and the official MCP registry.