# ClipOnIt API — guide for AI agents > ClipOnIt turns a long video into short, subtitled, reframed clips. REST API under `/api/*` (OpenAPI 3.1 at `/openapi.json`, interactive docs at `/docs`) and an MCP server at `POST /mcp` (discovery at `/.well-known/mcp.json`). Base URL: https://cliponit.app ## Auth - API key: `Authorization: Bearer ck_…`. Get one with `POST /api/me/api-key` (returns the key once; Starter plan or above, else 403). The user creates it in the app under Settings → API, or you can log in with `POST /api/auth/login {email,password}` → `{token}` and use that token as Bearer (30 days). - MCP: same header on `POST /mcp` (JSON-RPC 2.0, streamable-http; JSON responses, SSE only for agent-job waits with a progressToken). 401 without key, JSON-RPC `-32003` if the plan has no API access (free plan). 70 tools, each with `annotations` (readOnlyHint / destructiveHint / idempotentHint / openWorldHint): ask the user before calling a destructive one. A tool error comes back as `isError: true` with text `HTTP : [what to do]`; an unknown tool name is JSON-RPC `-32602`. - MCP via OAuth 2.1 (Claude.ai / ChatGPT connectors): no key needed. The 401 carries `WWW-Authenticate: Bearer resource_metadata=…`; discovery at `/.well-known/oauth-protected-resource` and `/.well-known/oauth-authorization-server`, dynamic client registration `POST /oauth/register`, PKCE S256, access tokens `cko_…` (1 h) + refresh (90 d, rotated). Same plan rules as the API key. - Chrome extension: `Authorization: Bearer cke_…`, minted by `POST /api/extension/token` from the signed-in website only (session cookie, not a Bearer). It is a portable session, so it works on **every plan including free** (unlike `ck_…`), slides 180 days forward on each call, and is listed/revoked with `GET /api/me/extensions` and `DELETE /api/me/extensions/{id}`. - Never put the key in a URL. ## Typical flow 1. `GET /api/me` → read `quota` (`videos_used`/`videos_limit`, feature booleans). The account owner gets an e-mail when a video finishes processing (clips ready, or failed); turn it off with `PATCH /api/me {notify_ready:false}` (`user.notify_ready` in `GET /api/me`). `user.live` says whether the Live studio (`/api/live/*`) is open for this account. 2. `POST /api/videos/url {url}` (or multipart `POST /api/videos` with `file`) → video `{id, status:"processing"}`. Optional clip objective (same fields as multipart text fields, `resources` as JSON there): `goal` = views | clients | comments | followers, `brief` (≤ 1000 chars), `resources` (≤ 5: `{kind:"site", url:"https://…"}` = the offer's site, read server-side to align the CTA; `{kind:"video", video_id}` = another of your videos for B-roll). It changes which moments are picked, the score (`score_detail.goal_fit`, 30 %), the titles, and adds `clip.cta` (caption call to action). Change it later with `PUT /api/videos/{id}/objective {goal?, brief?, resources?}` (replaces; `GET` to read, incl. `site_summary`) then `POST /api/videos/{id}/regenerate`. MCP: `import_video_url` / `upload_video_file` take the same fields; `set_video_objective {video_id, goal?, brief?, resources?, regenerate?}`. 3. Poll `GET /api/videos/{id}` every ~10 s until `state` is `ready` (or `failed` → read `error`; `POST /api/videos/{id}/retry`). `state` is `queued` (with `queue_position`) or `running`, `eta_s` is the estimated seconds left (~0.25 × duration). Processing takes ~1-5 min for 20 min of video. Over MCP use `wait_video` instead (server-side long-poll, one call). 4. `clips[]` sorted chronologically; choose by `score` (0-100, weighted sum of `score_detail` sub-scores 0-10: hook, standalone, payoff, emotion, value, shareability) and `reason`. YouTube imports may also return `heatmap: [{start_s, end_s, value}]` (the "Most replayed" curve, value 1 = most replayed slice; absent when YouTube has none) — handy to check which parts viewers rewatch. Need another moment? `POST /api/videos/{id}/search {query}` creates a clip (AI search), or `POST /api/videos/{id}/clips {start_s, end_s, title?, snap?}` creates one from exact bounds (3-180 s, snapped to sentence starts/ends within 0.6 s, else to the nearest word boundary within 0.6 s, unless `snap:false`; AI title when omitted, written in the background a few seconds after the response (the response carries the first words as a provisional title), inserted chronologically, no quota; full transcript with word timings: `GET /api/videos/{id}/transcript`, each segment carries `speaker` (0, 1, 2…) when the video was diarized). MCP: `create_clip`. 3. Poll `GET /api/videos/{id}` every ~10 s until `state` is `ready` (or `failed` → read `error`; `POST /api/videos/{id}/retry`). `state` is `queued` (with `queue_position`) or `running`, `eta_s` is the estimated seconds left (~0.25 × duration). `goal` echoes the clip goal chosen at import (`views|clients|comments|followers`, null if none). Processing takes ~1-5 min for 20 min of video. Over MCP use `wait_video` instead (server-side long-poll, one call). 4. `clips[]` sorted chronologically; choose by `score` (0-100, weighted sum of `score_detail` sub-scores 0-10: hook, standalone, payoff, emotion, value, shareability) and `reason`. YouTube imports may also return `heatmap: [{start_s, end_s, value}]` (the "Most replayed" curve, value 1 = most replayed slice; absent when YouTube has none) — handy to check which parts viewers rewatch. Need another moment? `POST /api/videos/{id}/search {query}` creates a clip (AI search), or `POST /api/videos/{id}/clips {start_s, end_s, title?, snap?}` creates one from exact bounds (3-180 s, snapped to sentence starts/ends within 0.6 s, else to the nearest word boundary within 0.6 s, unless `snap:false`; AI title when omitted, written in the background a few seconds after the response (the response carries the first words as a provisional title), inserted chronologically, no quota; full transcript with word timings: `GET /api/videos/{id}/transcript`). MCP: `create_clip`. 5. Optional edits on a clip (one edit per `?orientation=vertical|horizontal`): - `PATCH /api/clips/{id} {start_s, end_s}` to trim, `{title}` to rename (unknown fields → 400) (absolute seconds, ≥ 3 s; pick word boundaries from `GET /api/clips/{id}/transcript`) - `POST /api/clips/{id}/auto-reframe?mode=auto|speaker_tracking|keynote_split|keynote_speaker|follow|mix&framing=tight|medium|wide` (faces + camera cuts + speech level + slides → layout/keyframes, synchronous; `auto` (default, also applied in 9:16 to every detected clip after import) = keynote if a slide + speaker are detected, else `speaker_tracking` = frame whoever is talking (audio × mouth movement), clean cut per turn, split during rapid exchanges; `keynote_split` = slide on top + speaker (layout `keynote`); `keynote_speaker` = speaker only; `mode=mix` = mixed auto layout per shot: vertical / split / trio / full-frame wide shot; `framing` default medium, zoom capped), THEN `POST /api/clips/{id}/auto-zoom` (a later auto-reframe overwrites zoom keyframes) - `POST /api/clips/{id}/silences` → `{cuts}` (adaptive threshold, works with background noise) then `PUT /api/clips/{id}/edit {cuts}` (merge with `edit.cuts` from `GET /api/clips/{id}` if you had some) - `POST /api/clips/{id}/tics?intensity=light|normal|aggressive&apply=true` → removes ums, hesitations, repeats, false starts and dead air (keeps manual cuts; each auto cut has `kind`, removable one by one). Or `POST /api/clips/{id}/export?tics=normal` to do it at render time. - `POST /api/clips/{id}/export?style=premium` (also `export-all`; paid plans) → the "Premium" render, edited for the scroll without touching the edit: hook card quoting the clip, uppercase 3-word captions with keywords in yellow, ums/silences cut, shots from the video, light punch-ins, -14 LUFS. `style=standard` (default) = the edit as is. - `POST /api/montages` {media_ids, intent, reference_url | template_id | recipe, music, rights: true} (paid plans, 1 video of the monthly quota) → "Montage": the user's rushes (uploaded with `POST /api/media`) edited into a 9:16 video on the STRUCTURE of a reference link (TikTok, Reels, Facebook, Shorts: cuts, layout, on-screen texts, captions, tempo; the file is deleted after analysis, already-analysed references are reused for free) or a house format (`GET /api/montage/templates`: tier list, two-column sort, paid vs free, rated /10, wrong vs right split, before/after…). Poll `GET /api/montages/{id}` → url, edit_url, report (cost_usd, warnings). Recipe JSON schema: `GET /api/montage/schema`. Find references: `GET /api/montage/search?q=`. MCP: `montage_create`, `montage_wait`. - « Sans musique » (no music): `POST /api/clips/{id}/export?music=0` (also `export-all`, `montage=…`) → the render leaves out every music WE add (edit music bed, Montage pro library track); the clip's own sound/voice stays, loudness target unchanged. Remembered with the clip edit (`no_music`, also settable by `PUT /api/clips/{id}/edit {no_music:true}`); `music=1` puts it back. For the user to post, then pick a trending licensed sound in TikTok (« Add sound » → Trending, then « Volume » to keep the voice on top) or Instagram Reels (« Audio »). ClipOnIt never supplies third-party music. MCP: `music: false` on export_clip, export_all_clips, montage_pro, render_clip. Montage: `POST /api/montages {music: "none"}`. Explainer: `script.mix.mute: ["music"]` (also removes a chosen `script.music`). - `POST /api/clips/{id}/export?montage=entrepreneur|podcasteur|formateur|ecole|marque|auto` (paid plans, once `GET /api/montage` says `enabled`) → "Montage pro": Premium render edited like an ad — cold open on the strongest verified line, TikTok loop with rewind (`loop=1`), musical beat (wordless moment of the source chosen by loudness/motion), interlude shot from the shot index, boxed title, rhythm; options `cold_open`, `loop`, `moment`, `intermede`, `title` = 1/0; `auto` reads `brief` (« façon pub avec une boucle »). On a whole video: `POST /api/videos/{vid}/export-all?montage=…&n=3` = the 3 best moments. The export's `montage_report` lists what was applied or skipped and a hook score 0-3. MCP: `montage_pro`. Editable plan first: `POST /api/clips/{id}/montage/plan?recipe=entrepreneur` → cold-open options, title, musical beats, interlude shots, verified data cards (counter, table, transfer, quote) and toggles (split screen, ambient shots, loop, pace, handle); send it back as `{plan}` in the body of the export call. - `PUT /api/clips/{id}/edit` is a partial patch: `{subtitles:{style:"boxed"}}` changes only that field - `POST /api/videos/{vid}/clips/{id}/description`, B-roll/music/emojis (Starter+) - B-roll from the user's own video (Starter+): `POST /api/clips/{id}/broll/illustrate?orientation=` adds muted full-frame inserts taken from OTHER moments of the same source that show what is said (no talking-head shot, 1.5-2.5 s, ≥ 4 s apart; first call indexes the video, ~30-60 s) → `{broll, inserts, shots_indexed}`; `GET /api/clips/{id}/broll/shots` lists usable shots, `POST /api/clips/{id}/broll/from-video {start, end, source_s, shot?}` places one yourself. Add `?source_video_id=` (or `source_video_id` in the from-video body) to take the shots from another of your ready videos. MCP: `illustrate_clip_with_video`, `list_video_shots`, `add_video_broll`. Illustrate and shots also draw on the account's B-roll library (shots from your other videos; `source_video_id` on those shots). - B-roll library (all plans; free: 12 shots): reusable shots extracted from each of your videos (no talking head, not dark, sharp, 1.5-6 s) as 720x1280 + 1280x720 MP4 with tags, description and quality score. `GET /api/broll/library?q=&video_id=&tag=&favorite=` → `{items:[{id, video_id, start_s, end_s, description, tags, score, favorite, vertical_url, horizontal_url, thumb_url}], videos:[{id, status}], max}`; `PATCH /api/broll/library/{id} {favorite}`, `DELETE /api/broll/library/{id}`; `POST /api/videos/{id}/broll-library` (extract on demand, automatic on paid plans); `POST /api/clips/{id}/broll/library?orientation= {item_id, start, end?}` places one. `GET /api/videos/{id}/shots` = the full visual index as JSON (`talking`, `library`). MCP: `broll_search`, `broll_list`, `broll_add_to_clip`. - Add media (clip editor + Explainer studio): sources `{kind: broll|clip|upload, id}` resolved only for their owner. `GET /api/media` (your uploads), `POST /api/media` (multipart `file`, video or image, 200 MB, Starter plan; images become a 30 s still), `DELETE /api/media/{id}`, `GET /api/media/clips?q=` (your clips). Clip: `POST /api/clips/{id}/broll/media?orientation= {kind, id, start, end, mode: full|pip|split, audio}` → broll list (`place` + `audio` per item). Explainer: scene field `media: {kind, id, mode, at, dur?, audio}`, embedded at render. - `DELETE /api/clips/{id}` removes a clip (e.g. one created by search) 6. Subtitle defaults for every new clip edit: `PUT /api/me/subtitle-preset {style, color, size, words_per_line…}` once per account (merged over the built-in defaults, `GET` to read). 7. `POST /api/clips/{id}/export?orientation=vertical` → `{id, status:"queued", eta_s}`, or `POST /api/videos/{vid}/export-all?orientation=vertical` → `{exports:[{clip_id, export_id}]}` (one export per clip). Poll `GET /api/exports/{id}` every ~5 s (`eta_s`, `progress`) until `done` → download `url` (`/files/exports/.mp4`, prefix with the base URL). `GET /api/exports/{id}/frame?t=3` returns a 360 px JPEG to check framing/subtitles. `DELETE /api/exports/{id}` cancels a queued or running export (`status: canceled`). Over MCP use `wait_export`, `export_all_clips`, `get_export_frame`, `cancel_export`. ## Units and conventions - Times are seconds (float). Clip `start_s`/`end_s`, `cuts`, B-roll and emoji ranges: absolute in the source video. Keyframe `t`: relative to the clip start. - Zones `{x,y,w,h}` and emoji `x,y`: fractions 0..1. Layouts: vertical (1 zone), split (2), trio (3), spotlight (1), centered (1), horizontal (0 = full frame), keynote (2: slide on top in 16:9, speaker below). - `PUT /edit` is a patch: every field optional, absent = unchanged; `subtitles` merged field by field; `keyframes`/`cuts`/`broll`/`emojis` replace their list; `music: null` removes it. The edit row (and the account subtitle preset) is created on first access. - Subtitles: `style` karaoke|boxed|outline|glow|minimal|behind|serif|hollow|highlight (behind = one giant word behind the person, Premium), `bw` true = black & white, `color` yellow|white|green|cyan|pink|orange|rainbow, `position` bottom|middle|top, `size` 1|2|3, `words_per_line` 3..8, `lang` "original" or ISO code. ## Errors (`{"error": "message"}`) - 400 validation (bounds, enums, zone counts) — the message names the field (`champ subtitles.size invalide : attendu 1..3`); fix the payload, do not retry blindly. - 401 missing/invalid key — ask the user for a key. - 402 quota reached: monthly videos, 5 searches/day on free, file too big/long on free — stop and tell the user to upgrade at https://cliponit.app/billing. - 403 feature not in plan: API/MCP (Starter), translation/caption overrides/B-roll/music (Starter), Premiere XML (Pro), exporting the demo video (free) — tell the user, do not retry. - 404 not found or not owned by this key's user — the message names the resource and id (`clip introuvable…`). - 5xx transient (face model missing, ffmpeg) — retry once after a few seconds. ## Links - OpenAPI: https://cliponit.app/openapi.json - Docs: https://cliponit.app/docs - Integration guide (copy-paste curl / JavaScript / Python, MCP client configs, webhooks, quotas per plan): https://cliponit.app/integrations - MCP: https://cliponit.app/.well-known/mcp.json (endpoint https://cliponit.app/mcp), server card SEP-2127 https://cliponit.app/mcp/server-card - Catalogs: https://cliponit.app/.well-known/ai-catalog.json (AI Catalog), https://cliponit.app/.well-known/api-catalog (RFC 9727 linkset) - Plans: https://cliponit.app/pricing - Affiliate program (30% for life): `GET /api/me/affiliate` → `{ref_code, link, signups, paying, pending_cents, paid_cents, payouts}`; share `link`, sign-ups within 60 days are attributed. Public page: https://cliponit.app/affiliates ## Formats & hook (v2026-10) - orientation: vertical 1080x1920 | horizontal 1920x1080 | square 1080x1080 | portrait45 1080x1350. One independent edit per orientation; auto_reframe supports all four (square/portrait45: same face-following plan as vertical, at their own ratio). - subtitles.hook: {enabled, text (null = clip title, max 120 chars), duration_s 0.5..10 (default 2.5), style card|plain} — title card burned at the top during the first seconds. Set it via update_clip_edit or set_default_subtitles before export. - auto_zoom anchors the punch-in on the mean face box of the clip when faces were detected (run auto_reframe first). ## Explainer videos (v2026-10) Fully generated vertical videos (voice-over, pixel-art or dithered low-poly 3D scenes) from a JSON script that references catalogue elements by name — you never draw anything. Styles chosen by `script.style`: `pixel-news` (default, 2D pixel art panel + HUD + subtitles), `archibald` (full-screen low-poly 3D, moving camera, red highlights, title/number at the top), and every other style listed by `GET /api/explainer/styles`. Contract: docs/explainer.md. - `GET /api/explainer/styles` → `[{id, name, description, family (visual family), scene (2d|3d: scenes shaped like pixel-news / archibald), tags, poster, theme, scene_fields, video_fields, camera_presets, catalogue, library, safe_area}]`; `safe_area` = usable area in panel coordinates (pixel-news e.g. `{x:[0,180], y:[-18,150]}`: subtitles above, caption/credit below) — keep elements inside. - `GET /api/explainer/library?style=&compact=1` → `{style, text}`: that style's catalogue (~2-3k tokens). Read it once. `?all=1` → merged catalogue (one entry per concept, one variant per style). Animated previews: `/explainer-assets/