---
name: hubfluencer-create
description: Generate a finished, post-ready video ad, short, image carousel, or tracking-overlay clip with Hubfluencer — and run the surrounding content operation (plan → draft → review → schedule → measure). Use when the user asks to make an ad, a short, a carousel, or a social clip, to turn a product/idea into a video, or to "turn my product page into a campaign" / "plan a month of ads". Drives Hubfluencer end-to-end via the @hubfluencer/mcp tools or the REST API.
---

# Make videos with Hubfluencer

Hubfluencer turns a text prompt into a finished, post-ready MP4 (AI footage, voice-over, music, captions). You drive it via the `@hubfluencer/mcp` tools or, if MCP is not wired up, directly against the REST API with `bash` + `curl`.

## Creative export contract

- Production prompts (`product_prompt`, `scenario_prompt`, and scene `prompt`) are visual directions. Narration scripts are read aloud verbatim, so `set_narration_script.script` must contain only the exact spoken words—never camera notes, prompt syntax, labels, or scene directions. Never copy a creative brief into `script`; use `generate_narration` or Autopilot unless the user supplied final voice-over copy.
- Do not rely on the video model to spell essential brand names, UI, signs, prices, offers, or CTAs. Put exact copy in Hubfluencer overlays, narration, or supplied ending artwork.
- A short logo position is a preferred corner: the renderer moves it away from headline, captions, CTA, offer, and badge areas. Omit `treatment` so transparent marks default to `throughout` and opaque square lockups default to `end_card`. Use a tight transparent wordmark; use square or full-bleed art as the poster instead. If poster artwork already has its logo/CTA, set `poster_includes_lockup: true` and do not add an `end_card` logo.
- Short posters fill the ending frame: phone-shaped portrait art is cover-cropped, while square, classic portrait, and landscape art is contained over a blurred backdrop. Editor closing images are always cover-cropped. Keep important content in the central 9:16 safe area. An Editor closing image gets no generic CTA, so put the CTA in the artwork or state it in the preceding narration/scene.
- Pass `segments_count` (3–10) to `create_editor_ad` when length matters. It is the exact structural target used by the cost quote and Autopilot launch.
- Each Editor AI scene is 8 seconds. Default a concise social ad to 3 scenes (about 24 seconds) unless the user asks for a longer story.

## Generation lifecycle authority

Every Editor and Short product read includes a non-null `generation_summary`. Treat it as the sole
lifecycle authority even when product projections such as `stage`, `autopilot_status`, `batch_generation_status`, child
statuses, or `latest_render` disagree. Poll only while
`generation_summary.timing.poll_after_ms` is a positive integer, at that server cadence; `null`
means stop polling. Read terminality from `generation_summary.status`, invoke only
`generation_summary.available_actions`, and download only when `download_result` is advertised for
a verified delivery artifact. Product projection fields are display metadata or explicit route fences only;
never derive polling, retry, cancel, or download availability from them.

When a stopped summary advertises `edit_required_input` or `review_spend_cap`, call
`provide_generation_input` with that action and its non-empty `input` object or approved
`max_credits`. For a failed `editor.scene.generate` or `editor.scene.regenerate` summary that
advertises `retry_generation`, call `retry_segment_generation { slug, segment_id }`. Both tools
re-read the product and echo the run/revision/step fences; never call them from product projection fields
or guess a fence. Re-read status after the command.

## Connect (one-time)

The user buys credits and creates an access token in the Hubfluencer app: **Settings -> Access tokens**, with the **"Generate videos"** scope (`video:generate` + `video:read`). The token is supplied to the MCP server via the `HUBFLUENCER_API_TOKEN` env var.

Alternatively, the user can connect without copy-pasting anything: run `npx -y @hubfluencer/mcp@0.18.0 login` (it prints a URL and a short code; the user approves the connection in the signed-in Hubfluencer app). A scoped token is saved to `~/.hubfluencer/credentials.json` and the MCP server picks it up automatically.

**Never invent a token. Never echo a token in your output.**

## Native macOS Studio

If the bearer-gated `hubfluencer-studio` localhost MCP server is connected, treat it as the authority
for the open local project. Read `get_project_digest`, pass the exact required `doc_rev` on every call,
and use only the tools it advertises. Generations are opt-in and remain approval-gated in the app; the
app also owns cloud-context disclosure, so never bypass that consent with a direct Director REST call.

## The happy path: one shot

The simplest path is the `make_video` tool — prompt in, finished MP4 out. It creates the project, starts generation, polls to completion, and downloads the file. Shorts charge atomically at launch; editor Autopilot authorizes `max_credits` and spends incrementally as work starts. Credits are real money (**$0.99 each** bought singly, less in bulk) — so unless the user already approved the spend, preview free with `make_video({ prompt, dry_run: true })`, quote the credits + ~dollar cost, and generate only after an explicit yes.

```
make_video({ prompt: "A 15s ad for a cold-brew coffee brand, energetic, gen-z", save_path: "./cold-brew-ad.mp4" })
```

- `kind` defaults to `auto`: a fast single-clip short for simple prompts, or a multi-scene editor ad for story / multi-scene prompts.
- Override with `kind: "short"` or `kind: "editor"` when the user is specific.
- Generation takes a few minutes. That is normal — do not assume failure.
- If `make_video` returns `terminal: false`, the render is still going. Call `wait_for_completion({ slug, kind })` again until it finishes. For paid shorts, report `segments_completed / segments_total` and `active_segment_positions` from status as the real clip progress.
- **Don't ship a bare short.** For a short, pass `headline` (the on-screen TITLE, ≈4 words / ≤40 chars), `subheadline` (secondary title), and a fitting `music_vibe` right in the `make_video` call — and **always `cta_text`** (a short without a CTA doesn't convert; add `offer_text` only if there's a real deal). Set `visual_language` explicitly (`kinetic_creator` is a strong default). These are SHORTS fields (`theme` applies to both kinds; `headline`/`subheadline`/`music_vibe` are ignored for editor). If copy is left blank the server auto-writes the headline + caption beats at generate time (`skip_auto_text: true` renders a deliberately bare clip). `make_video` cannot attach local files; use `create_editor_ad`'s asset paths for an editor, or the granular tools for a short. If no title/assets were given, infer a sensible `headline`/`subheadline` from the prompt, or ask.

## The granular path: control and recovery

Use this when you need more control, or to recover after an interruption:

1. `get_credits` — check the balance before spending.
2. `create_short` or `create_editor_ad` — create the draft (0 credits; `create_editor_ad` without `max_credits` also returns a free quote and does **not** start generation).
3. `generate_short` (shorts) or `start_autopilot { slug, max_credits }` / re-calling `create_editor_ad { ..., max_credits }` (editor ads) — start the render. A bare `start_autopilot { slug }` only re-quotes; pass `max_credits` ≥ the estimate to launch.
4. `wait_for_completion({ slug, kind })` — poll until terminal.
5. `download_result` — save the MP4.

Act on Editor batch recovery only when `generation_summary.available_actions` advertises
`retry_generation` or `cancel_generation`; then call `retry_editor_batch { slug }` or
`cancel_editor_batch { slug }` respectively. The MCP tools re-read the canonical summary before writing.
Raw REST retry callers send `generation_run_id` and `generation_revision` from that summary; cancel
callers send `generation_run_id`. Product batch fields are display projections, never command fences or
action availability. A stale 409 means re-read and issue a new command only if the action is still
advertised; never guess fences. Retry targets the exact failed canonical step. Only a scene named by
`current_step` may be repaired before retry. Resolve the child batch before starting Autopilot again;
retrying it does not authorize a new parent run. For an insufficient-credit blocker, top up or reduce
scope and follow the action in the newly read summary.

Cancellation is run-fenced. A paid short is not cancellable once `generate_short` starts, and its draft cannot be deleted until the run is terminal; a failure still refunds its 15 credits. `cancel_segment_generation { slug, segment_id, generation_claim_token }` uses the current scene token from `get_editor` and refunds an individually generated scene; `cancel_autopilot { slug }` re-reads and forwards the current summary run/start fence, stopping only that parent chain while keeping completed/charged work. Re-read after a stale fence error; never guess a token, run, or start time.

**Minimum sellable short** — check all seven before you spend:

- hook (`headline`) ≤4 words
- `text_beats` = hook → proof → payoff
- `cta_text` set (a short without a CTA doesn't convert)
- product image attached (`set_short_product`) AND described
- poster (`set_short_poster`) or closing lockup (`brand_name` / `closing_claim`) carries the ask
- `visual_language` set explicitly (`kinetic_creator` is a strong default)
- iterate: hooks are free to re-render (`update_short` → `rerender_short`, 0 credits after the first generate)

Iterate the hook before you spend: `generate_hook_variations` writes 3–6 hook options on the free assist quota. If `headline`/`subheadline`/`text_beats` are all blank, the server auto-writes the headline + caption beats at generate time (pass `skip_auto_text: true` to `generate_short` for a deliberately bare clip).

**Never re-roll — iteration is free after the first generate.** The first `generate_short` (15 credits) buys the footage + music; after that, every overlay edit re-renders over them for **0 credits**: `update_short` the fields, then `rerender_short { slug }` (REST: `POST /api/shorts/:slug/rerender`, no body, `Idempotency-Key` honored, rate limit 10/min).

- **Free to re-render:** `headline`, `subheadline`, `text_beats`, `cta_text`, `offer_text`, `badge_text`, `star_rating`, headline/subheadline/accent colors, `font_family`, `text_position`, `text_animation`, `closing_claim`, `brand_name`, poster (+ `poster_includes_lockup`), `end_card`, logo overlay (`set_short_logo` / `short_logo_position` / `short_logo_treatment`).
- **Needs `generate_short` again (15 credits):** `product_prompt`, product image, `creative_format`, `visual_language`, `theme`, `music_vibe`, `music_instruments`, `language`.

Edits that add/remove the end card (poster, `end_card`) can shift the video between 12s and 14s. The pinned music bed is reused: when it is shorter than the export it crossfades into a repeated continuation, then fades at the export end, without charging for regenerated music.

Iteration recipe: generate once → `update_short` a hook/CTA variant → `rerender_short` (free) → compare → repeat → keep the winner. **409** = a render is already running (keep polling); **422 `short_not_ready`** = nothing generated yet — run `generate_short` first; **422 `short_paid_regeneration_required`** = a footage/music input changed — run `generate_short` again. A re-render is never a credit error.

`update_short` returns a compact receipt (`updated`, `end_card_mode`, `resolved_cta_text`, `next`), not the complete Short. Use `get_status` or REST GET for authoritative state.

**Short creative fields** (`create_short` / `update_short`, also carried by `make_video`): `headline` (on-screen TITLE — ≈4 words; ≤40 chars hits hardest; hard cap 80), `subheadline` (secondary title, ≤200), `visual_language` (kinetic_creator / premium_editorial / cinematic_product / ugc_realism / startup_explainer / luxury_minimal — drives Veo direction AND the render's typographic identity), `creative_format` (problem_solution / mistake_fix / myth_vs_reality / before_after / proof_demo / product_reveal), `music_vibe` (Upbeat default / Cinematic / Minimal / Luxury / Playful / Jazz), `theme` (deprecated for shorts — legacy fallback when `visual_language` is unset), plus `text_position` (top/center/bottom), `text_animation` (**auto** default / reveal / typewriter / fade_in / pop / bounce / word_stagger / word_spotlight), `font_family` (**auto** default, or one of 15 ShortFont* faces) — both default to `auto`, which follows the visual_language's typography; every concrete value (incl. fade_in / ShortFontSpaceGrotesk) is an explicit pick that wins — `headline_color` / `subheadline_color` / `accent_color`, `text_beats` (≤8 caption lines shown one at a time), `music_instruments` (≤10, each ≤40 chars), and conversion graphics: **always set `cta_text`** (≤24, e.g. "Shop now" — blank `cta_text` → the server renders a neutral localized ask (e.g. "Learn more"); a truly CTA-less short is not currently supported), `offer_text` (≤16) only if there's a real deal, and `badge_text` (≤24) / `star_rating` (0–5) — opt-in, user-supplied only, never AI-written; **only ratings/badges you can substantiate — fabricated social proof is deceptive advertising**. End-card fields: `closing_claim` (≤80 — end-card claim, falls back to the headline), `brand_name` (≤40 — shown on the closing brand-lockup slate when no poster is set), `end_card` (`"auto"` default — poster or brand-lockup slate, 14s; `"none"` — no end card, 12s, loop-style). Populate at least the title/subtitle and a visual_language. `brand_profile_id` (from `list_brand_profiles`) on `create_short` seeds blank fields from a saved brand; `language` is a closed set — `en fr es de it nl pl pt ru zh ja ko ar` (no region subtags like `en-US`). (REST: send these in the `POST /api/shorts` body, mapping text/font fields to `short_text_position` / `short_text_animation` / `short_font_family`.)

Helpful extras: `get_status` (one-off status check), `list_voices` (pick a voice whose normalized `languages` contains the editor language), `list_projects` (see existing work; `include_completed: true` recovers a finished short's slug), `list_renders { slug }` (every editor render version plus thumbnail, duration, retryable/stale flags, snapshot hash, and presigned URL; REST: `GET /api/editor/:slug/renders`), `retry_render { slug, render_id }` (re-run a **failed** editor render from its saved snapshot, **0 credits**; REST: `POST /api/editor/:slug/renders/:render_id/retry`).

## Image carousels (sliders)

A **slider** is an image carousel (N still slides + a ready-to-post caption + hashtags), not a video — `wait_for_completion` / `download_result` do **not** apply; poll `get_slider` instead.

1. `create_slider { prompt, mode, slide_count? }` — one prompt → a carousel draft (0 credits). `mode` is `creative` (story-driven) or `ad_driven` (facts); `slide_count` 3..10. (REST: `POST /api/sliders`.)
2. `generate_slider { slug }` — render it (copy → AI backgrounds → composite). **1 credit per slide.** (REST: `POST /api/sliders/:slug/generate`.)
3. Poll `get_slider { slug }` until `completed`, then download each `slides[].image_url` and post them with the returned `caption` + `hashtags`. (REST: `GET /api/sliders/:slug`.)

Tweak a finished carousel without re-paying: `restyle_slider` (new template/accent/position — re-renders every slide, reusing the backgrounds) or `edit_slider_slide` (rewrite one slide's headline/body/kicker, re-composite just that slide). Both **0 credits**; poll `get_slider` again.

## Tracking-overlay videos (burn a CV overlay on your own clip)

`create_tracking_video { video_path, save_path?, classes?, color? }` — one shot: uploads a local clip, burns a computer-vision object-tracking overlay (convex-hull polygons hugging each moving subject + HUD brackets + telemetry), renders, polls, and downloads. Source clip **≤ 60s, ≤ 1080p**. Costs **1 credit** on render (refunded on failure). `classes` = COCO-80 class ids to track (e.g. `[0]` person, `[16]` dog; omit to track all); `color` = `[r,g,b]` or `[r,g,b,a]`, each 0..255 (omit for white).

- On `terminal: false` (slow upload / cold GPU), call the tool **again with the returned `slug`** to resume — it reuses the in-flight upload (no re-upload, no re-charge). Or poll `wait_for_completion({ slug, kind: "tracking" })` then `download_result`.
- A completed slug is **final** (its source clip is deleted on success), so you can't re-render it with a different overlay. For a variant (different `classes`/`color`), start a **new** project — omit `slug`, pass `video_path`.

## Granular control: author the full ad yourself (no autopilot)

When you want to direct every step — write the scenario, set the exact scene count, hand-write each scene's prompt and the narration — drive the editor step by step instead of autopilot. Autopilot (`create_editor_ad`) stays the default path (quote, then launch with `max_credits`); this is the controlled path, and it runs entirely agent-driven (no human approval step in the middle):

1. `create_editor_draft { product_prompt, language?, theme?, voice_id?, export_aspect_ratio?, project_intent? }` — creates an editor project **without** starting autopilot (0 credits). Returns a `slug`. (REST: `POST /api/editor`.)
2. `generate_scenario { slug, segments_count? }` — let the model draft a scenario (**1 AI assist**, `segments_count` 3..10, server default 5) — **or** `set_scenario { slug, scenario_prompt }` to write your own (free, 1..50000 chars).
3. `get_editor { slug, response_format: "concise" }` — **the REVIEW step.** Read back `scenario_prompt`, `segments[]`, `narration_script`/`narration_status`, `current_audio`, `current_music`, `latest_render`, `segments_count`, `ai_assist_quota`. Decide what to change before spending credits; omit the format only for the backward-compatible full payload.
4. `set_scene_count { slug, count }` — grow/shrink the storyboard (1..20). Each AI scene is a fixed **8s**, so this sets the ad's length (`count × 8s`); uploaded clips keep their own length. Only deletes **trailing un-generated** scenes; never a completed or in-progress one.
5. `set_segment_prompt { slug, segment_id, prompt }` per scene — hand-write each scene (free, 1..2000 chars).
6. `generate_segment { slug, segment_id }` one at a time, **or** `generate_all_segments { slug }` to run every pending scene **sequentially in position order** (each finishes before the next, so visual continuity carries forward). **5 credits per scene.** Stop on the first 402 and report.
7. `set_narration_script { slug, script }` (write your own, free, ≤10000 chars) **or** `generate_narration { slug }` (**1 AI assist**). AI narration requires every scene completed with a known positive duration and at least one scene without retained original audio; an exact retry deduplicates for free. Then write a music `description` (the ≤1200-char direction).
8. `generate_voice { slug, voice_id }` (**3 credits**; `voice_id` **required** — pick one from `list_voices`) and `generate_music { slug, description, mood?, genre?, tempo?, instruments? }` (**5 credits**). `instruments` is an array of up to 10 non-empty strings (≤80 chars each), never one comma-separated string.
9. `render { slug }` (**0 credits** — only auto-charges any still-ungenerated or previously failed scenes) → `wait_for_completion({ slug, kind: "editor" })` → `download_result`.

Optional AI helpers along the way (1 assist each): `enhance_prompt`, `suggest_next_scene`, `suggest_music_prompt`.

## Bring your own media (local uploads)

An **editor** project can use the user's **own local files** — video clips on the timeline, or a product / closing / logo image — all **0 credits**. A **short** can take a product image, an exact deterministic logo overlay, and an end-card poster — also all **0 credits**.

- **MCP:** `upload_video { slug, file_path, add_to_timeline? }` does the whole dance (presign → PUT → poll until processed → optionally append the clip). Then `add_segment_from_upload { slug, upload_id }` to append a ready clip, or `use_asset_for_segment { slug, segment_id, upload_id }` to set a specific scene (also takes `source_segment_id` to reuse a finished scene). Editor images: `set_product { slug, file_path, description? }` accepts jpeg/png ≤8 MiB (8,388,608 bytes); for ordinary/new projects that photo is a required product-identity reference in every generated AI scene, its description grounds scenario/narration, and the exact photo becomes the closing image unless separately authored. Replacing/removing it or changing its identity description invalidates completed AI scenes; re-read/retry on `editor_references_changed`. `set_closing_image { slug, file_path }` accepts jpeg/png ≤20 MiB (20,971,520 bytes); `set_logo { slug, file_path }` accepts jpeg/png ≤5 MiB (5,242,880 bytes; no WebP). Short images: `set_short_product { slug, file_path, description? }` and `set_short_poster { slug, file_path, poster_includes_lockup? }` accept jpeg/png ≤20 MiB (20,971,520 bytes); `set_short_logo { slug, file_path, position?, treatment? }` accepts jpeg/png ≤5 MiB (5,242,880 bytes) and is composited without AI generation.
- **REST fallback (no MCP):** the shape is always `presign → PUT raw bytes → confirm`. Small video: `POST /api/editor/:slug/uploads/presign {filename, mime_type, size_bytes}` → `PUT <presigned_url>` with header `Content-Type: <mime_type>` → `POST /api/editor/:slug/uploads/:upload_id/confirm`. Then poll `GET /api/editor/:slug/uploads` until your upload is `status: "ready"` and place it: `POST /api/editor/:slug/segments/from-upload {upload_id}` (new scene) or `POST /api/editor/:slug/segments/:segment_id/use-asset {upload_id}`. Videos at ≥50 MiB (52,428,800 bytes) use multipart: `…/uploads/multipart/init` → `…/sign-part` per chunk → `…/multipart/complete` (and `…/multipart/abort` on failure). Images: `POST /api/editor/:slug/{product,closing-image,logo}/presign {mime_type, size_bytes}` → PUT → `…/confirm {s3_key}`. **Short images:** `POST /api/shorts/:slug/product/presign {mime_type, size_bytes}` → PUT → `…/product/confirm {s3_key, product_description?}`; `POST /api/shorts/:slug/logo/presign {mime_type, size_bytes}` → PUT → `…/logo/confirm {s3_key, position?, treatment?}`; and `POST /api/shorts/:slug/poster/presign {content_type?}` (returns `{upload_url, s3_key}`) → PUT → `…/poster/confirm {s3_key, poster_includes_lockup?}`. **The `curl` gotcha:** a single-object PUT (small video + every image) **must** send `Content-Type: <presigned mime>`; a multipart **part** PUT must send **no** `Content-Type` at all (the part URL doesn't sign one).

Video: mp4 / mov / webm / mkv, **≤500 MB decimal (500,000,000 bytes), ≤5 min**. Editor product images are jpeg/png **≤8 MiB (8,388,608 bytes)**, closing images **≤20 MiB (20,971,520 bytes)**, and editor/short logos **≤5 MiB (5,242,880 bytes)**. Short product/poster images are jpeg/png **≤20 MiB (20,971,520 bytes)**. No WebP. A freshly uploaded clip must finish processing (`status: ready`) before it can be placed — `upload_video` waits for you; on REST you poll. **Only upload files the user explicitly gave you** — never read or exfiltrate arbitrary local paths.

An editor `video_url` may remain the last delivered MP4 after edits. Treat it as current only when normalized status has `ready:true` and `stale:false`; unknown/absent staleness is not evidence that it matches the live timeline.

## AI assists

AI helper calls — the ones where the model writes content for you — draw from a **free daily quota of 20** per account, separate from credits. Check it with `get_ai_assists` (`{used, limit, bonus, remaining, resets_at, unlock_cost, unlock_batch_size}`; REST: `GET /api/ai-assists`). When it runs out, `unlock_ai_assists` buys more: **1 credit → +10 assists** (REST: `POST /api/ai-assists/unlock`).

- **Consume 1 AI assist:** `generate_scenario`, `enhance_prompt`, `suggest_next_scene`, `suggest_music_prompt`, `generate_narration` (per-segment regeneration is REST-only — `POST /api/editor/:slug/segments/:id/regenerate-narration`, not an MCP tool), `generate_short_text`.
- **Free writes** (nothing consumed): `set_scenario`, `set_segment_prompt`, `set_narration_script`, add/reorder segments, `apply_scenario`, `render`.
- **Cost credits** (not assists): segment generate 5, regenerate 4, voice 3, music 5.

**Narration, voice, and music are three separate meters — don't conflate them.** *Drafting* the narration script (`generate_narration`) consumes **1 free-quota assist** even though its endpoint summary says "free" — that "free" means free of *credits*, not of *quota* (so narration generation is **not** unlimited). *Synthesizing* the voice-over (`generate_voice`) costs **3 credits**. *Generating* music (`generate_music`) costs **5 credits**.

Two distinct signals: **429 `ai_assist_quota_exceeded`** (body has `remaining: 0`, `can_unlock`, `unlock_cost`, `unlock_batch_size`) = out of assists → `unlock_ai_assists` **once** or write the content yourself; **402 `credits_insufficient`** = out of credits → stop. **Bounded loop only:** check `remaining`; if 0 and you still want the assist, unlock once *or* write it yourself. Never loop.

## Delegate the whole content operation (plan → draft → review → schedule → measure)

Beyond one asset, you can run the surrounding content operation. **Everything through drafting is $0** — the planning tools draw the free AI-assist quota (not credits), and materializing a plan produces **0-credit drafts**. **Generating/rendering each draft is a separate, explicit, paid step** — create the drafts, then generate only the ones the user approves; never auto-spend a month of content. The whole loop is agent-scoped (`video:read` + `video:generate`); nothing needs `account:admin`. Every tool has a REST equivalent (paths below, base `https://hubfluencer.com`).

1. **Onboard a product.** `extract_product_page { url }` pulls facts + candidate image/logo URLs from a public page (SSRF-guarded server-side, 0 credits; REST `POST /api/product-sources/extract`). Persist the identity once so later drafts inherit it: `create_product_profile` + `create_brand_profile` (`list_*`/`update_*` manage them; REST `/api/product-profiles`, `/api/brand-profiles`). Pull the primary image + logo in with `import_product_image { product_profile_id, url, link_as }` (`primary_image` default, or `logo`; REST `POST /api/product-sources/import-image`).
2. **Plan (free quota).** `plan_campaign { product_profile_id | product }` proposes angles/formats + a per-item credit **estimate** (REST `POST /api/campaign-packs/plan`); `generate_hook_variations { ..., brand_profile_id? }` writes 3–6 draftable hook options (REST `POST /api/campaign-packs/hook-variations`). On 429, unlock once or write the copy yourself.
3. **Pack & materialize ($0).** `create_campaign_pack { title?, product_profile_id?, brand_profile_id? }` is the container (REST `POST /api/campaign-packs`); `add_pack_variations { pack_id, variations, materialize: true }` persists the picks and turns each into an editable **draft** (≤12/call; REST `POST /api/campaign-packs/:id/variations`); `materialize_pack_item { pack_id, item_id }` (re)tries one (REST `POST /api/campaign-packs/:id/items/:item_id/materialize`); `get_campaign_pack`/`list_campaign_packs` show each item's status + **draft slug** + estimate (REST `GET /api/campaign-packs[/:id]`). Shortcut: `create_campaign { url | product_profile_id, brand_profile_id?, item_count?, formats? }` runs steps 1–3 in one MCP call.
4. **Generate per draft (credits — a deliberate step).** For each approved draft slug: `start_autopilot { slug, max_credits }` (video — a bare `start_autopilot { slug }` only re-quotes; pass `max_credits` ≥ the estimate to launch) or `generate_slider { slug }` (carousel).
5. **Review (no-login).** `create_review_link { video_result_id | slider_id }` mints an expiring page where a human approves / requests changes / comments with **no account**. The token + URL come back **once** — surface them immediately. `list_review_links` shows activity; `revoke_review_link { id }` kills a link (REST `/api/review-links`). Gate spend or posting on approval.
6. **Calendar (reminder-only).** `schedule_post { video_result_id, platform, scheduled_at }` stages a calendar entry that pushes a **reminder to the user** at the due time — it does **not** auto-publish. `list_scheduled_posts` is the calendar; `reschedule_post` / `cancel_scheduled_post` manage it; after the user posts, `mark_scheduled_posted { id }` (REST `/api/scheduled-posts`, mark-posted `POST /api/scheduled-posts/:id/mark-posted`). All 0 credits; a reminder-only post (no social account) needs only `video:generate`.
7. **Measure → double down.** `record_performance { item_id, platform, views, ... }` logs a snapshot for a pack item; `set_item_attributes { item_id, ... }` tags its creative (REST `POST /api/campaign-pack-items/:item_id/performance`, `PUT …/attributes`). `get_recommendations` then returns a **cautious**, sample-size-aware read of your own best hook/format — **present its `confidence` + `note` verbatim; never claim causation, ROAS, or attribution** (REST `GET /api/performance/recommendations`). `make_more_like_winner { item_id }` spins fresh hooks from a proven item (REST `POST /api/campaign-pack-items/:item_id/make-more`) → feed a chosen one back into `add_pack_variations` (step 3).
8. **Recurring shows.** `create_series { name, template, brand_profile_id?, product_profile_id? }` defines a repeating format; `plan_series_episodes { series_id }` drafts upcoming ideas (free quota); `materialize_episode { series_id, episode_id }` turns one into a 0-credit draft; `get_series_dashboard { series_id }` shows the upcoming / drafted / published lanes; `mark_episode_posted { series_id, episode_id }` closes the loop (REST `/api/series`, `…/episodes/plan`, `…/episodes/:episode_id/materialize`, `…/dashboard`, `…/episodes/:episode_id/mark-posted`).

**Reusable assets.** `list_assets` / `get_asset_quota` / `upload_asset { file_path, ... }` manage a cross-project media catalog (images + videos) so a clip/logo can be reused across projects (REST `GET /api/assets`, `GET /api/assets/quota`, `POST /api/assets/presign` → PUT → `POST /api/assets/:id/confirm`). These need an **active subscription** (402 `subscription_required` otherwise) and cost **0 credits**; `upload_asset` reads are confined to `HUBFLUENCER_INPUT_DIR`.

## Field limits

| Field | Min | Max |
|-------|-----|-----|
| editor `product_prompt` (create) | 0 or 10 | 5000 |
| factory `product_prompt` (non-editor) | 10 | 5000 |
| `scenario_prompt` (`set_scenario`) | 1 | 50000 |
| `generate_scenario` `segments_count` (continuous) | 3 | 10 |
| `apply-scenario` `segments_count` (`0` or continuous `3..10`) | — | — |
| scene count via `set_scene_count` | 1 | 20 |
| segment `prompt` (add/update) | 1 | 2000 |
| segment `narration_text` | — | 5000 |
| factory `narration_script` (`set_narration_script`) | — | 10000 |
| music direction `description` (silently truncated, not 422) | — | 1200 |
| `voice_id` (required, regex `^[A-Za-z0-9_-]+$`) | 1 | 64 |
| `language` (create) | 2 | 10 |
| `product_description` (editor/short product upload) | — | 500 |
| short `headline` (title; keep ≈4 words / ≤40 chars) / `subheadline` | — | 80 / 200 |
| short `closing_claim` / `brand_name` (end card) | — | 80 / 40 |
| short `music_vibe` | — | 80 |
| AI assist daily quota | 0 | 20 |

`product_prompt` on create is **empty OR 10..5000** (empty = omit it). The assembled full music prompt (5000) and music timeline lines (240) are server-side internals — don't try to set them.

## Credits

- **A credit is $0.99** bought singly; bulk packs and subscriptions cost less per credit — translate credit counts into dollars for the user (short 15cr ≈ **$15 max**; autopilot ad ~28cr ≈ **$28 max**).
- **Confirm before spending:** quote the estimated credits + approximate dollar cost and get an explicit yes before the first charging call; for a batch, confirm the total once up front. Skip only when the user already approved that specific spend.
- Any draft create / edit / reorder (`create_short` / `create_editor_ad` / `create_editor_draft` / `create_slider` / campaign & series drafts): **0 credits**.
- Rendering a short: **15 credits**.
- Granular editor steps: generate segment **5**, regenerate **4**, **batch per-segment (batch of ≥3) 4** (so `render` bills any still-ungenerated scenes at the batch rate), voice **3**, music **5**, render **0** (charges ungenerated or previously failed scenes). Unlock AI assists = **1 credit → +10**.
- Multi-scene editor ads (autopilot) ≈ **28** — preflight with `GET /api/editor/:slug/autopilot/cost` (or trust the MCP tool's preflight) before kicking off.
- Carousel render: **1 / slide** (3..10 slides); carousel restyle / per-slide edit: **0**. Tracking render: **1** (refunded on failure).
- Onboarding / planning / packs / series / calendar / performance / review / profiles: **0 credits** (planning tools draw the free daily assist quota).
- Check the balance with `get_credits` (REST: `GET /api/studio/credits`).

## Result files

Result URLs are **presigned and short-lived (~24h TTL)** — they are not permalinks. Download promptly with `download_result` (or `curl`), then hand the local file to the user.

## Failure handling

- **402 `credits_insufficient`** — tell the user the required vs available credits and stop. Do **not** loop or retry.
- **429 `ai_assist_quota_exceeded`** — the daily AI-assist quota is used up (NOT a credit problem; body has `remaining: 0` + `unlock_cost`). `unlock_ai_assists` once or write the content yourself; never loop.
- **409 already-running** — a render is already in progress. Keep polling with `wait_for_completion`; do **not** start a new one.
- **403 `insufficient_scope`** — the token lacks `video:generate`. Tell the user to create a token with the "Generate videos" scope.
- **Long waits** are normal. A few minutes per render is expected.

## What NOT to do

- Do **not** auto-publish to TikTok / Instagram. Publishing needs a human-linked social account (interactive OAuth) and `account:admin`, which agent tokens never carry. Return the finished MP4 (or carousel images) **plus a ready-to-paste caption** so the user posts it themselves. You **can** stage a **reminder-only** calendar with `schedule_post` (it pushes a reminder to the user, never a publish) and close the loop with `mark_scheduled_posted` after they post — that's the supported "run my calendar" path.
- **Label synthetic media.** These renders are AI-generated: TikTok requires an AI-generated content label (and attaches C2PA content credentials since 2025), and Meta requires disclosure for AI-generated/altered content in paid ads. Whenever you hand over an export or stage a posting reminder, tell the user to enable the platform's AI label.
- Do not echo or log the access token.
- Do not invent endpoints — the OpenAPI spec is at `GET /api/openapi`.

## REST fallback (no MCP)

If the MCP tools are not available, drive the same API directly. Base URL `https://hubfluencer.com`, all paths under `/api`, auth header `Authorization: Bearer <token>`.

```bash
# Check credits
curl -s https://hubfluencer.com/api/studio/credits \
  -H "Authorization: Bearer $HUBFLUENCER_API_TOKEN"

# Preflight an editor ad's cost
curl -s https://hubfluencer.com/api/editor/$SLUG/autopilot/cost \
  -H "Authorization: Bearer $HUBFLUENCER_API_TOKEN"

# Check the AI-assist quota
curl -s https://hubfluencer.com/api/ai-assists \
  -H "Authorization: Bearer $HUBFLUENCER_API_TOKEN"
```

Granular editor REST equivalents (reads need `video:read`, generation/spend needs `video:generate`): `POST /api/editor` (create) → `POST /api/editor/:slug/generate-scenario` (1 assist) or `PATCH /api/editor/:slug/scenario {scenario_prompt}` → `GET /api/editor/:slug` (review) → add/`DELETE /api/editor/:slug/segments/:id` → `PATCH /api/editor/:slug/segments/:id {prompt}` → `POST /api/editor/:slug/segments/:id/generate` (5cr) or `POST /api/editor/:slug/batch-generate` → `PATCH /api/editor/:slug {narration_script}` (≤10000) or `POST /api/editor/:slug/generate-narration` (1 assist) → `POST /api/editor/:slug/generate-voice {voice_id}` (3cr) + `POST /api/editor/:slug/generate-music {prompt}` (5cr, ≤1200 direction) → `POST /api/editor/:slug/render` (0cr). `Idempotency-Key` is required on scene generate/regenerate, batch-generate, full or per-segment narration generation, voice, music, and render requests. Unlock more assists with `POST /api/ai-assists/unlock` (1 credit → +10). 429 = quota exhausted, 402 = credits short.

The full schema (every endpoint and field) lives at `https://hubfluencer.com/api/openapi`.
