FicReel
Developer docs

FicReel API

Content engine (/v1) + accounts. Base URL: https://api.ficreel.com

Overview

The FicReel backend = two domains: the content engine (/v1 — presets, worlds, generation, rendering, quality checks, cost) and the account domain (auth, quota, subscriptions, admin). Frontend, CLI and any agent share the same API. All public traffic goes through the API gateway (auth / quota / rate limiting / event tracking).

Endpoint classAuth requiredNotes
Reads — GET /v1/worlds, styles, shots, tasks, cost, …Public (rate limited to 120/min)World cards, world pages and task status are visible anonymously
Generation — POST /v1/generations/*JWT or device ID requiredQuota is pre-reserved; settled or refunded automatically at terminal state; 402 when out of credits
Render — POST /v1/renders, POST /v1/projects/:id/exportJWT or device ID requiredCPU-heavy, strict rate limit (5/min), 0 credits. A project export is rejected with 409 music_not_ready while a submitted score is still rendering
Writes (worlds / entities / preset registration, verify confirm)Admin onlyContent assets are maintained by the official team
Asset upload / quality checks / task cancelSigned-in user or deviceUsers can upload reference images, run checks and cancel their own tasks
Admin JWTBypasses quotaCLI workflows; events are still tracked
  • Task model: submit → poll GET /v1/tasks/{id} → creations[].url. Outputs are mirrored to COS + EdgeOne CDN automatically (cdn.ficreel.com/ce/...); local /v1/files is the fallback path.
  • Credit rates: image 3 credits, video 4 credits/second, music 7 credits, render 0 credits (server compute). Credits are the billing unit — /pricing shows what each plan costs. New devices get 150 trial credits, which cover one complete 30-second watermarked short (6 shots + a score) with a little left for a retry.
  • Rate limits: reads 120/min/IP, writes 5/min/IP (in-memory token bucket, per node).

Projects (storyboard → per-shot generation → export)

A project is the server-side timeline for one reel: the storyboard skeleton plus the generated per-shot clips, the music bed and the final export. The wizard stops at the storyboard skeleton; per-shot generation, music and export all happen in the editor — there is no one-click full film.

MethodPathDescriptionAuth
POST/v1/projects{title?, worldview_id, character_id?, style_id, story, shots?=9, shot_seconds?=5} → 201 {project}JWT/device
GET/v1/projectsProject list → {projects:[{id,title,updated_at,segment_count,cover_url}]}JWT/device
GET/v1/projects/:idProject detail → {project}JWT/device
PATCH/v1/projects/:id{title?, segments?} → {project} (editor timeline write-back)JWT/device
POST/v1/projects/:id/segments/:segmentId/generateGenerate one shot → 202 {task_id, credits_reserved}; poll GET /v1/tasks/{id}JWT/device (video 4 credits/sec)
POST/v1/projects/:id/music{seconds?} → 202 {task_id}; poll GET /v1/tasks/{id}JWT/device (7 credits)
POST/v1/projects/:id/exportExport the cut → 202 {task_id} when async, or 200 {output_url} when it finishes inside the server window. 409 {error:"music_not_ready", music_state, detail} while a submitted score is still rendering — poll the score task, then retryJWT/device (0 credits, strict rate limit)
project fieldTypeNotes
id, titlestringStable project id; title is user-editable via PATCH
worldview_id, character_id?, style_idstringPreset ids; character_id is optional
storystringSource text the skeleton was built from
modelstringAlways ficreel-1.0
segments[]arrayid, index, seconds, narration, camera, subject, environment, lighting, prompt, state, task_id?, url?, error?
segments[].statestringempty | queued | running | done | failed — an asset only exists when state is done and url is set
music, exportobject | nullMusic bed and export result; null until that step has run
creditsobjectavailable / reserved / consumed for this project
created_at, updated_atstringISO timestamps
  • Live since 2026-09-14: these endpoints are served in production. Creating a project costs 0 credits — it only plans the shots.
  • Workflow: the wizard only produces the skeleton (segments start in state "empty" and cost 0 credits). Generation is per shot, in the editor; music and export are separate calls. There is no single request that renders a whole film.
  • Costing: image 3 credits, video 4 credits/second, music 7 credits, render 0 credits. A generation reserves credits up front and settles or refunds at terminal state. A 45-second film = 9 × 5s shots (180) + one score (7) = 187 credits.
  • Export timing: call POST /v1/projects/:id/music first and poll GET /v1/tasks/{id} until the score is terminal (music.state === "done" or "failed") before exporting. Exporting while a submitted score is still rendering is rejected with 409 {error:"music_not_ready", music_state, detail} — no cut is produced, so you can never receive a silent cut without audio through this path. Only an export with no score at all comes back without audio; check export.note when present.

Presets (five-block assembly assets)

Four asset classes — worldview / style / character / shot. Registrable, referenceable, composable. Public presets are original (platform-neutral, D009).

MethodPathDescriptionAuth
POST/v1/{worldviews|styles|characters|shots}Register a preset (id + asset body, idempotent upsert); shot fields documented belowadmin
GET/v1/{worldviews|styles|characters|shots}List all presets of that classPublic
GET/v1/{worldviews|styles|characters|shots}/:idSingle preset detailPublic
ClassBuilt-in seed idsNotes
worldviewshio-morning / white-mist-station / kasumi-shrineContent layer: era/architecture/atmosphere/palette/avoid
styleanime-90s / painterly-modern-v2 / anime-modern / photoreal-cos / cinematic-3dLook layer: prompt_block + style reference images (required for photoreal)
charactershio / saku / shizukiCharacter bible: hair/eyes/outfit/signatures/aura
shotcloseup-eyes / high-angle-gaze / low-angle-gaze / reach-to-camera / platform-walk / portrait-halfShot template: camera + motion + duration + aspect_ratio (+ optional resolution / movement_amplitude)
  • Shot payload fields: camera (camera-language block), motion (subject movement), avoid_list, duration, aspect_ratio, resolution?, movement_amplitude? — when gen video does not pass duration/aspect explicitly, the shot defaults are applied.

World Space

Shared worldview universes: World Card → World Page (modules JSON protocol) → Entity (characters / items / scenes / rules).

MethodPathDescriptionAuth
POST/v1/worldsCreate a world + page skeleton {id?, name, tagline?, theme_tpl?, modules[]?}admin
GET/v1/worldsWorld list (World Card data source)Public
GET/v1/worlds/:idWorld metadata + canon_version + tagsPublic
PATCH/v1/worlds/:idUpdate name/tagline/preview_asset_id/theme_tpl/tagsadmin
GET/v1/worlds/:id/pagePage render data (full modules list)Public
PATCH/v1/worlds/:id/pageDIY entry: add/remove/reorder modules, switch themeadmin
POST/v1/worlds/:worldId/entitiesRegister an entity {kind: character|item|scene|rule|timeline, name, bible, anchors?, tags?}admin
GET/v1/worlds/:worldId/entities?kind=Entities inside a worldPublic
GET/PATCH/v1/entities/:idEntity detail / edit (automatic version history)read public / write admin
GET/v1/entities/:id/versionsVersion history (audit / rollback)Public
  • Pages are data, not code: modules[] are rendered by the frontend renderer according to theme_tpl (classic template live: hero/characters/items/scenes/codex/gallery).

Generation (unified task model)

Image R2I / video R2V / music text2audio. Video requires reference2video: without reference images the server auto-generates the character baseline image first.

MethodPathDescriptionAuth
POST/v1/generations/image{worldview_id, style_id, character_id?, shot_id?, prompt_override?, prompt?, resolution?, n?} → {task_id, prompt}JWT/device (3 credits)
POST/v1/generations/video{model?, duration, aspect_ratio, resolution, movement_amplitude, references[], worldview_id, style_id, character_id, shot_id, seed?, off_peak?} → {task_id, prompt}JWT/device (4 credits/sec)
POST/v1/generations/audio{prompt, duration?, model?, off_peak?} → {task_id} (model is optional and always resolves to ficreel-1.0)JWT/device (7 credits)
POST/v1/generations/tts{text, voice?} → 202 {task_id}; narration audio (audio track), queued like any other taskJWT/device (2 credits per item, +1 per 200 characters)
ModelPurposeCredits
ficreel-1.0Image (reference-to-image)3 credits per image
ficreel-1.0Video (image-to-video, 3–15s)4 credits per second
ficreel-1.0Music (text-to-audio)7 credits per track
ficreel-1.0Narration (text-to-speech)2 credits per item (≤200 characters), +1 per further 200
ficreel-1.0Render (EDL edit + post)0 credits
  • Each references element = a public http(s) URL or a /v1/files/ relative path (converted to public automatically).
  • prompt_override / prompt semantics = full replacement (not append); when omitted, the engine assembles the five blocks.
  • off_peak = off-peak queue (upstream occasionally returns InternalServiceFailure — no credits charged, the engine retries once on the on-peak queue automatically).
  • Failed generations roll quota back automatically; successes settle against the pre-reserve; the ledger is queryable via /v1/cost.

Tasks (polling / cancel)

MethodPathDescriptionAuth
GET/v1/tasks/:idTask state: created → queueing → processing → success|failed|cancelled; creations[{url,type}], error, meta (originalUrls = upstream public URLs, valid 24h)Public
POST/v1/tasks/:id/cancelCancel a task (terminal states cannot be cancelled)JWT/device

Rendering (EDL edit + post)

Deterministic post chain: edit + color grade + music + watermark. light tier is synchronous only when the cut finishes inside the server window (~15s) — longer cuts come back as 202 and must be polled.

MethodPathDescriptionAuth
POST/v1/rendersEDL payload (below). Finishing inside the ~15s window → 200 + creations[].url. Not finishing → 202 + {task_id, state:"processing"}; you MUST then poll GET /v1/tasks/{id} until success/failed. A 2xx response does NOT by itself mean the cut is ready. Status codes are passed through from the render engine, so upstream 4xx come back as 4xx (not 502). This endpoint takes a raw EDL and has no project context; the project export endpoint POST /v1/projects/:id/export additionally returns 409 {error:"music_not_ready", music_state, detail} while a score you submitted is still rendering (queued/running/created) — no cut is produced, so poll the score task to success/failed (or drop the score) and export again.JWT/device (0 credits, strict rate limit)
{
  "segments": [{ "src": "<local path or /v1/files/...>", "in": 0, "dur": 0, "speed": 1 }],
  "grade": { "saturation": 1.14, "contrast": 1.05, "bloom": 0.22, "vignette": "PI/5.5", "grain": 3 },
  "tier": "light | heavy-2k | heavy-interp",
  "output": { "w": 1080, "h": 1920, "fps": 25 },
  "title": "ASCII only (non-ASCII titles skip the title card)",
  "audio": { "src": "<bgm local path or /v1/files/...>", "volume": 0.35, "loop": true },
  "watermark": false
}
  • dur: 0 = the whole segment (server probes the duration); speed ≠ 1 changes playback speed.
  • Score ordering: a submitted score is still rendering while GET /v1/projects/:id reports music.state of queued/running/created. Exporting a project in that window is refused with 409 music_not_ready (body: {error, music_state, task_id, detail}) instead of producing a cut without audio — poll the score task until success/failed, then export again.
  • tier: light (default, ≈7s per 10s of footage) / heavy-2k (2K upscale, async) / heavy-interp (60fps interpolation, ≈4.5 min, off by default). "light" is synchronous only up to the server window (~15s of wall-clock); anything longer returns 202 + task_id and must be polled.
  • watermark = trial preview watermark (persistent bottom-right corner); paying plans omit it.

Quality checks (verify)

A 6-frame grid + metric checks + a human confirmation slot; the generate → verify → retry programmatic loop.

MethodPathDescriptionAuth
POST/v1/verify{creation_urls[]} or {latest: "render"|"gen"} → VerifyReport (6-frame grid + dur/fps/black frames + humanConfirm); the grid is served via CDNJWT/device
GET/v1/verify/:idReport detail (includes ownerType/ownerId)Public
PATCH/v1/verify/:idHuman confirmation {human_confirm: pending|approved|rejected, note?}. Callers may only confirm a report they own (their device, or the account that device is bound to); reports created before ownership existed are admin-only.JWT/device (owner match)

Prompt assembly (presets/build)

MethodPathDescriptionAuth
POST/v1/presets/build{worldview_id, style_id, character_id?, shot_id, prompt_override?} → {worldview, character, motion, camera, style, anchor, combined}Public
  • Assembly order: worldview → character → motion → camera → style → anchor; character_id accepts preset characters or in-world entities.

Cost ledger

MethodPathDescriptionAuth
GET/v1/cost?month=YYYY-MMCredit ledger: total / offPeak / onPeak / byModel — per-model credit consumption in FicReel credits, in the same unit as the credits shown above.Public

File serving

MethodPathDescriptionAuth
GET/v1/files/*Output download: raw binary passthrough, Range 206 streaming (Content-Range + Accept-Ranges), correct Content-Type, CORS enabled for the site origin — use this when you need a same-origin fetch() download (public distribution goes through cdn.ficreel.com, which does not send CORS headers on plain GET).Public

Accounts (auth / quota / subscription / admin)

MethodPathDescriptionAuth
POST/auth/otp/sendEmail OTP send (Resend; logs the code when no key is configured)Public
POST/auth/otp/verifyVerify code → JWT + user (admin emails get the admin role)Public
POST/auth/password/loginPassword login (set a password first via /auth/password/set)Public
POST/devicesDevice registration: 150 trial credits + KOL attribution (?ref=)Public
GET/meQuota (available/reserved/consumed) + subscription stateJWT/device
GET/me/credits/transactionsCredit ledger (consumption detail)JWT/device
GET/plansSubscription tiers (starter/creator/studio + add-on packs)Public
POST/checkoutPaddle checkout (subscription/add-on; attribution code travels in custom_data)JWT
POST/webhooks/paddlePaddle events (signature check → subscription booking/refund)Paddle signature
GET/admin/stats | /admin/users | /admin/applicationsAdmin: stats/users/applicationsadmin
POST/admin/users/:id/quotaAdjust quota (ledger delta, visible in transactions)admin
GET/POST/admin/db/tables | /admin/db/queryRead-only database browseradmin
POST/eventsBusiness event tracking (19 event types: conversion / refund / KOL attribution / quota consumption, …)Public (with device header)
  • The legacy creator tools (/projects, /workbench node graph, /generations old pipeline) still work, based on the generation domain (that pipeline is temporarily disabled); all new features go through /v1.

Examples

# CLI (log in once, then everything goes through the gateway; admin JWT bypasses quota)
node apps/cli/src/index.mjs login --token <JWT> --base https://api.ficreel.com
node apps/cli/src/index.mjs preset build --world white-mist-station --style anime-90s --character saku --shot platform-walk
node apps/cli/src/index.mjs gen video --preset shio-morning --audio <bgm> --out out/
node apps/cli/src/index.mjs verify --jobs render --latest
node apps/cli/src/index.mjs cost --month 2026-09

# curl (generate a video: signed-in user, quota pre-reserved)
curl -X POST https://api.ficreel.com/v1/generations/video \
  -H "Authorization: Bearer <JWT>" -H "Content-Type: application/json" \
  -d '{"worldview_id":"white-mist-station","style_id":"anime-90s","character_id":"saku","shot_id":"platform-walk","duration":5}'
# → {"task_id":"...","prompt":"..."}
curl https://api.ficreel.com/v1/tasks/<task_id>   # poll until success → creations[].url

# curl (create a project — the storyboard skeleton; per-shot generation comes later)
curl -X POST https://api.ficreel.com/v1/projects \
  -H "Authorization: Bearer <JWT>" -H "Content-Type: application/json" \
  -d '{"worldview_id":"white-mist-station","character_id":"saku","style_id":"anime-90s","story":"A girl waits for a train that never comes.","shots":9,"shot_seconds":5}'
# → 201 {"project":{"model":"ficreel-1.0","segments":[{"state":"empty"}]}}