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.

POST /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"
  }
}
GET /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" }
  ]
}
DELETE /api/tokens/:id account:admin

Revoke a token. Returns 204 No Content.

Credits & voices

GET /api/studio/credits video:read

Current credit balance. Check it before generating; a short costs 15 credits.

200 OK
{ "data": { "credits": 120 } }
GET /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.

POST /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"
  }
}
POST /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"
POST /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"
GET /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"
    }
  }
}
GET /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).

Method & path Purpose
POST /api/shorts/:slug/product/presign · /product/confirm Attach a product image woven into the footage. presign {mime_type, size_bytes} → { data: { presigned_url, s3_key } }; confirm {s3_key, product_description?}.
POST /api/shorts/:slug/poster/presign · /poster/confirm Set the end-card poster (a closing still that extends the render to 14s). presign {content_type?} → { upload_url, s3_key }; confirm {s3_key}.
GET /api/shorts lists your in-progress/failed shorts. Add ?include_completed=true to also list finished ones (handy for recovering a slug).

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.

Rate limits: generate 10/min; restyle and the per-slide edit 20/min each.
POST /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.
Per-slide on-image text (set/edited via the slide PATCH below): headline ≤120, body ≤600, kicker ≤40 chars. kicker is the small eyebrow/label line shown above the headline.
POST /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.

GET /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).

PATCH /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.

POST /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.)

GET /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.

POST /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.
POST /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"
    }
  }
}
autopilot_status: running → keep polling; completed → done; failed/cancelled → stop (read autopilot_error_message). The MP4 is ready when latest_render.status is completed with a video_url.

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.

Method & path Body Cost
GET /api/editor/:slug — full project state free
PATCH /api/editor/:slug narration_script?, target_duration_seconds?, creative_format?, visual_language?, theme? free
POST /api/editor/:slug/generate-scenario segments_count? (3–10), creative_format?, visual_language?, theme? assist
PATCH /api/editor/:slug/scenario scenario_prompt (1–50000) — style fields not accepted here free
POST /api/editor/:slug/apply-scenario segments_count (0,3,5,7,10) free
POST /api/editor/:slug/segments prompt (1–2000) free
PATCH /api/editor/:slug/segments/:id prompt (1–2000) free
DELETE /api/editor/:slug/segments/:id free
PUT /api/editor/:slug/segments/reorder order: [ids] free
POST /api/editor/:slug/segments/:id/generate — render one scene 5 cr
POST /api/editor/:slug/segments/:id/regenerate 4 cr
POST /api/editor/:slug/batch-generate — all pending scenes 4 cr/seg
POST /api/editor/:slug/generate-narration — from the timeline assist
POST /api/editor/:slug/generate-voice voice_id (required) 3 cr
POST /api/editor/:slug/generate-music prompt? (≤1200) 5 cr
POST /api/editor/:slug/render — final MP4 0 cr*
POST /api/editor/:slug/segments/:id/enhance-prompt assist
POST /api/editor/:slug/suggest-next-scene assist
POST /api/editor/:slug/suggest-music-prompt assist

* 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.

POST /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
  }
}
POST /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.

GET /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] }
  ]
}
POST /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).

POST /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).

The one header rule that breaks raw uploads: a single-object PUT — the small-file path above and every image — MUST send Content-Type matching the mime you presigned (confirm HEADs the object and 422s on a mismatch). A multipart part PUT must send NO Content-Type header at all — the part URL doesn't sign one, so an extra header breaks the S3 signature.

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.

Method & path Purpose
POST /api/editor/:slug/product/presign · /product/confirm Attach a product photo (features across AI scenes).
POST /api/editor/:slug/product/apply-default-placement Body placement: "throughout" or "end" (end needs a first+last-frame-capable model).
POST /api/editor/:slug/closing-image/presign · /confirm Upload a closing-card image.
POST /api/editor/:slug/closing-image/from-product Reuse the product image (0-credit copy); overwrite? to replace.
POST /api/editor/:slug/logo/presign · /confirm · PATCH /logo Overlay a brand logo (PNG/JPEG); PATCH sets treatment/position/duration.

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

GET /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" }
  ]
}
POST /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.

GET /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
  }
}
POST /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

HTTP error What to do
401 Unauthorized Bad/expired/missing token — re-auth.
402 credits_insufficient Out of credits (body has required). Stop, don't loop.
403 insufficient_scope Token lacks the scope (e.g. video:generate).
409 *_in_progress Already running — keep polling, don't re-POST.
422 validation Fix fields; body is errors: field → messages.
429 ai_assist_quota_exceeded Daily assist quota used up — unlock once or write content yourself. NOT a credit error.

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 }
Result URLs (video_url) are presigned MP4s with a ~24h TTL — download promptly, they're not permalinks. Poll every ~15s; generation takes a few minutes (autopilot can run longer). Treat a still-rendering/running project as healthy; give up only at a stated max wait. Publishing to TikTok/Instagram needs a human-linked account — return the MP4 + a caption instead.