# ReelsBuilder API

> REST API for short-form video, with MCP tools for generation, clipping and signals. Three areas: **Generate** (YouTube clips, stock video, AI video, AI influencer, blog to video), **Post** (REST only: publish finished videos to connected social accounts), **Signals** (trending topics and proven hooks). Every paid Generate call is an async job you poll; dry runs create no job.

Machine-readable capabilities: https://api.reelsbuilder.ai/api/v1/capabilities
Use /capabilities for the mode/input map and /tools for the current MCP allowlist. Some capability receipt notes still name legacy top-level aliases; every Generate job receipt uses data.job_id and data.poll_url below. YouTube clips are billed per delivered clip, with the requested maximum charged up front and undelivered clips refunded on completion.
Base URL: https://api.reelsbuilder.ai/api/v1
Human docs: https://reelsbuilder.ai/docs/api
Machine-readable tool catalog (no key): https://api.reelsbuilder.ai/api/v1/tools
Workflow tools (importable n8n and Make templates, Zapier recipe; quote, create, wait by webhook or polling): https://reelsbuilder.ai/docs/api/workflows
Agent quickstart (copy-paste prompt for Claude Code/Codex/Cursor; MCP server https://api.reelsbuilder.ai/api/v1/mcp with the same Bearer key): https://reelsbuilder.ai/docs/api/agent-quickstart

## Authentication
- Header: `Authorization: Bearer <key>` (exact case) or `X-API-Key: <key>`.
- Live keys start `rb_`; sandbox keys start `rb_test_`. Create a live key at https://reelsbuilder.ai/dashboard/v5/settings?tab=api. Request a sandbox key from admin@reelsbuilder.ai or your ReelsBuilder contact. Keep keys server-side.
- Sandbox: every Generate call returns a placeholder job and charges nothing; inputs are not validated. To validate (and price) with either key, send the real body with `"dry_run": true` (see Contract below).
- 401 `AUTH_REQUIRED` (no key) / `AUTH_INVALID_API_KEY`.

## Billing
- Prepaid USD balance (top up from $10) — the only charge; plan tokens are never used — for stock video ($0.60 per accepted job), YouTube clips ($0.40 per delivered clip), AI influencer (per engine x spoken length: $1.00/30s Avatar III on a persona, $2.45/30s Avatar IV, $7.25/30s Avatar V; persona creation $2.65 once), AI video (priced by model and length; the charge is in the `X-Api-Credits-Cost-Usd-Cents` header and in 402 bodies), blog to video ($0.50), posting ($0.05).
- YouTube clips: $0.40 per clip. clip_count (default 5) is charged up front; any requested clip not delivered is refunded automatically, and a failed job is refunded in full. dry_run free.
- Free: `GET /usage`, `GET /tools`, `GET /jobs/{id}`, trends and hooks.
- Failed jobs refund automatically. `GET /usage` returns the live balance and the same price table as `GET /tools`: `pricing.operations` holds flat prices and is `null` for request-priced tools (AI video, AI influencer), which are explained in `pricing.variable` and the catalogs; `dry_run: true` gives the exact price of one request.
- 402 `INSUFFICIENT_API_CREDITS` (not retryable — top up).

## Contract (all Generate endpoints)
Generate endpoints: POST /generate-and-post, /jobs (generation body), /blog-to-video, /videos/magic, /videos/text-to-video, /videos/image-to-video, /videos/ai-influencer, /clipper/youtube, /avatars, /batch.
- Dry run: add `"dry_run": true` to the body. Same validation and normalization as a real call (invalid -> the same 400), no job, no charge, no Idempotency-Key. 200 `{ "success": true, "data": { "dry_run": true, "valid": true, "mode", "normalized_request", "price": { "usd_cents", "display", "breakdown": [...] }, "estimated_duration_sec"?, "estimated_render_seconds": { "min", "typical"?, "max" }, "warnings": [], "charged": false } }`. For a presenter_broll video the estimate is measured (typically about 20 minutes, up to 46) and a warning says so. price.usd_cents is exactly what the real call charges. /batch takes a top-level dry_run (per-item results + total price). dry_run on other paid routes (e.g. /post) -> 400 `DRY_RUN_NOT_SUPPORTED`, nothing runs.
- Receipt (every queued Generate success, excluding dry runs/avatar records/batch envelopes): `data.job_id`, `data.video_id`, `data.status`, `data.poll_url`, plus `data.price_usd_cents` (charged) and `data.delivery` (private delivery) when known; header `X-Job-Id`. Top-level `jobId`/`videoId` are deprecated aliases. Always read `data.job_id`.
- Errors: `{ "success": false, "error": { "code", "message", "retryable", "details"? }, "meta": { "request_id" } }` (see Errors).
- Canonical field names (aliases still accepted; dry-run warnings flag them): `captions` (include_captions), `music` (include_music), `duration_sec` (duration_target_sec, duration), `aspect_ratio` (aspectRatio), `voice_id` (voice, voiceId), `webhook_url` (webhookUrl). The canonical name wins if both are sent.

## Idempotency
- `Idempotency-Key` header (8–256 chars) is REQUIRED on paid live calls (400 `IDEMPOTENCY_KEY_REQUIRED`), except dry runs. Use one key per creative. Dry runs are never cached, so the real call may reuse the key you planned.
- Same key + same body within 24h replays the stored response (header `x-idempotent-replay: true`), including error responses. Same key + different body -> 409 `RESOURCE_ALREADY_EXISTS`.
- Keys are scoped per account (live and sandbox share the cache). After fixing an error or topping up, send a NEW key.
- The same request twice (same account, endpoint and JSON body in any field order; the key does not matter) on /generate-and-post, /jobs, /blog-to-video, /videos/magic, /videos/text-to-video, /videos/image-to-video, /videos/ai-influencer (incl. variations), /clipper/youtube, /avatars and /avatars/{id}/looks: while the first is being handled the second waits up to 10 s for its answer, else 409 `REQUEST_IN_PROGRESS` (retryable, `Retry-After: 5`, nothing created or charged, never stored under the key); for 5 minutes after the first was accepted the second gets the first answer (`x-idempotent-replay: true`, `x-idempotent-replay-match: body`, body `meta.replayed: true`, `meta.replay_reason: "identical_request"`; a key replay sends `x-idempotent-replay-match: key`). A rejected first request, or one whose jobs have all failed, does not block the next attempt. To repeat on purpose inside 5 minutes: change the body or send header `X-Repeat-Token: <any value up to 128 chars>`. Two identical items of one /batch are two jobs. /post, /articles and /schedules rely on the key alone. With `series_id`, once the first request was accepted the same topic with a different key is refused earlier with a free 409 `SERIES_TOPIC_REPEATED` (not charged); resend with the key that created it to get the first answer.
- Client timeout: set it to at least 120 s on paid POSTs (most answer in a few seconds; persona creation can take longer). A client timeout does not cancel the request (the work continues and is charged once). The answer is stored when it is produced, even if the client has disconnected: resend the identical body with the SAME key to read it.

## Video input map (what each field controls; maturity 0-10)
Every video = one visual source + independent layers: URL inputs, niche (clip_type), purpose (content_purpose), script (prompt/topic or verbatim script), voice (voice_id/voice_gender/speed), captions, music, brand (brand object), format (aspect_ratio, duration, resolution, quality_tier/model, clip_count), delivery (delivery.visibility private, webhook_url).
- YouTube clips (maturity 8): url, purpose, captions, aspect_ratio, clip_count, private, webhook. No brand yet.
- Stock video (7): brand, niche (clip_type; default from brand.industry, else business), purpose, script / prompt + cta + include_hook, voice_id/voice_gender/voice_speed/language, caption_style {preset, text_color, highlight_color, position, font_size}, caption_language, music {enabled, volume}, 9:16 only, private, webhook.
- AI video (6): prompt = visuals, image_url, quality_tier/model/resolution, optional script (narration + captions), voice, music (default on), native_audio, brand, private, webhook.
- AI influencer (6): image_url (Avatar IV) or avatar_id (persona via POST /avatars) + engine avatar_iii|avatar_iv|avatar_v, script, voice_id/voice_gender/speed, captions/music (default on), brand (logo + caption accent), aspect_ratio, private, webhook. format presenter_broll (layout creator: purpose-driven creator edit, presenter on hook/opinion/payoff/close only, default with 9:16; layout split: visuals top / presenter bottom / seam caption; or layout cutaway: stock B-roll cutaways) accepts topic (+ duration_sec, content_purpose) instead of script.
- Not yet verified for partners: /videos/magic (5), /videos/motivational (5), /blog-to-video (4), /videos/news (3). Other consumer modes return 410.
Full table: https://reelsbuilder.ai/docs/api/video-matrix

## Generate: stock video
POST /generate-and-post  (renders only; never publishes despite the path name)
Body: one of `prompt` | `topic` | `script` (required; `script` is narrated word for word — size it ≈25 words/15s, 65/30s, 100/45s, 135/60s; never cut — a longer script makes a longer video, up to 120s, same price); `content_purpose` (education|sales|awareness|product_demo|case_study|news|entertainment); `duration_sec` 15–120 (default 30; aliases duration_target_sec, duration); `aspect_ratio` 9:16 only (other values 400); `clip_type` footage niche, also picks the music ("tech" for B2B technology, "business" for office/teams; tech/ai, tech/cyber etc. accepted and currently use the same technology library; when omitted: inferred from brand.industry/keywords, then the brief, then the account brand industry, else "business"; unknown -> 400 listing valid values); `content_style` professional (default)|educational|inspirational|energetic|calm|entertaining; `include_branding` (default true; false removes the ReelsBuilder watermark on paid plans); `webhook_url` (https; one signed callback on completion); `agent_run_id`.
Render settings (all optional; every one has a default; invalid -> 400 `INPUT_INVALID`, nothing charged; precedence request > brand colors > account brand > defaults):
- `caption_style` { `preset` highlight (default; 1-3 word chunks, spoken word coloured) | classic (bold uppercase phrases, no word colour) | none; `text_color` #RRGGBB (default #FFFFFF); `highlight_color` #RRGGBB (highlight preset only; default brand.accent_color, else account brand, else #FFD60A); `position` lower_third (default) | center | top; `font_size` small | medium | large (default) }. `captions` false (alias include_captions) = preset none.
- `music` true/false, or { `enabled` (default true), `volume` 0-1 (default 0.35) }; aliases `include_music`, `music_volume` (`music` wins).
- `voice_id` (ElevenLabs id; wins) | `voice_gender` female|male (default voice pick); `voice_speed` 0.7-1.3 (default 1.0; generated scripts are sized for it; narration that still overruns the length is sped up, max 1.35x).
- `language` ISO 639-1 narration+script language (default detected; no voice for it -> 400); `caption_language` (translated captions; default = language).
- `cta` { `text` ≤120 } appended verbatim as the last line of a GENERATED script; ignored when `script` is sent. Without cta, generated scripts close by naming brand.website_url (or the account website) when set.
- `include_hook` true = researched hook as the opening line (English, generated scripts only); false = no hook line; default natural opening.
Optional `brand` object (per-video brand; omit to use the account brand; when sent it REPLACES the account brand, nothing is merged): { `name` ≤80, `website_url` https, `logo_url` public https image (bottom-right watermark), `primary_color` / `accent_color` #RRGGBB, `tone` professional|casual|energetic|bold|calm|analytical, `industry` ≤80, `keywords` ≤10 }. At least one field; unknown fields or bad values -> 400. Pair with `include_branding: false` so only the customer's logo shows.
On this endpoint only (POST /generate-and-post), do not send `brand_id` (422 `CUSTOMER_CONTEXT_UNAVAILABLE`); pass the brand inline as `brand`. Saved brand kits (`brand_id`, see POST /b2b/brands below) are for POST /videos/ai-influencer. Send calls to action as `cta` (or end your own `script` with one).
Response 202: the standard receipt `{ "success": true, "data": { "job_id", "video_id", "status": "queued", "poll_url", "price_usd_cents", ... } }`.
Concurrency: up to 5 stock renders in flight per account (429 `RENDER_CONCURRENCY_LIMIT_EXCEEDED`).

## Generate: YouTube clips
POST /clipper/youtube — send exactly one of:
- `youtube_url`: one video (watch?v=ID, youtu.be/ID, shorts/ID, live/ID).
- `channel_url`: autoclipper — clips the channel's newest upload this account has not clipped yet (youtube.com/@handle, /channel/UC…, /c/name, /user/name). Call daily per channel.
Other fields: `clip_count` 1–10 (default 5; a maximum), `aspect_ratio` (default 9:16), `captions` (default true), `focus` (≤250 chars), `content_purpose`, `selection` newest_first|oldest_first (channel only; ~15 most recent uploads considered), `dry_run` (validate, price and report the selected source; no job, no charge). Unlisted fields are ignored; invalid values -> 400 `INPUT_INVALID`.
Response 200: `{ "success": true, "data": { "job_id", "video_id", "status": "queued", "poll_url", "price_usd_cents", "requested_clip_count", "source_video"?: { "youtube_video_id", "title", "url" } } }`.
Channel outcomes (never charged): 409 `NO_NEW_CHANNEL_VIDEOS` (nothing new), 400 `CHANNEL_NOT_FOUND`, 502 `CHANNEL_UNAVAILABLE` (retryable).
Optional `webhook_url` (https) — signed callback on completion, as on stock, blog, Magic and AI video/influencer.

## Generate: AI video
POST /videos/text-to-video  { `prompt`, `quality_tier` low|medium|high|xhigh|max OR `model`, `duration_sec` 30 (default) | 60, `aspect_ratio`, `resolution`, `music`, `native_audio`, `script`, `content_purpose`, `voice_id`, `voice_gender`, `captions`, `brand` }
POST /videos/image-to-video { `image_url`, `prompt` (motion), same options }
Finishing (both endpoints): `prompt` = what is SHOWN (never spoken); `script` = what is SAID, narrated verbatim by ElevenLabs (≈2.4 words/s: ~72 words for 30s, ~144 for 60s; slightly over budget adds one extra shot, charged; far over -> free 400 `INPUT_INVALID` stating the word budget; max 2000 chars). No script + `content_purpose` -> we write the narration from the prompt (brand name/website woven in). `voice_id` (ElevenLabs) or `voice_gender` female (default)|male. `captions` (default true when narrated): word-highlight captions. `brand` (top level; same object as stock: name, website_url, logo_url -> bottom-right watermark, primary_color/accent_color -> caption accent, tone, industry, keywords). Narration adds TTS+transcription cost (`prices_narrated` in GET /tools).
Soundtrack: `music` (boolean, default true) mixes a licensed instrumental bed under the video; false = no music, no price change. `native_audio` (boolean) keeps the model's own generated soundtrack: default false on kling-3.0-standard / kling-3.0-pro (medium/high tiers; true costs more, see `prices_native_audio` in GET /tools), default true on other models; the music bed ducks under it. Both false -> silent audio track (stream kept for platform compatibility).
Tiers: low=wan-3.0 (720p), medium=kling-3.0-standard, high=kling-3.0-pro, xhigh=bytedance/seedance-2.0, max=google/veo-3.1. Prices per tier/model at 30s and 60s: GET /tools -> pricing.ai_video.
Models: bytedance/seedance-2.0 (default), google/veo-3.1, kling-3-t2v, kling-3.0-standard, kling-3.0-pro, wan-3.0, fal-ai/wan/v2.7, fal-ai/ltx-2.3, fal-ai/pixverse/v6. Unknown model ids fall back to the default (check the `X-Api-Credits-Model` header).
Validation (prompt ≤1000 chars, resolution, duration, model, images, script budget) runs before billing: invalid -> free 400 `INPUT_INVALID`.
Response 200: the standard receipt `{ "success": true, "data": { "job_id", "video_id", "status": "queued", "poll_url", "price_usd_cents" } }`. Poll data.poll_url.

## Generate: AI influencer (talking head)
Engine choice, OpenRouter-style: we set the default, you may pick any engine, every engine has a published price (GET /tools -> pricing.ai_influencer).
Personas (reusable presenters; private to your account):
- POST /avatars { `image_url` (required; public https JPG/PNG of one person), `name` } -> 201 `{ "success": true, "data": { "avatar_id", "name", "status": "ready", "supported_engines": [...] } }` or 202 with status "processing" + `poll_url`. $2.65 once (provider cost x 2); Idempotency-Key required; refunded if creation fails; image passes a safety check (422 when blocked).
- GET /avatars (free) -> `data.avatars[]`; GET /avatars/{avatar_id} (free; poll until status "ready"); DELETE /avatars/{avatar_id} (free).
- Another account's avatar_id -> 404 `AVATAR_NOT_FOUND`.
POST /videos/ai-influencer { EITHER `avatar_id` (a ready persona) OR `image_url` (public https photo of one person, face visible), `engine` avatar_iii|avatar_iv|avatar_v (avatar_id default avatar_iii; image_url renders on avatar_iv only — any other engine with image_url -> 400), `script` (required; spoken verbatim; ≈75 words/30s, 150/60s; max 120s of speech), `voice_id` (ElevenLabs; if omitted the default voice matches the presenter), `voice_gender` female|male (optional hint), `speed` 0.5–1.5, `aspect_ratio` 9:16 (default)|16:9|1:1 }. Unlisted fields are ignored.
Engines: avatar_iii = best lip sync, cheapest, slower (~6.5 min render in our tests), personas only; avatar_iv = more expressive (~3.5 min), the only engine for image_url; avatar_v = premium/experimental, only if the persona's supported_engines lists it (provisional price).
Prices (x2 provider cost, rounded up to 5c, 5s minimum): avatar_iii $1.00/30s, $2.00/60s; avatar_iv $2.45/30s, $4.90/60s; avatar_v $7.25/30s, $14.50/60s. Headers: `X-Api-Credits-Cost-Usd-Cents`, `X-Api-Credits-Engine`.
Free errors: 400 `INPUT_INVALID`, 404 `AVATAR_NOT_FOUND`, 409 `AVATAR_NOT_READY` (retryable), 400 `ENGINE_NOT_SUPPORTED`. Response 200: the standard receipt (`data.job_id`, `data.poll_url`, `data.price_usd_cents`). Poll data.poll_url; failed jobs refund.
Presenter + B-roll (`format: "presenter_broll"`; default `talking_head`): the organic AI-explainer edit (for organic reach, not ads). Three layouts (`layout`, same price). `creator` (default with 9:16 or no aspect_ratio; 9:16 only): 1080x1920 creator edit planned as a storyboard (every shot has a purpose and rationale); the presenter, cropped around the face with 100/118/130% punch-ins, only on the hook, opinion/payoff lines and the close (<= 2.5 s at a time, <= 35% of the runtime); full-screen stock footage and motion graphics elsewhere; no clip over 4 s except one earned culmination hold (<= 6 s); varied clip lengths and transitions; cuts on word boundaries (J-cuts); 1-3 word captions in the safe zone; subtle SFX; the voice cast to the presenter's face (an avatar keeps it). metadata.storyboard and metadata.qa come with the video; it falls back to split, then cutaway, then the talking head. `split` (9:16 only): 1080x1920; the top half shows a visual for what is being said, changing every 1.5-4 s for the whole video; the bottom half is the presenter, always visible, a fixed chest-up crop around the face; a caption on the seam; the logo small at top right. `cutaway` (default for 16:9 / 1:1): the presenter full frame; explanatory lines cut to full-screen stock B-roll while the voice continues; the hook and the last sentence stay on the presenter; never more than ~8 s of B-roll in a row. `caption_style`: `seam` (default for creator and split: one 1-3 word phrase at a time, white bold text in a rounded dark box on the seam, the key word in brand.accent_color) or `karaoke` (default for cutaway: word-highlight captions). `broll.visuals`: `stock` (default: motion graphics rendered locally in the brand accent showing the subject of each line (message thread, notification, inbox, number, steps, comparison, quote), real stock footage where a real-world scene helps, headline cards; no AI images), `generated` (the same graphics, AI scene stills for the footage moments, at most ~1 shot in 4) or `mixed` (graphics + real footage, AI stills only when no footage fits); generated/mixed are opt-in: AI stills are produced by a third-party image model (Higgsfield); prompts derived from your script are sent to it (brand name, links and logo are never sent). Stock footage comes from licensed free-stock libraries (Pixabay, Coverr: free commercial use, no attribution required) and our own library; people in stock footage are never presented as endorsing you. A split that cannot be built falls back to cutaway, then to the talking head; metadata.presenterBroll.layout says what was delivered and each split shot has a source (ui | stock | generated | card), the planned visual type and, for footage, its provider. Send exactly one of `script` (verbatim) or `topic` (≤500 chars; we write a hook-first, one-idea script with concrete examples and no sales pitch or "link in bio" CTA; brand.name at most once; returned as artifacts.script). `duration_sec` 15–90 (default 30; topic only; a target for the speech, not an exact file length: the script can run a few seconds short or long and an opening card and the end card, about 3 s each, can be added; requests for 45 s have delivered 39–51 s files; read artifacts.durationSec), `content_purpose` (default education; topic only), `broll` { `visuals`, `coverage` 0.2–0.8 (default 0.5; cutaway only), `footage_keywords`[], `exclude_keywords`[] (≤32 each), `clip_type` (default inferred from brand/topic, else business) }. Price: unchanged — with script as above; with topic, the engine's price for duration_sec seconds of speech (dry_run quotes both). B-roll is best effort: on any timing/footage failure the talking head is delivered (captions/music/logo, same price) and artifacts.presenterBroll = { applied: false, reason }; on success { applied: true, shots: [{ start, end, clipId, query }] }.
Example: POST /videos/ai-influencer { "avatar_id": "6703...", "format": "presenter_broll", "topic": "Why most budgets fail in week two", "duration_sec": 45, "broll": { "coverage": 0.5 } } -> priced as 45 s on avatar_iii ($1.50).
`presenter_video_url` (your own talking-head recording instead of avatar_id/image_url, presenter_broll only) is NOT enabled yet: it returns a free 400 until it has a published price.
Creator layout options (presenter_broll with `layout: "creator"`; all optional, same price; checked against production 2026-10-11):
- `presenter_placement` bottom_left (default) | bottom_center: where the cut-out presenter (background removed) sits over the full-frame B-roll and graphics. bottom_left: a smaller presenter, about 30-34% of the frame height, anchored bottom-left with the body running off the bottom edge; 1-2 word captions at chest height beside the presenter; graphics use the full width above the head. bottom_center: centred, chest-up, 35-42% of the frame height, captions on the chest below the face. Another value, or sending it with split / cutaway, is a free 400 `INPUT_INVALID`.
- `host` { type: "avatar" } (default) or { type: "logo", logo_url?, style? cube|card|auto, show_url? (default true), url_text? ≤40 chars }: a faceless edit where the brand's logo is the animated host (cube: a rotating 3D cube with the logo on its faces; card: a glassy 3D card for wide wordmarks; auto picks by logo shape). Do not send avatar_id, image_url or engine with a logo host (free 400). Narration is ElevenLabs: voice_id, else the brand kit's default presenter voice, else voice_gender's default. logo_url defaults to the brand logo (brand_id kit or brand.logo_url); with neither it is a free 422 `LOGO_HOST_NO_LOGO`. The URL line under the host reads url_text, default the brand website's domain. A 30 s logo-host video is $1.00 (`X-Api-Credits-Engine: logo_host`).
- `outro` (default true) and `outro_cta` (≤40 chars): after the last spoken word the video closes on a 2.5-3 s animated brand end card (logo, website domain, CTA line, music-led). Needs a logo (brand.logo_url, a brand_id kit logo or host.logo_url). outro_cta sets the CTA line; default: the brand kit's profile.cta, else the script's closing line shortened to 6 words or fewer. `outro: false` ends on the last shot; outro false together with outro_cta is a free 400. Recorded in metadata.presenterBroll.outro.
- `progress_bar` true | { enabled, position top|bottom (default bottom), color #RRGGBB (default the brand accent) }; alias `progress_bar_enabled`. Off by default: a thin bar that fills left to right over the video. presenter_broll only (free 400 on talking_head).
Example (logo host): POST /videos/ai-influencer { "format": "presenter_broll", "brand_id": "acme-logistics", "host": { "type": "logo", "style": "cube", "url_text": "acme.example" }, "topic": "Why carrier updates arrive late", "duration_sec": 30, "outro_cta": "See the 2-minute demo", "progress_bar": { "enabled": true, "position": "bottom" } }

## Client series: profiles, funnel stages and variations (presenter_broll)
For agencies running one recurring, on-brand series per client. All free to configure; videos are priced exactly as single presenter_broll videos.
1. Client profile on the brand kit (POST/PATCH /b2b/brands, field `profile`, optional, private to your account): `industry`, `icp` { roles[], company_size?, regions?[], pains[] }, `offer` (one line), `tone` authoritative|friendly|provocative|technical|playful, `proof_points` [{ claim, source_url? }] (the ONLY numbers or research claims a script may state; anything else is rewritten or dropped, even when the topic invites a statistic), `banned_topics`[], `cta` { text, url? }, `default_presenter` { avatar_id, voice_id?, engine? } (used when a request names no presenter), `languages`[], `topic_pool`[] (video topics; filled from the website on auto-built kits, see 8). PATCH merges profile fields; null clears one; `profile: null` clears all.
2. Funnel inputs on POST /videos/ai-influencer (format presenter_broll + topic): `content_stage` awareness|consideration|decision (default awareness) and `content_type` (default educational). awareness: educational, infographic, statistic, checklist, hot_take, summary. consideration: comparison, how_to, case_study, insight, myth_vs_fact. decision: alternatives, review, versus, demo, objection, pricing_explainer. A type outside its stage is a free 400. They pick the hook, the body pattern, the last line (soft follow for awareness, a no-pressure next step for consideration, the kit's `cta.text` said directly for decision) and the graphic templates (stat, steps/checklist, icon panel, comparison/vs, UI walkthrough, notification, quote).
3. `series_id` (your own id; needs `brand_id`): the first video fixes the presenter, voice, engine, hook style and palette; later videos reuse them. A different explicit avatar_id/voice_id/engine is a free 409 `SERIES_CONFLICT`; the exact same topic again is a free 409 `SERIES_TOPIC_REPEATED` (an idempotent retry still replays). Covered topics are passed to the script writer so episodes do not repeat each other.
4. `variations` { count 1-5, vary?: ["hook","angle","content_type","presenter"] (default ["hook","angle"]), presenters?: [avatar_id, ...] }: one request writes N distinct scripts from the same topic (Claude scores every pair; a too-similar variant is rewritten) and queues N normal jobs. Price: N x the single-video price (`dry_run: true` quotes the total, writes nothing). `Idempotency-Key` required: it covers the whole set (items use <key>:v<index>; resending the same request replays the same scripts and jobs). The request records the set and its jobs and answers in a few seconds; the scripts are written and checked right after, so the first answer has `distinctness.status: "pending"` (resend with the same key to read the verdict: no new jobs, no charge). An identical request with any key inside 5 minutes returns the first set. The response lists every job in `data.job_ids`; poll each one. vary "presenter" cannot be combined with series_id.
5. `mode` / `submode` (aliases of content_stage / content_type, presenter_broll): mode awareness|consideration|decision, plus conversion / bottom / bofu = decision, middle / mofu = consideration, education / top / tofu / marketing_funnel_top = awareness (`content_stage` accepts the same stage words). submode is any content_type or a natural word for it, resolved within the stage: objections -> objection, vs / comparison -> comparison (consideration) or versus (decision), how-to -> how_to, myths -> myth_vs_fact, pricing -> pricing_explainer, stats -> statistic, recap -> summary. Free 422s naming the valid values: MODE_UNKNOWN / STAGE_UNKNOWN, MODE_CONFLICT (mode and content_stage disagree), SUBMODE_UNKNOWN (not a type of that stage), SUBMODE_CONFLICT (submode and content_type disagree).
6. `instructions` (alias `custom_ai_video_instructions`; presenter_broll; 1-1000 characters): a free-text steer for this video (angle, hook, tone, emphasis). It reaches the script writer, the shot planner and the plan review as a delimited, lower-priority block: it can NOT override the kit's banned_topics, content moderation, the no-third-party-trademark rule, the QA gates, the closing call-to-action line, or the rule that numbers come only from proof points / your topic. With a supplied script it steers the visuals only. Moderated before billing (free 422 `content_policy_violation`); both fields with different text is a free 422 `INSTRUCTIONS_CONFLICT`. Every variations item gets the same instructions. Video metadata keeps only its length and SHA-256.
7. `captions` also accepts { enabled, style?, position? }: style karaoke | seam | kinetic_bold (= karaoke) sets caption_style; position is ignored. On layout creator every style burns the creator edit's own captions (1-2 words, the spoken word highlighted), placed clear of its graphics. An unknown style (422 CAPTION_STYLE_UNKNOWN) or one that disagrees with caption_style (422 CAPTION_STYLE_CONFLICT) is refused, never dropped.
8. Website -> brand kit (free): POST /b2b/brands with only `website_url` (a domain or http(s) URL; + optional `customer_id`, `brand_id`) crawls the site (home + about / pricing / customers or case studies + one blog post; same domain only, at most 6 pages, ~40 s budget) and builds the kit: name, logo (header logo, apple-touch-icon, og:image or favicon, normalized), colours (site theme / CSS colours that agree with the logo), Google font, and the client profile (industry, icp, offer, tone, cta, `topic_pool` of 8-12 topics, suggested banned_topics, and `proof_points` quoted verbatim from the crawled pages with their `source_url`; a claim not found on a page is dropped). `brand_id` defaults to the domain slug (exampleclient.net -> `exampleclient-net`). Response 201 with `source: "auto"`, `domain`, `crawled_at`, `fields_auto`, `locked_fields`, `authorized_by` / `authorized_at`, `autogen` (pages, logo/colour source, socials, warnings). A kit for that domain already on the account is returned instead (200, `reused: true`, no crawl) unless `refresh: true`. Instead of `brand_id`, a generation request (/videos/ai-influencer, /generate-and-post, /jobs, /videos/text-to-video, /videos/image-to-video) may send `website_url`: the account's kit for the domain is reused or built before any charge (headers `X-Brand-Kit-Id`, `X-Brand-Kit-Source` built|reused, `X-Brand-Kit-Stale`); `brand_id` wins when both are sent; a dry run never crawls. PATCH locks every field you edit (`locked_fields`); `PATCH { "refresh": true }` re-crawls and never overwrites a locked field (`unlock_fields` releases one). Kits crawled over 30 days ago are `stale: true` (still used; refresh on request). Free errors: 422 `BRAND_AUTOGEN_FAILED` with `reason` (SITE_BLOCKED, SITE_UNREACHABLE, SITE_HTTP_ERROR, SITE_REDIRECTS_OFFSITE, NOT_HTML, NO_CONTENT, TIMEOUT), 422 `BRAND_CONFIRMATION_REQUIRED` for well-known brand domains (send `confirm_authorized: true`: you confirm the account is authorised to market that brand). Kit creation and refreshes are audit-logged per account.
Example (website only): POST /b2b/brands { "website_url": "https://exampleclient.net", "customer_id": "exampleclient" } -> 201 { data: { brand_id: "exampleclient-net", source: "auto", domain: "exampleclient.net", crawled_at, fields_auto: ["name","logo_url","primary_color","profile.industry",...], profile: { proof_points: [{ claim, source_url }], topic_pool: [...] }, ... } }; then POST /videos/ai-influencer { "website_url": "exampleclient.net", "format": "presenter_broll", "topic": "..." } reuses it.
Example (alias request): POST /videos/ai-influencer { "brand_id": "acme-logistics", "format": "presenter_broll", "topic": "Why carrier updates arrive late", "mode": "conversion", "submode": "objections", "custom_ai_video_instructions": "Open on the dispatcher's frustration; keep it warm, not salesy.", "captions": { "enabled": true, "style": "kinetic_bold" } } -> the same as content_stage "decision", content_type "objection", instructions, captions true + caption_style "karaoke".
Example (set up a client once): POST /b2b/brands { "brand_id": "acme-logistics", "customer_id": "acme", "name": "Acme Logistics", "logo_url": "https://cdn.acme.example/logo.png", "profile": { "industry": "B2B freight software", "icp": { "roles": ["Head of Operations"], "company_size": "50-500 employees", "pains": ["manual load planning", "late carrier updates"] }, "offer": "Load planning that books carriers automatically", "tone": "authoritative", "proof_points": [{ "claim": "Customers cut planning time by 40% in the first quarter" }], "banned_topics": ["layoffs"], "cta": { "text": "Book a 15-minute route audit", "url": "https://acme.example/audit" }, "default_presenter": { "avatar_id": "6703..." } } }
Example (one decision-stage episode): POST /videos/ai-influencer { "brand_id": "acme-logistics", "series_id": "acme-weekly", "format": "presenter_broll", "topic": "Spreadsheets vs automated load planning", "content_stage": "decision", "content_type": "versus", "duration_sec": 45 } -> { jobId }
Example (3 variations, one call): POST /videos/ai-influencer (Idempotency-Key: acme-wk12) { "brand_id": "acme-logistics", "format": "presenter_broll", "topic": "Why carrier updates arrive late", "content_stage": "awareness", "content_type": "statistic", "variations": { "count": 3, "vary": ["hook", "angle"] } } -> 200 { data: { variation_set_id, job_ids: ["...", "...", "..."], variants: [{ index, job_id, hook_style, angle, content_type, max_similarity }], distinctness: { status, method } } }; poll each job id as usual.

## Jobs (all Generate products)
GET /jobs/{job_id} -> `data.job` = { id, status, progress[], artifacts { videoId, videoUrl, videoUrls[], thumbnailUrl, title, description (incl. hashtags), script, durationSec, clips[]? { videoUrl, title, transcript, viralScore, durationSec } }, error, attempts, createdAt, updatedAt }. Use title/description as ready-to-post copy.
Statuses: queued -> leased/running -> uploading -> completed. Final: completed, failed, dead_lettered, cancelled.
On failure: `error` = { code, category, message, retryable }; charges are refunded; resubmit with a NEW idempotency key only if retryable.
Poll every 10–15 s (reads limited to 60/min per key). Typical: stock 30s ≈ 3–6 min, clips ≈ 3–7 min for short sources, AI video 30s ≈ 6–8 min, AI influencer talking head ≈ 4–7 min, AI influencer presenter_broll (30–45s) ≈ 15–25 min for one video and 35–45 min each when two or more run at once (measured October 2026: half within 20 min, 9 in 10 within 39, slowest 46). For presenter_broll prefer `webhook_url` over waiting on a poll loop.Cancel: POST /jobs/{job_id}/cancel (409 `JOBS_CANCEL_NOT_ALLOWED` once finishing).
Copy finished files to your own storage; CDN files have a retention window.
Sandbox polls return the same `data.job` shape (already completed, example artifacts).
Private delivery: add `"delivery": { "visibility": "private" }` on POST /generate-and-post, /jobs, /clipper/youtube, /videos/text-to-video, /videos/image-to-video or /videos/ai-influencer (other routes return 422 `PRIVATE_DELIVERY_UNAVAILABLE`). Files are kept in a private bucket; job artifacts then hold signed links that expire after 24h (`artifacts.delivery.expires_at`). Poll again for a fresh link. Webhooks carry no link. GET /b2b/library/videos returns the same links in `media_access`.

## Post
POST /post { `video_id` (a ReelsBuilder video), `social_account_id` | `platform` (youtube|instagram|tiktok|facebook|x), `title`, `caption`, `description`, `hashtags`, `privacy_status` } -> synchronous result. $0.05. Accounts are connected in the ReelsBuilder dashboard; list them with GET /social-accounts.

## Signals (free)
GET /trending?woeid=&category=&limit= (≤50) -> `data.trends[]` { name, tweet_count, category, score }.
GET /x-hooks?topic=&limit= -> `data.hooks[]` { hook, format, score, … }.

## Batch
POST /batch { "jobs": [ { "endpoint": "generate-and-post" | "clipper/youtube" | "videos/text-to-video" | "videos/image-to-video" | "videos/ai-influencer", "body": { ...that endpoint's body }, "idempotency_key"?: string } ] } — 1-25 jobs. Header `Idempotency-Key` required (item keys default to <key>:<index>) unless `"dry_run": true` is sent at the top level (validates and prices every item, queues nothing). Response 200 { data: { accepted, rejected, results: [ { index, endpoint, ok, http_status, job_id, idempotency_key, response } ] } }. Items are processed by their normal endpoint (same prices, validation, refunds). Re-sending the same batch is safe; resend rejected items with a new key.

## Webhooks
Stock, YouTube clips, blog, Magic and AI video/influencer requests may include `webhook_url` (https, public host). On completed/failed/dead_lettered we POST { event, job_id, status, video_id, video_url, video_urls, title, description, duration_sec, error, agent_run_id, poll_url, timestamp } with header `X-ReelsBuilder-Signature: t=<unix>,v1=<hex HMAC-SHA256(secret, "<t>.<raw body>")>`. Get the per-account secret from GET /webhook-secret (free). Reject timestamps older than 300s. 3 attempts (0s, 30s, 120s); handlers must be idempotent by job_id.

## Errors
Envelope on every Generate endpoint: `{ "success": false, "error": { "code", "message", "retryable" (boolean), "details"? }, "meta": { "request_id" } }`; request id also in the `X-Request-Id` header. Validation errors are always 400 `INPUT_INVALID` (never 500, never charged). A top-level `message` may also appear (deprecated alias). A few older non-Generate routes (e.g. schedules) can still return `{ "success": false, "message", "error": "<text>" }` — fall back to `message` there.
Retry only when `retryable` is true. 429 responses include `Retry-After`.

## Video input controls
The live catalog on October 7 advertises stock footage_keywords/exclude_keywords (up to 32 nonempty strings, at most 100 characters each; trimmed, lowercased and deduplicated) and influencer boolean captions/music (default true) plus per-request brand for logo/caption accent only (the supplied script stays verbatim). Inspect actual tool schemas before sending them; unknown influencer fields may be ignored. Stock positives are search hints; exclusions are literal clip-metadata filters before ranking and library fallback, not a visual guarantee, and preserve caller assets. Exclusions match singular/plural forms of whole words (robots excludes robot; news does not exclude new); inspect selections. Influencer finishing may return the original provider video on failure; inspect returned influencerFinishing metadata where available and actual captions/music/logo before publishing. A completed job is not overlay or quality acceptance. No new avatar-consent or directory-eligibility claim.

## Not available yet
Hosted ChatGPT/Claude OAuth account connection is being prepared and is not live. Current MCP clients must attach the account's API key from a server-side secret store. A copied prompt alone grants no connectivity or permission. MCP publishing is unavailable; use the separate REST publishing operation. Discover the actual MCP tool allowlist with tools/list; clip_youtube supports either youtube_url or channel_url. Use clip_youtube with dry_run:true for a free preview (no key needed); paid clip_youtube calls require idempotency_key, which MCP forwards as the Idempotency-Key header. Private delivery and mode limits must be checked against the account and current contracts; preview validation is not proof of a completed private render.
