# AwesomeVideo API for agents > Transcribe, translate, dub, trim, clip, and burn styled captions into video, and write titles, descriptions, hashtags, and chapters. Prepaid credits: 1 credit = 1 cent. REST and MCP share one key and one balance. - REST base: https://api.awesomevideoeditor.com/v1 - Full docs in one file (site overview + this file): https://api.awesomevideoeditor.com/llms-full.txt (same as https://awesomevideoeditor.com/llms-full.txt) - OpenAPI: https://api.awesomevideoeditor.com/openapi.json - MCP (Streamable HTTP): https://api.awesomevideoeditor.com/mcp - MCP server card: https://api.awesomevideoeditor.com/.well-known/mcp.json - Live prices: GET https://api.awesomevideoeditor.com/v1/pricing (no auth) - Website: https://awesomevideoeditor.com (pricing: /pricing, account: /account) ## Fastest path: one call (caption_video) To put burned subtitles on a video, call MCP `caption_video` (REST `POST https://api.awesomevideoeditor.com/v1/captions`) once. It runs transcribe -> optional translate -> burn and returns the MP4, SRT, VTT, JSON transcript, and `creditsCharged`. Converting an existing subtitle file between SRT, VTT, and plain text needs no API call: every job already returns SRT, VTT, and JSON, and the conversion is a free local text transform (SRT uses `00:00:01,000` and numbered cues; VTT starts with `WEBVTT` and uses `00:00:01.000`). Humans can use the free in-browser converters at https://awesomevideoeditor.com/tools. - Source: `sourceUrl` (public link: direct https/http file, chat attachment URL, Google Drive or Dropbox share link) or `fileId` (from `create_upload`, `import_url`, or `upload_base64`). YouTube, TikTok, and Instagram links answer 422 `youtube_not_supported` / `platform_url_unsupported` with `charged: false`: upload the file instead. - Options: `language` (default auto), `translateTo` (ISO 639-1), `preset` or `brandPresetId`, `platform` (same ids as burn_captions), `autoFit` (default true; see below), `glossary`, `review: true` (transcribe now, send `review.reviewUrl`, burn charged on approval), `maxCredits`, `webhookUrl` / `notifyEmail`, `Idempotency-Key`. - Waiting: MCP waits up to 45 s by default (`waitSeconds` 0-50, `wait: false` to return at once). REST returns 202 at once unless you send `wait: true` or `waitSeconds`. Still running after the window: you get `status: queued|running` and `id`; poll `get_job` / `GET /v1/jobs/{id}` after `pollAfterSeconds`. Every job view has `step` (queued, transcribe, translate, burn, finalize, done, failed), `progress` and `stepProgress` (0-100; burn is real ffmpeg progress), `eta_seconds`, `estimatedCompletionAt`, and `pollAfterSeconds`. `quote` and `plan` return `estimatedSeconds`. - Resume: a failed job is refunded in full. For caption_video, transcribe, create_clips, dub_video, trim/remove_silences and generate_metadata, `resume_job {jobId}` (REST `POST https://api.awesomevideoeditor.com/v1/jobs/{id}/resume`) continues where it stopped: transcription is saved in 30 s chunks, so only missing chunks are transcribed and the price is the original minus the minutes already transcribed (1/min); a caption job with a finished transcript pays only the remaining steps (usually burn, 4/min). Idempotent, never a double charge. No speech: transcribe finishes as succeeded with `result.noSpeech: true` and 0 credits; caption/clips/metadata fail with error `no_speech`, refunded, `resumable: false`; resume_job on either returns 409 `job_not_resumable` (reason `no_speech`, charged: false). Burns of videos over 5 min render in 4 min segments saved as they finish (same frames, timing and untouched audio as one pass), so a retry or resume re-renders only the missing segments; no 10 min limit. 422 `resume_not_supported` / `resume_media_expired` (charged: false): call the tool again. - Price: exactly transcribe + (translate) + burn, no surcharge: 5 credits per audio minute, 7 with `translateTo`. `quote` with `operation: "caption"` (plus `translateTo`) or `plan` with `{op: "caption"}` prices it for free. If the audio is already in `translateTo`, the translate share is refunded. No speech or any failure: everything is refunded. - Example (MCP): `caption_video {"sourceUrl": "https://drive.google.com/file/d/FILE_ID/view", "language": "es", "platform": "tiktok"}` - Example (REST): `curl -X POST https://api.awesomevideoeditor.com/v1/captions -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" -d '{"sourceUrl":"https://example.com/clip.mp4","translateTo":"en","wait":true}'` Need to check or fix the text before burning? Use `transcribe` -> `edit_transcript` -> `burn_captions` instead. ## Get a key Agent, no human needed: `POST https://api.awesomevideoeditor.com/v1/keys/anonymous` (no auth, optional body `{"name": "my-agent"}`) or the MCP tool `create_key`. You get `token` once. The key starts with 0 credits. A network can create a few keys per day; reuse yours. Human: sign in at https://awesomevideoeditor.com/account with a magic link and create keys there. A human can also claim a self-serve key (`POST /v1/account/claim-key` with `{"token": "av_live_..."}` while signed in) so its balance and jobs move onto the account. Send `Authorization: Bearer av_live_...` on REST and on the MCP connection. Without a key, MCP `initialize`, `tools/list`, `create_key`, `quote` (durationSeconds only), `plan`, and `list_presets` work; other tools return an error that explains how to get a key. REST `POST /v1/quote` also works without a key when you pass `operation` and `durationSeconds`. OAuth 2.1 (for MCP clients that prefer it, e.g. Claude, ChatGPT connectors): connect to `https://api.awesomevideoeditor.com/mcp/oauth`. It answers 401 with `WWW-Authenticate: Bearer resource_metadata=...`, and the client discovers `https://api.awesomevideoeditor.com/.well-known/oauth-authorization-server`, registers itself (`POST https://api.awesomevideoeditor.com/oauth/register`, RFC 7591), and runs the authorization code flow with PKCE S256. The human signs in with an email magic link and approves. Access tokens (`av_oat_...`, 1 hour) and rotating refresh tokens (30 days) spend that account's balance, scope `mcp`. Each connected app gets its own key in /account (it does not use up API key slots); revoke it there. OAuth tokens also work on REST. ## Credits, packs, and plans 1 credit = 1 cent. Live list: GET https://api.awesomevideoeditor.com/v1/pricing. - `pack_5`: $5 one-time = 500 credits. Never expire. - `pack_20`: $20 one-time = 2,200 credits. Never expire. - `pack_50`: $50 one-time = 6,000 credits. Never expire. - `creator`: $9/month = 1,200 credits/month, or $90/year (10× monthly, 12 grants). - `pro`: $29/month = 4,500 credits/month, or $290/year (10× monthly, 12 grants). Includes the account webhook. - `scale`: $99/month = 18,000 credits/month, or $990/year (10× monthly, 12 grants). Includes the account webhook. Credit policy: pack credits never expire. Monthly plan credits roll over through the end of the following month. `plan`, `quote`, and `list_voices` are free. A paid call over a limit answers 422 before charging (`charged: false`). A server job that fails after charging is refunded automatically to the same balance. ## Paying: 402 insufficient_balance Paid calls check the balance first. If it is short, the call returns HTTP 402 (MCP: an error result) and nothing is charged: `{"error": "insufficient_balance", "requiredCents": 2, "balanceCents": 0, "checkoutUrl": "https://...", "howToBuy": "..."}` What to do: show `checkoutUrl` to your human (or open it). It buys pack_5 ($5 = 500 credits) for this exact key, no sign-in. After payment the credits land on the key within seconds; poll `GET /v1/balance` and retry the same request (send the same `Idempotency-Key` to avoid duplicates). Use `maxCredits` on any paid call to refuse jobs that would cost more than you expect. - `buy_credits` (MCP) = `POST /v1/checkouts` with an empty body: the $5 checkout tied to this key. Works on any key, including self-serve keys. - `create_checkout` (MCP) = `POST /v1/checkouts` with `{"product": "pack_20" | "pack_50" | "creator" | "pro" | "scale", "interval": "month" | "year"}`: bigger packs and plans. The key must be claimed by an account first (otherwise 409 `account_required`). Neither call grants credits by itself; payment does. Failed jobs refund their credits automatically. ## Paid tools and rates | Tool | REST | Price | | --- | --- | --- | | `caption_video` | POST /v1/captions | 5 credits / audio minute (transcribe + burn), 7 with `translateTo` | | `transcribe` | POST /v1/transcribe | 1 credit / audio minute | | `burn_captions` | POST /v1/captions/burn | 4 credits / audio minute | | `preview` | POST /v1/captions/preview | 1 credit flat (one PNG frame or a clip of at most 5 s) | | `translate` | POST /v1/translate | 2 credits / minute (+ burn price with `burn: true`) | | `create_clips` | POST /v1/clips | 1 credit / source minute (transcribe, 0 with a transcript) + 1 per started 10 source minutes (highlights) + 3 / output minute (render; reserved for count × maxSeconds, unused part refunded) | | `shorts` | POST /v1/shorts | 6 credits / source minute | | `remove_silences` (alias `trim`) | POST /v1/trim (alias /v1/captions/trim) | 3 credits / kept minute, + 4 / kept minute with `burnCaptions` | | `generate_metadata` | POST /v1/metadata | 1 credit / platform from an existing transcript; + 1 credit / minute transcription when only media is passed | | `dub` | POST /v1/dub | 5 credits / source minute per language (basic), 12 (standard), + transcription and translation if not reused | | `batch` | POST /v1/batch | sum of items, no surcharge | Billing is per second of audio, rounded up to the next cent per job. - transcribe: `mediaUrl` or `fileId`, optional `language` (default auto), `format` srt | vtt | json (json has word timestamps), `glossary` (names and terms; included). - burn_captions: media plus `subtitles` or `transcriptionJobId`, plus `preset` or `brandPresetId`. `combined: true` also returns SRT, VTT, and JSON at the burn price. `platform` (optional, same price) keeps captions out of the target app's UI: tiktok, instagram_reels, youtube_shorts, facebook_reels, snapchat_spotlight, linkedin, youtube (16:9), x; aliases like reels, instagram, shorts, snap, twitter; case-insensitive. If the user says "subtítulos para TikTok" or "for Reels", pass platform=tiktok / instagram_reels. Also on shorts, preview, prepare_client_burn, and plan steps. Only position and margins change, never the preset's style. The result has `safe_zone {platform, margins, frame}` and a `warning` when the video shape does not match (e.g. 16:9 into tiktok: margins apply to the real frame; use shorts to crop to 9:16). Unknown values return 422 `invalid_platform` with `validPlatforms` before any charge. List: `GET /v1/platforms` (MCP `list_platforms`, free, no key). - autoFit (default **on** for burn_captions, caption_video, preview, shorts, prepare_client_burn, review): caption size follows the frame (scale = short side / 720, cap height clamped to 3.0–4.6% of height on 9:16 and 1:1, 3.6–5.4% on 16:9), so a preset looks the same at 720p and 1080p; long captions split to at most 4 words on 9:16, 5 on 1:1, 7 on 16:9; and when no `platform` is given a default safe zone applies (9:16: bottom 35%, top 15%, left 8%, right 15%, clear of TikTok, Reels and Shorts UI; 1:1: bottom 12%; 16:9: bottom 7%). An explicit `platform` safe zone wins. Pass `autoFit: false` only when the user wants the preset's exact fixed pixel sizes and cue length. Same price either way. The web editor uses the same rule ("Auto size for video shape" checkbox), so a browser export and a server burn look identical. - Caption forms (style fields, same in the web editor and every burn): `maxWordsPerCaption` (integer 1-8) sets words per caption: 1 = one big word at a time (MrBeast/Hormozi hook style), 2-3 = short chunks. It always wins over autoFit's per-shape cap (autoFit still sizes the text and keeps the safe zone); unset = autoFit's cap. Validated by `validate_preset_style`. - Word pop and entrances (style fields): `activeWordReflow: true` makes a scaled active word (`wordStateStyles.active.scaleBy`, smart-emphasis pop) push the words on its line aside instead of overlapping them (line breaks keep resting widths, so lines never jump). `wordEntrance` bounce (grows 0.55 to 1.15 then settles to 1, with overshoot), slide-up, or fade plays per word when it is spoken (words are hidden until then but keep their place); `wordEntranceDuration` 0.05-1 s (default 0.22). Same timing curve in the web editor and the burn. - Position modes (style fields): `positionMode` fixed (default) | cycle | keyword. cycle walks captions through `positionCycle` (1-6 of top|center|bottom, default top, center, bottom) by caption order; keyword moves captions containing one of `positionKeywords` (up to 50 words, accent-insensitive, or any active wordHighlights match) to `keywordPosition` (default center). Deterministic, and top/bottom always stay inside the safe zone (vertical 9:16 keeps captions in the middle ~63%). `dynamicPositionEnabled` jumps are also clamped to the safe zone. - Stacked poster (style fields): `captionLayout: "stacked"` puts one word per line; the hero word (first `positionKeywords` or highlighted word, else the longest) is `posterHeroScale` times bigger (1-3, default 1.8). Every word shrinks to fit the caption width; sizes come from the same font metrics in the web editor and the burn. Pair with `maxWordsPerCaption` 2-4. - Face avoidance (opt-in style field): `faceAvoid: true` makes burns and previews sample the video for faces at 2 fps (same detector as face reframe) and move each caption to the first clear band inside the safe zone (its own position, then bottom, top, center; it stays put while clear). The result returns `faceAvoid.faceLayout` ([{start, end, position}]); save it in the style as `faceLayout` and the web editor and later burns reuse it (no new detection). Same price; adds about 5 s of container time per 100 s of 720p video. - preview: same inputs as a burn, `mode` still | clip, `atSeconds`, `clipSeconds` up to 5. - translate: `transcriptionJobId` or `subtitles`, `targetLanguage` (ISO 639-1, e.g. es, fr, de, pt, ja; unsupported codes get 400). Keeps cue timing. Word-level highlight (karaoke) is not kept on translated captions. - create_clips (long video → short clips, one call): `sourceUrl` or `fileId` (YouTube, TikTok and Instagram links get 422), optional `transcriptionJobId` or `subtitles` to reuse a transcript (JSON with word timing gives the tightest cuts). Transcribes, then an LLM (Workers AI Llama 3.3 70B, 8B fallback) reads the numbered sentences and picks `count` (1-10, default 3) self-contained, non-overlapping moments of `minSeconds`-`maxSeconds` (default 15-60, range 5-90). Cuts start on a sentence start and end on a sentence end (or a word end when one sentence is too long). `reframe` face (default, 720×1280 crop that follows the speaker, center fallback), center, or none (source shape, long edge up to 1280). `captions` (default true) burns captions with `preset` (any preset or `custom:cp_...`), `platform`, `autoFit` as in caption_video. Result: `clips [{name, rank (1 = best), start, end, seconds, title, hook, reason, score 0-100, text, crop}]` in time order, each MP4 in `files` (`clip-1`...) with a URL, plus `transcript_srt`/`transcript_vtt`/`transcript_json` when it transcribed. `quoteOnly: true` with `durationSeconds` returns the price without a job. `wait`/`waitSeconds` (max 50) as in caption_video; long sources keep running: poll `GET /v1/jobs/:id` or pass `webhookUrl`/`notifyEmail`. Example: 30-minute video, 3 clips up to 60 s = 42 credits reserved (30 + 3 + 9), less when clips are shorter. Failed jobs refund automatically. - shorts: media plus transcript, `count` 1-5, `minSeconds` 5-60 (default 15), `maxSeconds` minSeconds-60. Returns non-overlapping 720×1280 clips with burned captions. `reframe` face (default) follows the main speaker with a smoothed crop and falls back to center when no face shows; center forces a center crop. Same price. - remove_silences / trim: media plus transcript (`transcriptionJobId` or `subtitles`). Cuts pauses between words over `silenceGapSeconds` (default 0.5, 0.2-3) keeping `paddingSeconds` (default 0.08) on each side, plus long pauses inside word timings found by ffmpeg silencedetect (`silenceNoiseDb` default -35; `detectSilences: false` to skip). Fillers, `fillerMode` "clear" (default): uh, um, uhm, er, erm, ah, eh, ehm, em, hmm, mhm, mm, mmm, and "este" only when said on its own. "aggressive" also cuts like, you know, I mean, basically, kind of, sort of, o sea, bueno, pues, digamos, tipo, como que, a ver, only when they stand alone (comma or pause around them). `fillers` adds words or phrases (always cut), `keepFillers` keeps some, `removeFillers: false` keeps all. Every cut gets a 12 ms audio fade so joins do not click. Returns the cut MP4, retimed SRT/VTT/JSON, `ranges` kept, and `stats` (sourceSeconds, keptSeconds, removedSeconds, cuts, fillersRemoved, fillerSample). `burnCaptions: true` burns the retimed captions in the same job with `preset`/`brandPresetId`/`platform`/`autoFit` (same renderer as burn_captions; the uncaptioned cut is also returned as `mp4_uncaptioned`). Billed on the kept duration. - generate_metadata: `transcriptionJobId` (alias `jobId`: any finished transcribe, caption_video, trim/remove_silences, or translate job; never re-transcribes) or `subtitles`, else `mediaUrl`/`fileId` (transcribed first, 1 credit/min). `platforms`: youtube (default), shorts, tiktok, instagram (alias reels); one result per platform at 1 credit each. `language` (default: the video's language), `titleCount` 1-10 (default 5), `context` (creator notes, 500 chars). Result `platforms.
`: `titles`, `description`, `hashtags` (shorts always includes #shorts), `chapters` [{start, timestamp, title}] for youtube on videos of 60 s or more (first at 0:00, 3+ chapters at least 10 s apart, snapped to speech; dropped with a warning otherwise), and `descriptionWithChapters` ready to paste. Llama 3.1 8B on Workers AI with validated JSON; invalid output is retried once, then the job fails and is refunded. For a trimmed video pass the trim job id so chapter times match the cut.
- dub: media (`mediaUrl`, `fileId`, or a `transcriptionJobId` whose media is stored) and `targetLanguage`. Basic voices: en, es, fr, zh, ja, ko (Workers AI MeloTTS) and hi, ar, bn, pt, ru, ur, id, de, tr, it, vi (Google WaveNet). `voiceTier: "standard"`: en, es (Deepgram Aura-2) and hi, ar, bn, pt, ru, ur, id, de, tr, it, vi (Google Chirp 3 HD). `GET /v1/limits` lists what this deployment has enabled. Voices: `GET /v1/voices?language=de&tier=basic` (MCP `list_voices`, free, no key) lists every voice with gender, credits per minute, and a short MP3 `sampleUrl`; pass its `id` as `voice` to dub or plan (422 `voice_unknown`, `voice_language_mismatch`, or `voice_tier_mismatch` before any charge). Without `voice` you get the default voice. Google voices have a monthly character quota; when a job would pass it, `plan` lists it and dub returns 422 `provider_quota_exceeded` before any charge. Other languages return 422 `limit_exceeded` before any charge. Reuse `transcriptionJobId`/`subtitles` and `translationJobId`/`translatedSubtitles` to skip those charges. Each line is fitted to the original timing (up to 1.25x, then one shorter rewrite, then a cut with a short fade; short lines are padded). `keepOriginalAudio` duck (original at -18 dB, default) or mute. Returns MP4 plus target-language SRT, VTT, JSON; `audioTrack: true` adds M4A. No voice cloning, source separation, or lip sync.
- batch: up to 10 items with one `operation` (transcribe or burn). `dryRun: true` returns the total and debits nothing. Invalid items reject the whole batch before any charge.
## Human review and glossary (free)
When the person wants to check captions first ("quiero revisarlo antes", "let me check it first"): `transcribe`, then `request_review` (POST /v1/reviews) with `transcriptionJobId` and `preset` → `reviewUrl` (web editor, no login, up to 7 days). Send it to the person; poll `get_review` (awaiting_review → approved with `burnJobId` → `burnJob.result.url`, or rejected with `note`). The burn is charged only on approval, once. With `webhookUrl`/`notifyEmail` you also get `review.approved` (burnJobId), `review.rejected` (note) and `review.expired`, once each, signed like job webhooks. Saved glossary: `save_glossary` / `get_glossary` / `delete_glossary` (/v1/glossary) with `{term, aliases, doNotTranslate}`; applied to every transcribe and translate (`useSavedGlossary: false` skips).
## Custom presets (design your own style)
Free. `save_custom_preset` (POST /v1/custom-presets) stores a full caption style for this key, or the account when the key is claimed: pass `style` (the same JSON document `list_presets` returns and the web editor stores: font, size, color, stroke, background, shadow, position, reveal mode, animation, motion, `wordStateStyles` for the active word, `wordHighlights`, `smartEmphasis`, `segmentColorCycle`, caption forms such as `maxWordsPerCaption`) or `basePreset` (e.g. `hormozi-style`) plus `overrides` (deep-merged; null removes a field). It is validated with the renderer schema: invalid styles return 422 `invalid_style` with `errors [{path, message}]` (unknown fields are rejected). `fontFamily` (anywhere in the style, and in brand presets) must be a bundled font: Inter, Poppins, Oswald, Roboto, Anton, Bebas Neue, Playfair Display, Caveat, IBM Plex Mono (`GET /v1/fonts`, or `fonts` in `list_presets`); other fonts return 422. The result's `preset` (`custom:cp_...`) works anywhere a preset is accepted: burn_captions, caption_video, preview, shorts, prepare_client_burn, request_review, batch; server and browser renders are identical. `validate_preset_style` checks without saving; preview one still before a paid burn. Limits: 50 presets per key/account, 20 KB per style. `list_presets` with a key also lists yours (source `custom`). Custom presets are private. `submit_custom_preset` (POST /v1/custom-presets/:id/submit) proposes one for the public community gallery: it becomes `pending`, a human moderator reviews it, and it shows in `list_presets` for everyone only after approval (never automatic; max 5 submissions per day). Status: `get_custom_preset` → `community.status`.
Built-in caption presets (28; pass as `preset`, e.g. `builtin:beast-glow`): clarity, neon-glow, highlight, hormozi-style, mrbeast-style, sleek-caps, epic-lines, bubble-talk, power-play, sharp-notes, retro-glow, tiktok-vertical, karaoke, pop-words, frosted, editorial, handwritten, terminal, color-burst, iman-lowercase, beast-glow, signature-gold, beast-one-word, hormozi-pop, poster-hero, bounce-chunks, zone-jumper, podcast-karaoke. New: `iman-lowercase` (clean lowercase Inter, the spoken word turns bold), `beast-glow` (white Bebas Neue caps, black outline, green glow, the spoken word in neon green), `signature-gold` (gold Caveat script, centered, the spoken word in pale cream). Caption-form presets: `beast-one-word` (one giant Anton word at a time, bounce entrance, active word yellow), `hormozi-pop` (3 Poppins caps words, the active word grows green and reflows its neighbours), `poster-hero` (stacked Anton poster, the active word is the big orange hero line), `bounce-chunks` (white Inter with black outline, 3 words, each bounces in), `zone-jumper` (Oswald caps chunks alternating center and bottom, active word sky blue), `podcast-karaoke` (left-aligned white Inter in 2-3 full lines, words light up as spoken).
Per-call style (no saved preset needed): caption_video, burn_captions, preview, shorts, create_clips, remove_silences (with burnCaptions), translate with burn, batch items, request_review and prepare_client_burn (REST: the same body fields on POST /v1/captions, /v1/captions/burn, /v1/captions/preview, /v1/shorts, /v1/clips, /v1/trim, /v1/translate, /v1/batch, /v1/reviews, /v1/captions/prepare) accept `styleOverrides` (style fields deep-merged onto the preset, null removes a field, validated like custom presets with bundled fonts only; 422 `invalid_style` lists bad fields) and `faceAvoid: true` (shortcut for styleOverrides.faceAvoid: captions move off faces, sampled at 2 fps; the result returns `faceAvoid.faceLayout`). Same price. Example: `{"sourceUrl": "https://example.com/talk.mp4", "preset": "builtin:hormozi-pop", "faceAvoid": true, "styleOverrides": {"fontSize": 72, "maxWordsPerCaption": 2, "wordEntrance": "bounce", "wordStateStyles": {"active": {"color": "#FACC15"}}}}`. Caption-form fields: maxWordsPerCaption (1-N words per caption), activeWordReflow (grown active word pushes neighbours), wordEntrance (bounce, slide-up, fade) + wordEntranceDuration, positionMode (cycle with positionCycle, keyword with positionKeywords + keywordPosition), captionLayout "stacked" + posterHeroScale, faceAvoid.
Preview before ordering, free: MCP `style_preview` (REST POST https://api.awesomevideoeditor.com/v1/captions/style-preview) never charges and never transcribes. It takes the same look fields as `caption_video` (sourceUrl/fileId, preset or presets (up to 12; default 6 for the video shape), language, translateTo, brandPresetId, styleOverrides, faceAvoid, highlightWords, highlightColor, platform, autoFit) plus preview-only `text` (sample phrase, max 80 chars), `fullText`, `atSeconds` (one second or up to 3, one sheet row each; default a frame with a face, else the middle) and `transcriptionJobId`/`subtitles` (show the real caption at that second). Each preset is rendered exactly like the burn: same output size, autoFit sizing, words per caption, safe zone, face avoidance and engine, on the real frame. If the text is longer than one caption you see the chunk on screen at that second (`truncated: true`), like the burn; `fullText: true` shows every line but no longer matches a burn. Response: `sheetUrl` (labeled contact sheet), `presets[].frames[]` (url per second) and `presets[].captionVideoArgs`: pass it unchanged to `caption_video` (or POST /v1/captions) to get what you saw. Limits: 30 sheets/hour and 200/day per key (up to 3 seconds = one sheet); 429 `rate_limited` with `retryAfterSeconds`; identical repeats come from cache and do not count. A brand logo is not drawn in the preview.
Preview then order (example):
1. `style_preview` {"sourceUrl":"https://example.com/talk.mp4","presets":["hormozi-pop","podcast-karaoke","clarity"],"language":"es","text":"Así se verán tus subtítulos","platform":"youtube","faceAvoid":true,"atSeconds":[5,20]}
2. Show `sheetUrl`; the person picks "podcast-karaoke".
3. `caption_video` with that preset's `captionVideoArgs`: {"sourceUrl":"https://example.com/talk.mp4","preset":"podcast-karaoke","language":"es","platform":"youtube","faceAvoid":true}
Audio-only input (podcast, voice note: mp3, m4a, wav, ...): `caption_video` and `burn_captions` (POST https://api.awesomevideoeditor.com/v1/captions, /v1/captions/burn) return an MP4 with one of ten editorial podcast designs plus the captions, drawn on the server by the web editor's own canvas code (same frames as exporting the audio in the editor). Same price as a video: 5 credits/min caption, 4 burn. `style_preview` with `audioVisual` is free. Catalog with names, descriptions, fields, recommended caption presets and preview images: GET https://api.awesomevideoeditor.com/v1/audio-visuals (MCP `list_audio_visuals`), free, no key.
Designs (`audioVisual.variant`, default `editorial-cover`):
- `swiss-grid` (Swiss grid, light): International-style poster: a huge episode numeral on a six-column grid, one red hairline tracing the last 1.6 s of voice, progress rule and timecode. Fields: showName, episode, speaker. Empty fields: No episode: the numeral becomes the running timecode. No show/speaker: those labels are omitted.
- `braun-radio` (Braun radio, light): Dieter Rams-style radio: tuning scale with an orange needle that follows the voice, perforated grille, two knobs (voice level, elapsed time). Fields: showName, episode, speaker. Empty fields: Labels are omitted; the radio itself carries the design.
- `riso` (Riso print, light): Two-ink risograph: halftone discs in fluoro pink and blue overprint and breathe with the voice; misregistered type on paper tooth. Fields: showName, episode, speaker. Empty fields: No show name: the big title reads PODCAST.
- `editorial-cover` (Editorial cover, light): Magazine cover: serif masthead and issue line; the sentence is set as a pull-quote word by word, with a hairline under the spoken word that thickens with the voice. Fields: showName, episode, speaker. Empty fields: No show name: the masthead reads Podcast. No speaker: the byline is omitted.
- `bauhaus` (Bauhaus, light): Circle, square and half-disc on a grid; each answers the voice with its own small delay. Heavy lowercase type. Fields: showName, episode, speaker. Empty fields: The type stack is omitted; the composition stands alone.
- `continuous-line` (Continuous line, light): One pen line writes the voice across ruled rows, back and forth so it never lifts; a nib dot marks the tip. Fields: showName, episode, speaker. Empty fields: Labels are omitted.
- `vinyl` (Vinyl, dark): Top-down turntable: matte record turning at 33 rpm, cover art on the label, a light sheen that brightens with the voice, tonearm moving with progress. Fields: showName, episode, speaker, coverSrc. Empty fields: No cover: a plain label in the ink colour with the show name (or nothing).
- `type-wave` (Type as waveform, dark): The captions are the visual: each word is set at the weight of its own loudness (Inter 200-900), with a live weight scale. Fields: showName, episode, speaker. Empty fields: Labels are omitted. In preset mode the show name (or PODCAST) breathes in weight instead.
- `duotone-portrait` (Duotone portrait, light): Cover art or host photo in two inks, framed by a hairline that carries the voice around its edge; heavy caps with a marker block on the spoken word. Fields: showName, episode, speaker, coverSrc. Empty fields: No cover: a two-ink gradient field with the show's initials.
- `ma` (Ma, light): Japanese restraint: one sumi ink dot on washi that breathes, slow rings on loud syllables, a vermilion seal and a vertical label. Fields: showName, episode, speaker. Empty fields: No show name: the seal is a plain vermilion square.
`audioVisual` fields (all optional):
- `showName` (string, max 60), `episode` (string or number, max 12), `speaker` (string, max 60): printed by the design; empty ones are left out.
- `coverImageUrl` (public https) or `coverFileId` (from `create_upload`): PNG/JPEG/WebP up to 5 MB, fetched before any charge; used by vinyl (record label) and duotone-portrait (two-ink portrait). Other designs ignore it with a warning.
- `background` (paper), `accent`, `ink`: #rgb or #rrggbb. Default: the design's palette. Changing the paper picks a contrasting ink automatically unless `ink` is set.
- `captionMode`: `integrated` (default) = the design sets each sentence itself, word by word; the caption preset is not drawn. `preset` = your caption preset draws and the design keeps its caption band clear.
- `contrastGuard` (default true, preset mode): a preset whose text colour is too close to the paper (white text on a light design) switches to the design's ink without stroke/shadow. Light designs read best with Editorial, Sharp Notes or Frosted; dark designs (vinyl, type-wave) with Clarity, Iman Lowercase or Podcast Karaoke.
- `aspect` 9:16 (default, or from `platform`) | 16:9 | 1:1 | 4:5, or `width`+`height` (128-3840); `enabled` false = paper + captions only; `overlays` up to 8 image/text items.
- Retired visualizer ids still work and render their replacement, with a `warnings` entry on the job: bars→swiss-grid, ribbon→continuous-line, rings→ma, spectrogram→riso, skyline→bauhaus, silkline→type-wave, ivory→editorial-cover, halo→vinyl, needle→braun-radio, mono→duotone-portrait. Their `presetId`/`config` fields are ignored (warning), gradient backgrounds fall back to the design's paper.
- Errors: unknown variant/field, bad colour, too-long text, unreachable or non-image cover: 422 `invalid_audio_visual`, `charged: false`.
curl: curl -X POST https://api.awesomevideoeditor.com/v1/captions -H "Authorization: Bearer $AV_KEY" -H "Content-Type: application/json" -d '{"sourceUrl":"https://example.com/episode.mp3","preset":"clarity","platform":"tiktok","audioVisual":{"variant":"editorial-cover","showName":"Field Notes","episode":"12","speaker":"Alex Doe"}}'
MCP: caption_video {"sourceUrl":"https://example.com/episode.mp3","preset":"clarity","audioVisual":{"variant":"vinyl","showName":"Field Notes","coverImageUrl":"https://example.com/cover.jpg"}}
Python: requests.post("https://api.awesomevideoeditor.com/v1/captions", headers={"Authorization": f"Bearer {key}"}, json={"fileId": file_id, "preset": "editorial", "audioVisual": {"variant": "riso", "captionMode": "preset", "showName": "Field Notes"}}).json()
Copy-paste per design (REST body for POST /v1/captions; the same `audioVisual` object works in MCP caption_video, burn_captions and style_preview):
- swiss-grid: {"sourceUrl":"https://example.com/episode.mp3","preset":"editorial","audioVisual":{"variant":"swiss-grid","showName":"Field Notes","episode":"12","speaker":"Alex Doe"}}
- braun-radio: {"sourceUrl":"https://example.com/episode.mp3","preset":"editorial","audioVisual":{"variant":"braun-radio","showName":"Field Notes","episode":"12","speaker":"Alex Doe"}}
- riso: {"sourceUrl":"https://example.com/episode.mp3","preset":"editorial","audioVisual":{"variant":"riso","showName":"Field Notes","episode":"12","speaker":"Alex Doe"}}
- editorial-cover: {"sourceUrl":"https://example.com/episode.mp3","preset":"editorial","audioVisual":{"variant":"editorial-cover","showName":"Field Notes","episode":"12","speaker":"Alex Doe"}}
- bauhaus: {"sourceUrl":"https://example.com/episode.mp3","preset":"editorial","audioVisual":{"variant":"bauhaus","showName":"Field Notes","episode":"12","speaker":"Alex Doe"}}
- continuous-line: {"sourceUrl":"https://example.com/episode.mp3","preset":"editorial","audioVisual":{"variant":"continuous-line","showName":"Field Notes","episode":"12","speaker":"Alex Doe"}}
- vinyl: {"sourceUrl":"https://example.com/episode.mp3","preset":"clarity","audioVisual":{"variant":"vinyl","showName":"Field Notes","episode":"12","speaker":"Alex Doe","coverImageUrl":"https://example.com/cover.jpg"}}
- type-wave: {"sourceUrl":"https://example.com/episode.mp3","preset":"clarity","audioVisual":{"variant":"type-wave","showName":"Field Notes","episode":"12","speaker":"Alex Doe"}}
- duotone-portrait: {"sourceUrl":"https://example.com/episode.mp3","preset":"editorial","audioVisual":{"variant":"duotone-portrait","showName":"Field Notes","episode":"12","speaker":"Alex Doe","coverImageUrl":"https://example.com/cover.jpg"}}
- ma: {"sourceUrl":"https://example.com/episode.mp3","preset":"editorial","audioVisual":{"variant":"ma","showName":"Field Notes","episode":"12","speaker":"Alex Doe"}}
- preset captions on a light design: {"audioVisual":{"variant":"swiss-grid","captionMode":"preset"},"preset":"sharp-notes"}
- own colours: {"audioVisual":{"variant":"ma","background":"#14213d","accent":"#f2c14e"}} (ink becomes light automatically)
- free preview first: style_preview {"sourceUrl":"https://example.com/episode.mp3","presets":["clarity"],"audioVisual":{"variant":"type-wave","showName":"Field Notes"}}
- legacy request (still works, warns): {"audioVisual":{"variant":"bars"}} renders swiss-grid.
A brand logo is drawn bottom right. preview, shorts and create_clips on audio return 422 `audio_only_input`, not charged. Long audio renders in 2-minute pieces that a retry or `resume_job` reuses; drawing takes about 0.2-0.4x the audio length on one vCPU; on staging a 50 s episode finished in 1-2.5 minutes end to end, transcription included.
Style reference: `get_style_reference` (MCP) / GET https://api.awesomevideoeditor.com/v1/style-reference (free, no key) lists every style field the web editor has, with type, values/range and examples. Word highlights: `wordHighlights: {rules: [{id, enabled, color, backgroundColor?, words?, minLength?}], customLists?: [{id, name, enabled, color, backgroundColor?, words}]}`; rule ids numbers (digits, %), custom (its words list), acronym (ALL CAPS), hashtag (# and @), long (longer than minLength), person/orgloc (web editor NLP only; use custom words for server burns). Keywords in an accent color: `{"rules":[{"id":"custom","enabled":true,"color":"#FACC15","words":["gratis","AwesomeVideo"]}]}` or the shortcut `highlightWords: ["gratis","AwesomeVideo"], highlightColor: "#FACC15"` on any burn tool. Numbers: `{"rules":[{"id":"numbers","enabled":true,"color":"#F59E0B"}]}`. Marker background: `{"rules":[{"id":"custom","enabled":true,"color":"#000000","backgroundColor":"#FACC15","words":["oferta"]}]}`. Smart emphasis: `smartEmphasis: {enabled: true, target: "auto", style: "color"|"pop"|"pill", intensity: "low"|"med"|"high", color: "#A855F7"}` (server burns support target auto; verbs/nouns/numbers/custom are web-only, use wordHighlights instead). Word states: `wordStateStyles: {"active": {"color": "#FACC15", "scaleBy": 1.2}, "upcoming": {"opacity": 0.5}}` with revealMode word-by-word. Color cycle: `segmentColorCycle: {"enabled": true, "segmentSize": 2, "mode": "fixed", "variants": [{"text": "#FFFFFF"}, {"text": "#FACC15"}]}`.
Free price checks: `quoteOnly: true` returns {costCents, breakdown, charged:false} without starting a job on create_clips (POST /v1/clips, needs durationSeconds), remove_silences (POST /v1/trim, durationSeconds; upper bound since it bills kept minutes) and generate_metadata (POST /v1/metadata; durationSeconds only when passing media without a transcript). `list_presets` returns each style JSON.
## Free tools
`create_key`, `import_url` (POST /v1/uploads/import), `create_upload` / `create_multipart_upload` / `get_upload_status` / `complete_upload` / `abort_upload`, `quote`, `plan`, `list_presets`, `get_balance`, `get_job`, `list_jobs`, `get_usage`, `edit_transcript` (PATCH /v1/jobs/:id; next burn pays only the burn), `prepare_client_burn` (returns ASS to render in a browser at no cost), `save_brand_preset` / `list_brand_presets` / `delete_brand_preset`, `save_custom_preset` / `list_custom_presets` / `get_custom_preset` / `update_custom_preset` / `delete_custom_preset` / `submit_custom_preset` / `validate_preset_style` (see Custom presets), `buy_credits`, `create_checkout`, webhooks, and `notifyEmail`.
`plan`: `POST /v1/plan` (no key needed, never charges, 120/hour per IP) with `steps` (ordered: caption with optional `translateTo` (= caption_video), transcribe, translate, burn with `renderer` server|client, shorts with `count`/`minSeconds`/`maxSeconds`, trim with `keepSeconds`, preview, dub with `targetLanguage`/`voiceTier`) and `source` (`durationSeconds`, `bytes`, `via` upload|url, `transcriptCues`, or `mediaUrl`, sized with one HEAD request). Transcription is added when a later step needs a transcript unless `hasTranscript` is true, and translation before dub unless `hasTranslation` is true. Returns each step's credits, `totalCredits`, `totalUsd`, `freeAlternatives` (client burn is free), `feasible`, `limitViolations` (`code`, `limit`, `actual`, `unit`, `message`, `hint`), `limits`, and with a key `account.balanceCents` and `account.sufficient`. Call it before a pipeline.
Limits: stored files (`fileId`) up to 2048 MB (one PUT or `POST /v1/uploads` up to 80 MB; larger via multipart upload or `import_url`), `mediaUrl` on a paid call up to 80 MB (a larger Content-Length is rejected before download; use `import_url` for bigger links), `dub` sources up to 80 MB for now, up to 3 hours, shorts 1-5 clips of 5-60 s, shorts transcripts up to 4000 cues (read in windows of 400). Paid tools check these before reserving credits and return 422 `limit_exceeded` with `violations` and `charged: false`.
`quote`: `POST /v1/quote` with `operation` (caption with optional `translateTo`, transcribe, burn, preview, translate, shorts, trim, dub with `voiceTier` and `targetLanguage`) and `durationSeconds`, or `fileId` / `mediaUrl` with a key. Never debits. Trim quotes from media are an upper bound because trim bills kept seconds.
## Media
Local files (for example a video the user attached in chat): call MCP `create_upload` (or `POST /v1/uploads/url`) with `filename`, `contentType`, `sizeBytes` (JSON). Do not send that JSON to `POST /v1/uploads`: it takes multipart/form-data only and answers 400 `multipart_form_required`; it is free, checks limits first, and returns `fileId` plus a single-use `uploadUrl` valid 15 minutes. PUT the raw bytes there (`curl -X PUT --data-binary @clip.mp4 -H "Content-Type: video/mp4" "