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.
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": 15 }
{ "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.
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. Two calls: create a draft (free), then generate (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 | e.g. "en" (default). |
headline optional |
string | On-screen TITLE overlay (≤160). |
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. |
music_vibe optional |
string | Upbeat (default), Cinematic, Minimal, Luxury, Playful, Jazz. |
short_text_position, short_text_animation,
short_font_family, music_instruments
optional
|
string / array | Overlay position (top/center/bottom), animation (reveal/typewriter/fade_in/pop/bounce), font, and instrument hints. |
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. No video credits are spent and no render starts.
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.
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
video:read
Poll for status. stage is the signal: draft → rendering → video_ready (read latest_render.video_url) | failed (read failed_stage + error_message).
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.
Branding images — product & end-card poster
Both are 0 credits (jpeg/png, ≤ 20 MB) and use a presign → PUT raw bytes → confirm flow. On the single PUT, send Content-Type matching the presigned mime. Shorts have no logo overlay (that's editor-only).
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.
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.
/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.
| Body field | Type | Notes |
|---|---|---|
language required |
string | 2–10 chars, e.g. "en". |
product_prompt |
string | Empty OR 10–5000 chars. Required (≥10) if you'll run autopilot. |
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. |
/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.
Request
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"
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"
}
}
}
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.
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.
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.
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).
AI assists
AI helper calls (generate-scenario, generate-narration, enhance-prompt, suggest-*) draw from a free daily quota of 20, account-wide — separate from credits. 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 & polling
402
402 Payment Required
{ "error": "credits_insufficient", "required": 15 }
429
429 Too Many Requests
{ "error": "ai_assist_quota_exceeded", "remaining": 0,
"can_unlock": true, "unlock_cost": 1, "unlock_batch_size": 10 }