REST API reference
The Hubfluencer API.
Everything an agent (or your own code) needs to turn a prompt — or your own footage — into a finished MP4. Base URL is hubfluencer.com, all paths sit under /api, and you authenticate with a Bearer token.
Overview & authentication
Send every protected request with an Authorization header. The token is opaque (a session token or a Personal Access Token) — it is not a JWT, so don't decode it.
authorization: Bearer <token> content-type: application/json
Scopes gate what a token can do: video:read for reads, video:generate for anything that spends credits, account:admin for account/token management. Agent tokens get video:generate + video:read only.
Prefer docs your agent can read directly? Download the Markdown mirror of this page: API.md . A machine-readable OpenAPI spec also lives at /api/openapi (Bearer-gated in production).
Response envelope
// Success — usually wrapped in `data` (a few endpoints return the object at top level)
{ "data": { "...": "..." }, "error": null, "meta": { "timestamp": "2026-06-03T10:00:00Z" } }
// Error — shapes vary; parse defensively
{ "error": "credits_insufficient", "message": "Not enough credits.", "required_credits": 15, "available_credits": 3 }
{ "errors": { "product_prompt": ["should be at least 10 character(s)"] } }
Access tokens
Personal Access Tokens are the recommended credential for agents. The token string is returned only once on creation. Managing tokens needs account:admin — so an agent token can't mint or revoke tokens; create one while signed in to the app (Settings → Access tokens), or run npx -y @hubfluencer/mcp login.
/api/tokens
account:admin
Create a Personal Access Token.
| Body field | Type | Notes |
|---|---|---|
name optional |
string | Label shown in the app. |
scopes optional |
string[] | Defaults to video:generate + video:read. |
Request
curl -X POST https://hubfluencer.com/api/tokens \
-H "authorization: Bearer $SESSION_TOKEN" \
-H "content-type: application/json" \
-d '{"name":"Claude Code","scopes":["video:generate","video:read"]}'
Response
201 Created
{
"data": {
"id": 42,
"name": "Claude Code",
"scopes": ["video:generate", "video:read"],
"token": "hf_pat_9f3c…ONCE_ONLY",
"inserted_at": "2026-06-03T10:00:00Z"
}
}
/api/tokens
account:admin
List your tokens. A ["*"] scope listing means a full-access token.
200 OK
{
"data": [
{ "id": 42, "name": "Claude Code",
"scopes": ["video:generate","video:read"],
"last_used_at": "2026-06-03T10:05:00Z",
"inserted_at": "2026-06-03T10:00:00Z" }
]
}
/api/tokens/:id
account:admin
Revoke a token. Returns 204 No Content.
Credits & voices
/api/studio/credits
video:read
Current credit balance. Check it before generating; a short costs 15 credits, and a credit is $0.99 (less in bulk). Quote the dollar cost and get the user's go-ahead before any charging call.
200 OK
{ "data": { "credits": 120 } }
/api/voices
video:read
Narration voices. Pass a voice id to generate-voice (editor ads). Shorts have no voice-over.
200 OK
{ "data": [
{ "id": "rachel", "name": "Rachel" },
{ "id": "adam", "name": "Adam" }
] }
Shorts
A short is a single-prompt, single-clip ad. Three requests: create a draft (free), start the render (15 credits), then poll until the stage is video_ready. Send an Idempotency-Key on generate so a retry doesn't double-charge.
/api/shorts
0 credits
Create a short draft.
| Body field | Type | Notes |
|---|---|---|
product_prompt required
|
string | ≥ 10 characters — what the ad is about. |
language optional |
string | Closed set of bare ISO-639-1 codes (default "en"): en, fr, es, de, it, nl, pl, pt, ru, zh, ja, ko, ar. Region/script subtags like "en-US" or "pt-BR" are rejected (422). |
headline optional |
string | On-screen TITLE overlay (≤80 hard cap; ≤40 chars ≈ 4 words is the render sweet spot). Leave headline, subheadline, AND text_beats blank to have generate write the copy for you (free). |
subheadline optional |
string | SECONDARY title / supporting line (≤200). |
theme optional |
string | Deprecated for shorts: legacy fallback only when visual_language is unset. Use visual_language for new shorts. |
creative_format optional
|
string | Optional structure: problem_solution, mistake_fix, myth_vs_reality, before_after, proof_demo, product_reveal. Omit for Auto. |
visual_language optional
|
string | Visual direction + render look: kinetic_creator, premium_editorial, cinematic_product, ugc_realism, startup_explainer, luxury_minimal. Bare creates (no visual_language, no theme) default to kinetic_creator; send an explicit null for the plain neutral look. |
music_vibe optional |
string | Upbeat (default), Cinematic, Minimal, Luxury, Playful, Jazz. When unset, the soundtrack follows the visual_language (e.g. luxury_minimal → Luxury, cinematic_product → Cinematic). |
short_text_position, short_text_animation, short_font_family,
music_instruments
optional
|
string / array | Overlay position (top/center/bottom). Animation: auto (default) / reveal / typewriter / fade_in / pop / bounce / word_stagger / word_spotlight. Font: auto (default) or one of the 15 bundled faces — ShortFontSpaceGrotesk, ShortFontMontserrat, ShortFontTheBold, ShortFontImpact, ShortFontLato, ShortFontAnton, ShortFontBebasNeue, ShortFontOswald, ShortFontArchivoBlack, ShortFontPoppins, ShortFontInter, ShortFontTikTokSans, ShortFontBangers, ShortFontDMSerif, ShortFontPermanentMarker. "auto" follows the visual_language vibe's typography; every concrete value — font or animation, including ShortFontSpaceGrotesk / fade_in — is an EXPLICIT pick that always wins. music_instruments: up to 10 hints, each ≤ 40 chars. |
headline_color, subheadline_color, accent_color,
text_beats
optional
|
string / array | Overlay colors (headline/subheadline accept #rgb–#rrggbbaa hex; accent_color is strictly 6-digit #rrggbb), and text_beats — up to 8 caption lines (≤120 chars each) shown one at a time in place of the static subheadline. On PATCH, send [] to clear the beats. |
offer_text, cta_text, badge_text,
star_rating
optional
|
string / number | Conversion graphics: offer chip (≤16), CTA pill (≤24), badge stamp (≤24), and a 0–5 star rating. Offer/badge/stars are strictly user-supplied and OFF unless set — only claim what you can substantiate: badge_text and star_rating render as endorsements, so send a real rating/badge you actually hold, never an invented one. cta_text is the one exception: your value always wins, but when blank the render falls back to your brand profile's default_cta, then a fixed claim-free localized preset ("Shop now" for product formats, "Learn more" otherwise) — a neutral localized ask is ALWAYS rendered; a truly CTA-less short is not currently supported. Read the resolved value back as resolved_cta_text. On PATCH, send "" (or 0 for star_rating) to remove one. |
closing_claim, brand_name, end_card,
poster_includes_lockup
optional
|
string / boolean | End-card controls. By default every short closes on a 2s end card (14s total): your poster image, or — when none is set — a generated brand lockup slate (brand_name big, closing_claim or the headline, CTA pill). closing_claim (≤80): a rephrase of the promise so the end card doesn't recycle the hook; the copy assist may fill it. brand_name (≤40): display name (falls back to your brand profile's name). end_card: "auto" (default) or "none" for a 12s loop-style short with no end card. poster_includes_lockup (boolean, also accepted on poster/confirm): set true when your poster already carries its own name/CTA so the overlay is suppressed. The derived mode is returned as end_card_mode (poster | lockup | none). |
brand_profile_id optional
|
integer | Id of one of your own brand profiles (GET /api/brand-profiles). On create, the brand's defaults (creative format, visual language, font, CTA, accent color) seed any blank short fields and a snapshot is frozen onto the short — the fastest way to brand a short. A foreign/unknown id → 422 invalid_brand_profile. |
Request
curl -X POST https://hubfluencer.com/api/shorts \
-H "authorization: Bearer $HF" -H "content-type: application/json" \
-d '{"product_prompt":"a 15s ad for my soy candle brand","language":"en","creative_format":"proof_demo","visual_language":"kinetic_creator"}'
Response
201 Created
{
"data": {
"slug": "amber-candle-9x2",
"stage": "draft",
"language": "en",
"latest_render": null,
"inserted_at": "2026-06-03T10:00:00Z"
}
}
/api/shorts/:slug/text/generate
1 AI assist
Generate editable headline, subheadline, and caption beats from the saved short draft (it also fills blank brand_name / closing_claim — extraction and rephrase only, never new claims). No video credits are spent and no render starts. You usually don't need this before generating: generate auto-writes blank copy for free.
curl -X POST https://hubfluencer.com/api/shorts/amber-candle-9x2/text/generate \ -H "authorization: Bearer $HF"
/api/shorts/:slug/generate
15 credits
video:generate
Render the short. Idempotent per slug. Then poll GET /api/shorts/:slug. Auto-copy: when headline, subheadline, AND text_beats are all blank, the server writes the overlay copy inline first (free — it never touches the daily AI-assist quota) and persists it BEFORE charging, so retries never regenerate it; pass {"skip_auto_text": true} in the body for a deliberately text-free clip. If the copy generator is down → 503 copy_generation_unavailable, nothing charged — just retry. Auto-describe: a product image attached without a description is described inline at generate time (also free — the standalone product-description/generate endpoint stays metered) so the AI segments carry the product grounding; a describer failure just proceeds without one. The prompt (including auto-written copy) is content-compliance screened BEFORE any charge: a prohibited prompt → 422 prompt_rejected (code CONTENT_COMPLIANCE, with a category); screening down → 503 compliance_unavailable (fail-closed) — neither is charged. Out of credits → 402 credits_insufficient with required_credits + available_credits (the legacy required/credits aliases are deprecated).
curl -X POST https://hubfluencer.com/api/shorts/amber-candle-9x2/generate \ -H "authorization: Bearer $HF" \ -H "idempotency-key: gen-short:amber-candle-9x2"
/api/shorts/:slug/rerender
0 credits
video:generate
Re-render is FREE, forever — credits only ever buy new AI footage. Re-composites the short's CURRENT overlay/style state over the already-generated (pinned) segments + music: edit headline, subheadline, text_beats, cta_text, offer_text, badge_text, star_rating, colors, short_font_family, short_text_position, short_text_animation, accent_color, closing_claim, brand_name, poster (+ poster_includes_lockup), or end_card via PATCH, then call this (no body). Returns the same Short payload as generate — poll GET /api/shorts/:slug. Unchanged composition → the existing render is returned as-is (idempotent). The pinned music bed is reused even when an end-card edit changed the runtime — a shorter bed crossfades into a repeated continuation and fades at the export end instead of failing the render. Footage/music-affecting fields (prompt, product image, creative_format, visual_language, theme, music_vibe, music_instruments, language) need the paid generate instead; changing one returns 422 short_paid_regeneration_required. A FAILED free re-render never breaks the delivered short: stage stays video_ready, latest_render falls back to the latest completed render, and last_free_rerender_failed: true is set (latest_render_free flags any newest free re-render row) — just call rerender again. 409 short_generation_in_progress while any work is active; 422 short_not_ready unless the short has its 2 completed segments, completed music, and a completed render. Never 402.
# edit any free field first (0 credits), then re-render — free, no body
curl -X PATCH https://hubfluencer.com/api/shorts/amber-candle-9x2 \
-H "authorization: Bearer $HF" -H "content-type: application/json" \
-d '{"cta_text": "Order today"}'
curl -X POST https://hubfluencer.com/api/shorts/amber-candle-9x2/rerender \
-H "authorization: Bearer $HF" \
-H "idempotency-key: rerender-short:amber-candle-9x2:v2"
/api/shorts/:slug
video:read
Poll for status. stage is the signal: draft → rendering → video_ready (read latest_render.video_url) | failed (read failed_stage + error_message). A failed FREE re-render never flips a delivered short to failed — stage stays video_ready with the completed render in latest_render; check last_free_rerender_failed.
200 OK — poll this until stage is video_ready or failed
{
"data": {
"slug": "amber-candle-9x2",
"stage": "video_ready",
"failed_stage": null,
"error_message": null,
"latest_render": {
"status": "completed",
"video_url": "https://…/amber-candle-9x2.mp4?X-Amz-Expires=86400"
}
}
}
/api/shorts/:slug/cost
video:read
Cost preflight — returns total and available_credits before you spend.
/api/shorts/:slug
0 credits
video:generate
Edit a draft before rendering: any of the creative fields from POST /api/shorts (product_prompt, headline, subheadline, creative_format, visual_language, music_vibe, the opt-in conversion graphics, …). Present-aware — only the fields you send change. To REMOVE an opt-in graphic, send it empty: offer_text/cta_text/badge_text "", star_rating 0, text_beats []. 0 credits, no render. While the short is generating (processing), edits are locked → 409 short_generation_in_progress; wait for it to finish or fail.
Branding images — product & end-card poster
Both are 0 credits and use a presign → PUT raw bytes → confirm flow. On the single PUT, send Content-Type matching the presigned mime. The product image accepts jpeg/png; the end-card poster additionally accepts webp. Both ≤ 20 MB. A never-uploaded, oversized, or corrupt image is rejected at confirm (422). Shorts have no logo overlay (that's editor-only).
Tracking overlay
Upload your own video and get it back with a computer-vision tracking overlay burned on top (convex-hull polygons hugging each moving subject, HUD brackets, a crosshair, and per-subject telemetry). Source clip must be 60s or shorter and 1080p or lower. Create a draft (free), set the source clip — either upload it via the editor upload routes with the tracking slug, or reuse a library video via /source/from-asset — then render (1 credit). The source clip is deleted once the render completes.
/api/tracking
0 credits
Create a tracking draft. Optional body: language. Returns the slug; upload the source clip via POST /api/editor/:slug/uploads/* (presign or multipart, then confirm) using the returned slug.
/api/tracking
video:read
List your in-progress tracking projects (the Studio work-in-progress list) — finished ones aren't included. Each entry is the same shape as GET /api/tracking/:slug.
/api/tracking/:slug
video:read
Poll the project. The payload carries source (the latest source upload) and result. source.status is pending → processing → ready (or failed); wait for "ready" before rendering, and treat a non-ready, non-failed source as still uploading/processing. Once rendered, read result.status ("completed") + result.video_url; on a failed render read result.error_message.
/api/tracking/:slug/source/from-asset
0 credits
video:generate
Reuse a video already in your asset library as the source clip instead of uploading a new one. Body: asset_id (a ready catalog video asset you own). The asset object is copied into the project and normalized (your library asset is left untouched), so poll GET /api/tracking/:slug until source.status is "ready", then render. 422 asset_not_video / asset_not_ready if the asset isn't a ready video; 409 tracking_render_in_progress while a render is active.
/api/tracking/:slug/render
1 credit
video:generate
Burn the overlay. Idempotent per slug (send an Idempotency-Key). Optional body: classes (COCO-80 ids to track, e.g. [0]=person, [16]=dog; omit to track all) and color ([r,g,b,a] overlay). An identical re-render returns the existing result for 0 credits. Single-flight: a render started while one is still processing returns 409 tracking_render_in_progress (no extra charge). Out of credits returns 402 credits_insufficient with required_credits + available_credits. Then poll GET /api/tracking/:slug. The source clip is deleted once the render completes (you keep the output); set a new source to render again.
/api/tracking/:slug
Delete a tracking project and its uploaded clip + result. Deleting mid-render refunds the 1 credit. Requires the video:generate scope (like all write methods).
Sliders (image carousels)
A slider is a social-media carousel: one prompt produces N still slides (an AI background + a composited headline/body + optional logo) plus a ready-to-post caption and hashtags. No video — save the images and copy the text. Create a draft (free), then generate (1 credit per slide ⇒ 3–10 credits; default 5 slides = 5 credits). Generation is async: poll GET /api/sliders/:slug until status is completed or failed. Editing slide text, or the template/accent/logo, re-renders for free.
/api/sliders
0 credits
Create a carousel draft.
| Body field | Type | Notes |
|---|---|---|
prompt optional |
string | What the carousel is about. ≤2000 chars. Optional at draft, required (≥10 non-blank chars) before generate. |
mode optional |
string | creative (storytelling) or ad_driven (product facts). Default creative. |
template optional |
string | boldStatement / editorialStory / scrapbook (creative); featureGrid / offerCard / comparison (ad-driven). Defaults to the mode default. |
language optional |
string | Language the generated slide copy + caption are written in: en (default) / fr / es / de / it / pt / nl / pl. Latin-script only. |
slide_count, aspect_ratio, accent_color
optional
|
integer / string | slide_count integer 3–10 (default 5); aspect 4:5 (default) / 1:1 / 9:16; accent a hex matching ^#[0-9a-fA-F]{6}$ like #09EFBE. |
text_position optional
|
string | top / middle / bottom — vertical placement of the on-image copy across all slides. Omit to use the template's natural placement. |
caption, hashtags
optional
|
string / array<string> | Post copy: caption ≤3000 chars; hashtags an array of strings. Usually authored by generate, but editable via PATCH. |
/api/sliders/:slug/generate
1 credit / slide (3–10)
video:generate
Render the carousel. Costs 1 credit per slide ⇒ 3–10 credits (default 5 slides = 5 credits). Send a fresh Idempotency-Key per attempt (a stable per-slug key replays the first response for 24h and blocks an intentional re-generate). The prompt is screened for content-policy compliance first — a violating prompt returns 422 prompt_rejected (code CONTENT_COMPLIANCE, with a category) and is not charged. Rate-limited to 10/min. Then poll GET /api/sliders/:slug until status is completed or failed.
/api/sliders/:slug
video:read
Poll for status until terminal: draft → processing → completed (each slides[].image_url is a downloadable still; read caption + hashtags) | failed (read error_message; credits refunded). Each slides[] entry is { position, headline, body, kicker, status, image_url, background_url }; per-slide status is pending → processing → completed | failed (a not-yet-rendered slide is pending, not draft). image_url is the final composited slide (presigned, 24h TTL); background_url is the raw AI background preview (presigned, 1h TTL).
/api/sliders/:slug/slides/:position
0 credits
video:generate
Edit one slide's on-image text: headline (≤120), body (≤600), kicker (≤40). Re-composites just that slide for free (reuses the AI background — no new image cost). The slider must already be completed (else 409 conflict); it runs async, flipping the slide back to processing — poll GET /api/sliders/:slug until it is completed again. Rate-limited to 20/min.
/api/sliders/:slug/restyle
0 credits
video:generate
Restyle a completed carousel: body accepts template, accent_color (^#[0-9a-fA-F]{6}$), text_position (top/middle/bottom), and logo_s3_key — and re-composites EVERY slide for free (reuses the AI backgrounds). The slider must already be completed (else 409). Runs async (every slide → processing); poll GET /api/sliders/:slug until completed. Rate-limited to 20/min. (Confirming a logo via /logo/confirm on an already-completed carousel routes through restyle, so it likewise triggers a full re-composite of every slide, free.)
/api/sliders
List your carousels (newest first). GET /api/sliders/:slug/cost returns total + available_credits. PATCH /api/sliders/:slug always edits the copy fields (caption, hashtags); the prompt and the render-shaping fields (language, mode, template, slide_count, aspect_ratio, accent_color, text_position, logo_s3_key) are editable here only while the slider is draft or failed (a failed render reopens them) — once processing/completed they are silently ignored (the prompt is locked post-generation; use restyle / the slide PATCH instead). DELETE /api/sliders/:slug removes a carousel and its images. POST /api/sliders/:slug/logo/presign then /logo/confirm attach an optional brand logo, or POST /api/sliders/:slug/logo/from-asset {asset_id} reuses a ready catalog image you own (subscription-gated, 0 credits).
Editor ads — autopilot
An editor ad is a multi-scene, story-driven video. Autopilot runs the whole pipeline server-side (scenario → scenes → narration → voice → music → render). Create the project, start autopilot, then poll the project — the finished MP4 is embedded in latest_render. A 5-scene run is ~28 credits (batch scene rate + voice + music); preflight the exact figure with GET /api/editor/:slug/autopilot/cost.
/api/editor
0 credits
Create an editor project. Free accounts (0 credits) may hold up to 100 editor projects — a 101st returns 402 editor_factory_limit_reached; delete one or add credits. Accounts with credits are uncapped. Send an Idempotency-Key header so a retry doesn't fork a second draft.
| Body field | Type | Notes |
|---|---|---|
language required |
string | Supported code: en, fr, es, de, it, nl, pl, pt, ru, zh, ja, ko, or ar. Drives the WHOLE video: scenario text, narration script, narration voice, and captions. |
product_prompt |
string | Empty OR 10–5000 chars. Autopilot needs a usable brief: a structured scenario brief, product_prompt ≥10 chars, OR product_subject ≥3 chars. |
product_subject optional
|
string | ≤ 300 chars. The concrete product or main subject the video must be about (e.g. "Bordeaux red wine, dark green bottle with a gold label") — distinct from the freeform product_prompt brief. When set, the AI scenario + narration are hard-grounded on it so they can't invent an unrelated product (the common failure mode for ad videos). Recommended for project_intent social_ad. Omit/blank to leave the AI free. |
export_aspect_ratio
optional
|
enum | 9:16 (default), 16:9, or 1:1. |
creative_format, visual_language
optional
|
enum | Creative controls (same value lists as Shorts): narrative arc + render look. Omit for Auto. |
theme, voice_id, project_intent
optional
|
string | Visual theme / genre overlay (when visual_language is set it drives the look; "none" = no imposed style), narration voice, social_ad | creative_story. |
cast_mode optional
|
enum | Who/what is on screen in AI scenes: product_only (no people; the product image drives every scene) or presenter_led (a single locked, recurring presenter image is injected into every scene). presenter_led needs a presenter image set first via the presenter endpoints. Omit/blank/auto = default path. |
Request (create, then start autopilot)
curl -X POST https://hubfluencer.com/api/editor \
-H "authorization: Bearer $HF" -H "content-type: application/json" \
-d '{"language":"en","product_prompt":"a cinematic ad for my candle brand","export_aspect_ratio":"9:16"}'
# then start autopilot:
curl -X POST https://hubfluencer.com/api/editor/$SLUG/autopilot \
-H "authorization: Bearer $HF" -H "idempotency-key: autopilot:$SLUG"
/api/editor/:slug/autopilot
credits
video:generate
Run the full pipeline. Send an Idempotency-Key. Preflight the cost with GET /api/editor/:slug/autopilot/cost. Accepts an OPTIONAL JSON body: language (one of the supported project codes), product_subject (3–300 chars when non-blank), product_prompt (10–5000 chars when non-blank), and max_credits (non-negative integer). A structured scenario brief, product_prompt ≥10, or product_subject ≥3 satisfies the brief requirement. The cap is persisted for this run and atomically enforced again at every scene, voice, and music charge; if it would be exceeded, no further charge starts. Only non-blank text overrides are applied before the chain starts.
Request (body optional — pre-launch overrides)
curl -X POST https://hubfluencer.com/api/editor/$SLUG/autopilot \
-H "authorization: Bearer $HF" -H "content-type: application/json" \
-H "idempotency-key: autopilot:$SLUG" \
-d '{"language":"fr","product_subject":"Bordeaux red wine, dark green bottle with a gold label","product_prompt":"a cinematic 15s ad for my wine","max_credits":28}'
Response (poll GET /api/editor/:slug)
200 OK — poll this; autopilot embeds the finished MP4 in latest_render
{
"data": {
"slug": "candle-story-7k1",
"autopilot_status": "completed",
"autopilot_error_message": null,
"scenario_prompt": "Warm, handcrafted… ",
"segments_count": 5,
"segments": [
{ "id": 81, "position": 1, "prompt": "Macro of wax pouring…", "status": "completed" },
{ "id": 82, "position": 2, "prompt": "Hands trimming the wick…", "status": "completed" }
],
"narration_script": "Made by hand, poured in small batches…",
"narration_status": "completed",
"music": { "status": "completed" },
"latest_render": {
"status": "completed",
"video_url": "https://…/candle-story-7k1.mp4?X-Amz-Expires=86400"
}
}
}
/api/editor/:slug/autopilot/cancel
0 credits
video:generate
Stop a running autopilot chain (autopilot_status → cancelled). Already-charged scenes stay; nothing new is generated. No-op if it already finished.
Granular editor pipeline
Same editor project, driven step by step instead of autopilot — write the scenario, set the scene count, hand-write each scene prompt, then generate. Reads need video:read; anything that generates needs video:generate. Each row is free (no credit, no assist), assist (1 AI assist), or credits.
* render auto-charges only still-ungenerated scenes (batch rate), so a fully-generated project renders for 0. voice_id matches ^[A-Za-z0-9_-]+$ (≤64). Editing the scenario or narration after generating voice/music marks them stale — render returns 422 editor_voice_stale / editor_music_stale / editor_narration_stale until you regenerate.
apply-scenario materializes the saved scenario into the timeline: existing AI scenes are replaced with scene prompts generated from the scenario, uploads are preserved, and segments_count 0 removes the AI scenes. It is free — no credits and no AI assist (generate-scenario already spent the assist) — and runs async: poll GET /api/editor/:slug until scenario_apply_status leaves "applying". When segments_count is omitted, apply uses the saved scenario brief's scene count, then defaults to 5. regenerate (4 cr) creates a NEW version of one scene; pass prompt to rewrite the scene for the new version, or omit it to reuse the current prompt. A completed scene cannot be re-run through generate (422 editor_segment_completed) — regenerate is the path to a fresh take.
Scene prompts are screened for content-policy compliance at every paid generation entry (generate, regenerate, batch-generate, and render's auto-charge): a violating prompt returns 422 prompt_rejected (code CONTENT_COMPLIANCE) with nothing charged; if screening is temporarily unavailable the call returns 503 compliance_unavailable (fail-closed, nothing charged — retry shortly).
# Write your own scenario instead of generating it (free, no assist)
curl -X PATCH https://hubfluencer.com/api/editor/$SLUG/scenario \
-H "authorization: Bearer $HF" -H "content-type: application/json" \
-d '{"scenario_prompt":"Scene 1 … Scene 2 … Scene 3 …"}'
# Generate one scene (5 credits); poll the segment status until completed
curl -X POST https://hubfluencer.com/api/editor/$SLUG/segments/81/generate \
-H "authorization: Bearer $HF" -H "idempotency-key: gen-seg:$SLUG:81"
# Voice-over (3 credits) then render (0 credits; auto-charges ungenerated scenes)
curl -X POST https://hubfluencer.com/api/editor/$SLUG/generate-voice \
-H "authorization: Bearer $HF" -H "content-type: application/json" -d '{"voice_id":"rachel"}'
curl -X POST https://hubfluencer.com/api/editor/$SLUG/render -H "authorization: Bearer $HF"
Uploads & local assets
Bring your own media into an editor project — your own footage as scenes, plus a product image, closing card, and brand logo. All of this is free (0 credits). Uploads use a presign → PUT-to-storage → confirm flow; the file then processes asynchronously before it can go on the timeline.
/api/editor/:slug/uploads/presign
0 credits
Get a presigned PUT URL for a video upload.
| Body field | Type | Notes |
|---|---|---|
filename required |
string | Original file name. |
mime_type required |
string | video/mp4, video/quicktime, video/webm, video/x-matroska. |
size_bytes required |
integer | ≤ 500 MB per file; video ≤ 5 min. Also counts against your per-user storage quota. |
fit_mode optional |
string | "cover" or "blur" (default). |
Returns 422 upload_quota_exceeded (with a quota object: limit/used/reserved/remaining bytes) if the upload would push your total live upload storage over the limit. Delete unused uploads to free space.
200 OK
{
"data": {
"upload_id": 510,
"presigned_url": "https://r2…/editor/$SLUG/uploads/uuid.mp4?X-Amz-Signature=…",
"s3_key": "editor/$SLUG/uploads/uuid.mp4",
"expires_in_seconds": 3600
}
}
/api/editor/:slug/uploads/:upload_id/confirm
0 credits
Confirm the PUT. The server validates the object (Content-Type and size must match the presign) and queues processing.
/api/editor/:slug/uploads
video:read
List uploads with their processing status. Poll until status is "ready" before placing the clip.
200 OK — status flows pending → processing → ready | failed
{
"data": [
{ "id": 510, "filename": "clip.mp4", "mime_type": "video/mp4",
"status": "ready", "duration_seconds": 6.2, "width": 1080, "height": 1920,
"error_message": null, "attached_segment_ids": [83] }
]
}
/api/editor/:slug/segments/from-upload
0 credits
Append a ready upload to the timeline as a finished scene. Body: upload_id. Rejects editor_upload_not_ready, batch_generation_active, autopilot_active (while Autopilot is running), editor_segment_limit (max 20 scenes).
/api/editor/:slug/segments/:id/use-asset
0 credits
Set one scene's video from an existing asset. Body: exactly one of upload_id or source_segment_id. Reuse a clip across scenes, or swap a generated scene for your footage.
The 200 data is the updated segment plus an impact object describing downstream continuity fallout — replacing a scene changes the frame the next continuity-enabled AI scene inherits. Keys: requires_regeneration (bool), stale_video_segment_ids / stale_video_positions (following completed AI scenes now stale), affected_segment_ids / affected_positions (the whole downstream chain), anchor_segment_id / anchor_position, stop_reason / stop_position, and action ("segment_asset_replace"). When requires_regeneration is true, re-generate the listed scenes before rendering — render rejects a stale timeline.
Large files use a resumable multipart flow (use it at ≥ 50 MB, 8 MB parts):
POST …/uploads/multipart/init, …/sign-part, …/complete, …/abort. init returns part_size + parts_count; sign each part, PUT its bytes, keep the ETag, then send the ordered parts list to complete (abort on failure).
Image assets — product, closing card, logo
Same presign → PUT → confirm shape (image/jpeg or image/png, ≤ 20 MB). Thread the s3_key from presign into confirm verbatim.
Presenter (cast_mode presenter_led)
When cast_mode is presenter_led, lock a single recurring presenter image that is injected into every AI scene. All four endpoints need video:generate and cost 0 credits; presign/confirm/generate require the project to be in presenter_led (else 422 cast_mode_not_presenter_led). The image is returned on the editor object as presenter_url (presigned) and presenter_origin (user_uploaded or generated).
Full flow
# 1. Presign (Content-Type MUST equal the mime_type you send here)
curl -X POST https://hubfluencer.com/api/editor/$SLUG/uploads/presign \
-H "authorization: Bearer $HF" -H "content-type: application/json" \
-d '{"filename":"clip.mp4","mime_type":"video/mp4","size_bytes":4821004}'
# 2. PUT the bytes to presigned_url with the SAME Content-Type
curl -X PUT "$PRESIGNED_URL" -H "content-type: video/mp4" --data-binary @clip.mp4
# 3. Confirm — queues processing (metadata + frame extraction)
curl -X POST https://hubfluencer.com/api/editor/$SLUG/uploads/510/confirm -H "authorization: Bearer $HF"
# 4. Poll GET /api/editor/$SLUG/uploads until the row's status is "ready"
# 5. Drop it on the timeline as a finished scene
curl -X POST https://hubfluencer.com/api/editor/$SLUG/segments/from-upload \
-H "authorization: Bearer $HF" -H "content-type: application/json" -d '{"upload_id":510}'
Renders
/api/editor/:slug/renders
video:read
Every render version with status and a presigned video_url. Use it to recover a finished URL or find a failed render to retry.
200 OK
{
"data": [
{ "id": 9, "version": 2, "status": "completed",
"video_url": "https://…/render.mp4?X-Amz-Expires=86400",
"duration_seconds": 18.0, "error_message": null,
"inserted_at": "2026-06-03T10:20:00Z" },
{ "id": 8, "version": 1, "status": "failed",
"video_url": null, "error_message": "forge_timeout" }
]
}
/api/editor/:slug/renders/:id/retry
0 credits
Re-run a failed render from its saved snapshot. Only failed renders are retryable (editor_render_not_retryable otherwise).
Product sources
The start of the "delegate my content" loop: turn a product page into facts and imagery. Every fetch is SERVER-SIDE and SSRF-guarded (https only, no redirects to internal ranges). Extraction is stateless; import stores a catalog asset. All three spend 0 credits, need video:generate, and are rate-limited to 30/min.
/api/product-sources/extract
0 credits
video:generate
Fetch a public product page (HTML only) and return parsed facts: name, description, benefits, price, brand, availability, plus candidate product image URLs and a site/brand logo URL. URLs only — images are NOT downloaded here. Body: {url} (absolute http(s), ≤2048 chars). 422 unsupported_content_type for a non-HTML page, too_large for an oversized one.
/api/product-sources/preview-image
0 credits
video:generate
Fetch an image at a public URL (JPEG/PNG, verified by magic-number sniffing — not just the Content-Type — and capped to a small preview) and return an inline data URL for review. Writes nothing, stores no asset. Body: {image_url}. 422 for an unsafe or non-image URL.
/api/product-sources/import-image
0 credits
video:generate
Fetch an image at a public URL (JPEG/PNG magic-number verified, size-capped at the catalog image limit) and store it as a ready CatalogAsset. Body: {image_url, product_profile_id?, link_as?}. With product_profile_id it links the asset to that profile as link_as "primary_image" (default) or "logo". 404 for an unknown/foreign profile; 422 for an unsafe or non-image URL.
Product & brand profiles
Reusable, user-owned identity so every campaign draft inherits it instead of re-describing the brand. A product profile grounds the AI (benefits, offer, proof points, audience, CTA, compliance); a brand profile steers tone/format and carries the logo. Plain CRUD; reads need video:read, writes need video:generate; all 0 credits. Only name is required on each.
/api/product-profiles
video:read
List / read your product profiles. GET /api/product-profiles/:id returns one. Pass an id to plan / hook-variations / campaign-pack create so the campaign is planned from a saved source of truth.
/api/product-profiles
0 credits
video:generate
Create a product fact sheet. Body: {name (required), description?, benefits?[], offer?, proof_points?[], audience?, cta?, banned_claims?[], brand_tone?, source_url?}. PATCH /api/product-profiles/:id patches any of these (0 credits); DELETE /api/product-profiles/:id removes it (204). Attach imagery via product-sources/import-image. 404 for an unknown/foreign id.
/api/brand-profiles
video:read
List / read your brand profiles (GET /api/brand-profiles/:id for one). Pass an id as brand_profile_id to POST /api/shorts / hook-variations / campaign-pack create / series so drafts inherit the brand + logo.
/api/brand-profiles
0 credits
video:generate
Create a brand identity. Body: {name (required), logo_s3_key?, primary_color?, secondary_color?, font_family?, tone_words?[], default_cta?, default_audience?, banned_claims?[], preferred_visual_language?, preferred_creative_format?}. Colors are hex (#09EFBE); preferred_visual_language / preferred_creative_format take the same value lists as Shorts/editor. PATCH /api/brand-profiles/:id patches any field; DELETE removes it (204).
/api/brand-profiles/:id/assets
0 credits
video:generate
Attach one of your catalog assets to a brand profile. Body: {catalog_asset_id, role} where role is logo | product_image | other. DELETE /api/brand-profiles/:id/assets/:asset_id removes a link — :asset_id is the LINK id (the profile's assets[].id), not the catalog_asset_id. 422 if the catalog asset isn't yours; 404 for an unknown/foreign profile.
Campaign packs
The planning container: plan a campaign, persist it as a pack, and materialize each item into an editable DRAFT (an editor/short video project or an image slider). Planning (plan + hook-variations) generates no media and consumes the FREE AI-assist quota — NOT credits. Persisting a pack and materializing items are 0 credits. Generating/rendering each draft is a SEPARATE, explicit, PAID step (autopilot / generate). Reads need video:read, writes need video:generate.
/api/campaign-packs/plan
0 credits
video:generate
Turn product facts into a campaign PLAN: hooks, recommended formats, a positioning line, and a per-item credit ESTIMATE. Persists nothing. Body: {product_profile_id | product{name,...}, goal?, platform?, language?}. Spends 0 credits but consumes 1 free AI assist (429 ai_assist_quota_exceeded when the daily quota is spent). Rate-limited to 30/min.
/api/campaign-packs/hook-variations
0 credits
video:generate
Turn product facts + an optional brand_profile_id into 3–6 distinct hook variations, each with a hook, angle, suggested output format (editor_ad/short/carousel), a draft script, a caption, hashtags, and a per-variation credit estimate. Persists nothing. Body: {product_profile_id | product{...}, brand_profile_id?, count? 3..6, goal?, platform?, language?}. 0 credits, 1 free AI assist. Rate-limited to 30/min.
/api/campaign-packs
0 credits
video:generate
Persist a pack plus any inline hook/caption TEXT items. editor_ad/short/carousel items are added via /variations (below), not here. Body: {title?, product_profile_id?, brand_profile_id?, brief?, source_type?, source_url?, language?, items?:[{kind:"hook"|"caption", ...}]}. Link a product/brand profile so materialized drafts inherit the image + logo. GET /api/campaign-packs lists your packs; GET /api/campaign-packs/:id returns one with its items.
/api/campaign-packs/:id/variations
0 credits
video:generate
Persist selected hook variations as items under the pack and, by default (materialize:true), turn each into a DRAFT. Body: {variations:[{suggested_format:"editor_ad"|"short"|"carousel", hook?, angle?, draft_script?, caption?, hashtags?, credit_estimate?}], materialize?}. At most 12 variations per call; rate-limited to 15/min. Per-variation best-effort: one that can't be materialized stays a "planned" item with a recorded reason while the rest still materialize — the response summary lists created/materialized/planned + skipped_reasons.
/api/campaign-packs/:id/items/:item_id/materialize
0 credits
video:generate
Materialize one planned editor_ad/short/carousel item into a DRAFT and link it back to the item. Idempotent — re-calling a just-materialized item returns the existing draft (no duplicate). Rate-limited to 60/min. 422 for a hook/caption item, an already-materialized item, or when you hit a draft limit. Each item carries its status (planned → draft → generating → completed | failed), the draft slug + deep-link route (editor_ad/short → the video project, carousel → the slider), and a credit estimate. Generate each draft EXPLICITLY next: POST /api/editor/:slug/autopilot for videos, POST /api/sliders/:slug/generate for carousels.
Series & episodes
A series is a reusable recurring-content show template ("Myth vs Fact", "Founder Tip") bound to an optional brand + product profile. The planner drafts upcoming episode IDEAS (free AI-assist quota); materialize turns an idea into a 0-credit draft grouped under the series' rolling campaign pack. Reads need video:read, writes need video:generate; all 0 credits.
/api/series
video:read
List / read your series (GET /api/series/:id for one). GET /api/series/:id/dashboard returns the series plus its episodes grouped into lanes — upcoming (ideas), drafted, published — with per-item performance notes.
/api/series
0 credits
video:generate
Create a show template. Body: {name (required), template (required), brand_profile_id?, product_profile_id?, cadence?, tone?, default_format?, status?}. default_format is editor_ad/short/carousel; status toggles active/paused/archived (a paused series stops surfacing new ideas). A supplied brand/product profile must be your own (422 invalid_brand_profile / invalid_product_profile). PATCH /api/series/:id patches any field; DELETE removes it (204).
/api/series/:id/episodes/plan
0 credits
video:generate
Draft 3–5 upcoming episode ideas (status "idea") for the series, grounded on its product profile and steered by its brand/tone/cadence. Body: {count? 3..5, language?}. Persists them as episodes. 0 credits but consumes 1 free AI assist (429 when the quota is spent). Rate-limited to 30/min. GET /api/series/:id/episodes lists episodes (optional ?status filter).
/api/series/:id/episodes/:episode_id/materialize
0 credits
video:generate
Turn an "idea" episode into an editable 0-credit DRAFT under the series' rolling pack, inheriting the series' brand. Idempotent — re-calling a just-materialized episode returns the existing draft. Rate-limited to 60/min. 422 for an already-materialized / non-idea episode or a draft limit. The episode carries its draft slug + route; generate it explicitly next.
/api/series/:id/episodes/:episode_id/mark-posted
0 credits
video:generate
Record that you posted a DRAFTED episode manually (drafted → published, stamps posted_at) — there is no platform posting API. Idempotent: re-marking a published episode returns 200 with the original posted_at. 422 invalid_status for an idea/archived episode (materialize it first); 404 for an unknown/foreign series or episode.
Performance
Attach lightweight performance snapshots + creative-attribute tags to a campaign-pack ITEM (item_id from GET /api/campaign-packs/:id), then read a CAUTIOUS aggregate over your own items. Reads need video:read, writes need video:generate; all 0 credits (only make-more consumes the free AI-assist quota).
/api/campaign-pack-items/:item_id/performance
0 credits
video:generate
Record one performance snapshot. Body: {platform, views?, likes?, comments?, shares?, clicks?, conversions?, revenue?, watch_time_seconds?, source?, captured_at?, metadata?}. Append-only — many snapshots per item (captured_at, defaulting to now, distinguishes them), NOT idempotency-keyed, so don't blindly retry. Rate-limited to 30/min. GET /api/campaign-pack-items/:item_id/performance lists them (newest first) with a small summary. 404 for a foreign/unknown item.
/api/campaign-pack-items/:item_id/attributes
0 credits
video:generate
Insert-or-update the SINGLE creative-attributes row the recommendation loop correlates with performance. Body: {hook_type?, creative_format?, visual_language?, product_category?, cta?, length?, caption_style?, metadata?}. Atomic upsert — calling it again updates the same row. GET /api/campaign-pack-items/:item_id/attributes returns the row or null. 404 for a foreign/unknown item.
/api/performance/recommendations
video:read
Aggregate your items that have BOTH attributes AND ≥1 snapshot into a cautious recommendation: the top hook_type / creative_format / visual_language by average views, your best items, and an HONEST confidence ("none" / "low" / "moderate", a function of sample size) plus a non-causal note. It NEVER claims causation, ROAS, or attribution — these are correlations in your own data, a hint to test. Present the note + confidence as-is. No LLM, no quota, no credits.
/api/campaign-pack-items/:item_id/make-more
0 credits
video:generate
Generate 3 follow-up hook variations from a winning item, threading its angle/format/hook while keeping its brand + product profile. 0 credits but consumes 1 free AI assist (429 when spent). Rate-limited to 30/min. 404 for a foreign/unknown item; 422 when the item has no usable product context. Feed a chosen variation into /variations to draft it.
Review links
Mint an expiring, hashed-token review link for ONE completed output so a reviewer opens /review/:token with no account. The plaintext token + URL are returned ONCE (only the hash is stored) — surface them immediately. Reads need video:read, writes need video:generate; all 0 credits.
/api/review-links
0 credits
video:generate
Create a review link for exactly one of your own COMPLETED outputs. Body: {video_result_id | slider_id (exactly one), campaign_pack_id?, expires_in_hours?, download_enabled?, version_label?}. expires_in_hours defaults to 168 (7 days) and is clamped to a supported range. 422 for a not-completed/foreign output or a foreign campaign pack. Returns {id, token, review_url, expires_at, ...} — the token appears ONCE.
/api/review-links
video:read
List your review links (newest first) with status (pending → viewed → approved | changes_requested | revoked) and a visible comment_count. Never returns the token. GET /api/review-links/:id returns one. DELETE /api/review-links/:id revokes a link (status → "revoked"; the URL stops working immediately).
Asset catalog
Your reusable asset library (images + videos). Uploading here lets an asset be reused across projects via the from-asset routes on shorts / sliders / editor / tracking. Reads need video:read, writes need video:generate; all 0 credits. Subscription-gated: these routes return 402 subscription_required without an active subscription.
/api/assets
video:read
List your catalog (newest first), each with media_type, mime, size, description, status, and a presigned asset_url; meta.quota carries your storage quota. Optional ?limit / ?offset (offset = page * limit). GET /api/assets/quota returns just the quota (limit / used / pending / reserved / remaining bytes).
/api/assets/presign
0 credits
video:generate
Presign an upload. Body: {filename, mime_type, size_bytes} → { data: {asset_id, presigned_url, s3_key} }. PUT the raw bytes to the presigned_url with header Content-Type: <the presigned mime>, then POST /api/assets/:id/confirm {duration_seconds?, width?, height?, description?} — confirm HEADs the object and validates size/type/duration (422 on mismatch). Images ≤ 20 MiB; videos ≤ 500 MB and REQUIRE duration_seconds ≤ 60. Rate-limited to 30/min.
Scheduled posts (content calendar)
Schedule a COMPLETED render for a future posting time. A reminder-only schedule (no social account attached) fires a push notification at the scheduled time — you post manually, then close the loop with mark-posted. An agent token (video:generate) can create, reschedule, cancel, and mark these reminder-only schedules, so it can run the whole content calendar. Attaching a social account turns a schedule into an AUTO-PUBLISH: that additionally requires account:admin and is app-only (do it signed in to the app), and auto-posting to TikTok/Instagram is gated on posting access besides. Reads need video:read.
/api/scheduled-posts
video:read
List your scheduled posts, ordered by scheduled_at ascending. Optional query params: ?from= / ?to= (ISO-8601 UTC bounds on scheduled_at) and ?status= (scheduled | reminded | posted | publishing | published | failed | canceled).
/api/scheduled-posts/:id
video:read
Fetch one of your scheduled posts. 404 for unknown/foreign ids.
/api/scheduled-posts
0 credits
video:generate
Schedule one of your COMPLETED renders for a future UTC instant (scheduled_at) on a target platform. WITHOUT a social account id this is a reminder-only schedule — an agent token (video:generate) can create it. WITH an account id (tiktok_account_id for "tiktok", instagram_account_id for "instagram", one of your own accounts) it is an AUTO-PUBLISH that additionally requires account:admin (403 insufficient_scope otherwise) AND that platform's posting feature (403 feature_disabled). Optionally grouped under a campaign pack item. Persists the schedule only; spends NO credits. At most 500 pending posts per user (422 scheduled_post_limit_reached beyond).
/api/scheduled-posts/:id
0 credits
video:generate
Reschedule a "scheduled" or "reminded" post to a new FUTURE scheduled_at (a reminded post is re-armed to "scheduled"). video:generate manages a reminder-only post; a post that carries a social account (or an update attaching one) additionally requires account:admin (403 insufficient_scope). 422 not_reschedulable / scheduled_in_past / invalid_scheduled_at; 404 for unknown/foreign ids.
/api/scheduled-posts/:id
0 credits
video:generate
Cancel a "scheduled", "reminded", or "failed" post (status -> "canceled"). video:generate cancels a reminder-only post; canceling a post that carries a social account additionally requires account:admin (403 insufficient_scope). 422 not_cancelable for an already posted/publishing/published/canceled post; 404 for unknown/foreign ids.
/api/scheduled-posts/:id/mark-posted
0 credits
video:generate
Confirm you posted this content manually: transitions a "scheduled" or "reminded" post to "posted" and stamps posted_at. Atomic — a post in any other status (including an already-posted one, so a second call) returns 422 not_markable. 404 for unknown/foreign ids. Touches no platform API; spends NO credits. Lets an agent close the reminder loop after posting.
AI assists
AI helper calls draw from a free daily quota of 20, account-wide — separate from credits. This covers every content-drafting endpoint: the editor's generate-scenario, generate-narration, enhance-prompt, and suggest-* (music-prompt / next-scene); shorts text/generate; the editor presenter/generate; campaign-packs plan and hook-variations; series episodes/plan; and make-more. When it runs out you either unlock more (1 credit → +10) or write the content yourself with the free PATCH endpoints.
/api/ai-assists
video:read
The current quota.
200 OK
{
"data": {
"used": 4, "limit": 20, "bonus": 0, "remaining": 16,
"resets_at": "2026-06-04T00:00:00Z",
"unlock_cost": 1, "unlock_batch_size": 10
}
}
/api/ai-assists/unlock
1 credit
video:generate
Spend 1 credit for +10 assists. It's a repeatable purchase — do NOT send a stable Idempotency-Key (a reused key replays the first unlock for 24h, silently skipping later purchases).
Errors, rate limits & polling
402
402 Payment Required
{ "error": "credits_insufficient", "required_credits": 15, "available_credits": 3 }
429
429 Too Many Requests
{ "error": "ai_assist_quota_exceeded", "remaining": 0,
"can_unlock": true, "unlock_cost": 1, "unlock_batch_size": 10 }
Rate limits
Where a route is rate-limited, the per-user cap is listed below; requests over it are rejected — back off for a minute and retry. Everything else is governed only by credits and the AI-assist quota.