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 class | Auth required | Notes |
|---|
| 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 required | Quota is pre-reserved; settled or refunded automatically at terminal state; 402 when out of credits |
| Render — POST /v1/renders, POST /v1/projects/:id/export | JWT or device ID required | CPU-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 only | Content assets are maintained by the official team |
| Asset upload / quality checks / task cancel | Signed-in user or device | Users can upload reference images, run checks and cancel their own tasks |
| Admin JWT | Bypasses quota | CLI 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.
| Method | Path | Description | Auth |
|---|
| POST | /v1/projects | {title?, worldview_id, character_id?, style_id, story, shots?=9, shot_seconds?=5} → 201 {project} | JWT/device |
| GET | /v1/projects | Project list → {projects:[{id,title,updated_at,segment_count,cover_url}]} | JWT/device |
| GET | /v1/projects/:id | Project detail → {project} | JWT/device |
| PATCH | /v1/projects/:id | {title?, segments?} → {project} (editor timeline write-back) | JWT/device |
| POST | /v1/projects/:id/segments/:segmentId/generate | Generate 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/export | Export 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 retry | JWT/device (0 credits, strict rate limit) |
| project field | Type | Notes |
|---|
| id, title | string | Stable project id; title is user-editable via PATCH |
| worldview_id, character_id?, style_id | string | Preset ids; character_id is optional |
| story | string | Source text the skeleton was built from |
| model | string | Always ficreel-1.0 |
| segments[] | array | id, index, seconds, narration, camera, subject, environment, lighting, prompt, state, task_id?, url?, error? |
| segments[].state | string | empty | queued | running | done | failed — an asset only exists when state is done and url is set |
| music, export | object | null | Music bed and export result; null until that step has run |
| credits | object | available / reserved / consumed for this project |
| created_at, updated_at | string | ISO 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).
| Method | Path | Description | Auth |
|---|
| POST | /v1/{worldviews|styles|characters|shots} | Register a preset (id + asset body, idempotent upsert); shot fields documented below | admin |
| GET | /v1/{worldviews|styles|characters|shots} | List all presets of that class | Public |
| GET | /v1/{worldviews|styles|characters|shots}/:id | Single preset detail | Public |
| Class | Built-in seed ids | Notes |
|---|
| worldview | shio-morning / white-mist-station / kasumi-shrine | Content layer: era/architecture/atmosphere/palette/avoid |
| style | anime-90s / painterly-modern-v2 / anime-modern / photoreal-cos / cinematic-3d | Look layer: prompt_block + style reference images (required for photoreal) |
| character | shio / saku / shizuki | Character bible: hair/eyes/outfit/signatures/aura |
| shot | closeup-eyes / high-angle-gaze / low-angle-gaze / reach-to-camera / platform-walk / portrait-half | Shot 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).
| Method | Path | Description | Auth |
|---|
| POST | /v1/worlds | Create a world + page skeleton {id?, name, tagline?, theme_tpl?, modules[]?} | admin |
| GET | /v1/worlds | World list (World Card data source) | Public |
| GET | /v1/worlds/:id | World metadata + canon_version + tags | Public |
| PATCH | /v1/worlds/:id | Update name/tagline/preview_asset_id/theme_tpl/tags | admin |
| GET | /v1/worlds/:id/page | Page render data (full modules list) | Public |
| PATCH | /v1/worlds/:id/page | DIY entry: add/remove/reorder modules, switch theme | admin |
| POST | /v1/worlds/:worldId/entities | Register an entity {kind: character|item|scene|rule|timeline, name, bible, anchors?, tags?} | admin |
| GET | /v1/worlds/:worldId/entities?kind= | Entities inside a world | Public |
| GET/PATCH | /v1/entities/:id | Entity detail / edit (automatic version history) | read public / write admin |
| GET | /v1/entities/:id/versions | Version 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.
| Method | Path | Description | Auth |
|---|
| 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 task | JWT/device (2 credits per item, +1 per 200 characters) |
| Model | Purpose | Credits |
|---|
| ficreel-1.0 | Image (reference-to-image) | 3 credits per image |
| ficreel-1.0 | Video (image-to-video, 3–15s) | 4 credits per second |
| ficreel-1.0 | Music (text-to-audio) | 7 credits per track |
| ficreel-1.0 | Narration (text-to-speech) | 2 credits per item (≤200 characters), +1 per further 200 |
| ficreel-1.0 | Render (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)
| Method | Path | Description | Auth |
|---|
| GET | /v1/tasks/:id | Task state: created → queueing → processing → success|failed|cancelled; creations[{url,type}], error, meta (originalUrls = upstream public URLs, valid 24h) | Public |
| POST | /v1/tasks/:id/cancel | Cancel 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.
| Method | Path | Description | Auth |
|---|
| POST | /v1/renders | EDL 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.
| Method | Path | Description | Auth |
|---|
| POST | /v1/verify | {creation_urls[]} or {latest: "render"|"gen"} → VerifyReport (6-frame grid + dur/fps/black frames + humanConfirm); the grid is served via CDN | JWT/device |
| GET | /v1/verify/:id | Report detail (includes ownerType/ownerId) | Public |
| PATCH | /v1/verify/:id | Human 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)
| Method | Path | Description | Auth |
|---|
| 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
| Method | Path | Description | Auth |
|---|
| GET | /v1/cost?month=YYYY-MM | Credit ledger: total / offPeak / onPeak / byModel — per-model credit consumption in FicReel credits, in the same unit as the credits shown above. | Public |
File serving
| Method | Path | Description | Auth |
|---|
| 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)
| Method | Path | Description | Auth |
|---|
| POST | /auth/otp/send | Email OTP send (Resend; logs the code when no key is configured) | Public |
| POST | /auth/otp/verify | Verify code → JWT + user (admin emails get the admin role) | Public |
| POST | /auth/password/login | Password login (set a password first via /auth/password/set) | Public |
| POST | /devices | Device registration: 150 trial credits + KOL attribution (?ref=) | Public |
| GET | /me | Quota (available/reserved/consumed) + subscription state | JWT/device |
| GET | /me/credits/transactions | Credit ledger (consumption detail) | JWT/device |
| GET | /plans | Subscription tiers (starter/creator/studio + add-on packs) | Public |
| POST | /checkout | Paddle checkout (subscription/add-on; attribution code travels in custom_data) | JWT |
| POST | /webhooks/paddle | Paddle events (signature check → subscription booking/refund) | Paddle signature |
| GET | /admin/stats | /admin/users | /admin/applications | Admin: stats/users/applications | admin |
| POST | /admin/users/:id/quota | Adjust quota (ledger delta, visible in transactions) | admin |
| GET/POST | /admin/db/tables | /admin/db/query | Read-only database browser | admin |
| POST | /events | Business 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"}]}}