REST API
The HTTP endpoints behind the editor, for scripts and tools that don't speak MCP.
GET /api/project- current project JSONPUT /api/project- replace project JSON (body = full document). Conflict-safe: if the body'srevision≤ the revision currently on disk, the server rejects with 409 and returns{"error":"…","revision":<current>}. Append?force=1to overwrite unconditionally. A request with anX-FableCut-Projectheader (the editor sends the id of the project it loaded) is also refused with 409 when another project is open (see Projects). Writes are atomic (tmp file + rename), so a crashed write never corrupts the file.GET /api/media- list files in ./media (name, src, size)GET /api/media-info?src=/media/x.mp4→{src, info}·POST /api/media-info {srcs:[…], stat?}→{info:{src: info}}(up to 2000;stat:trueanswers only{size, mtimeMs}, no ffprobe - how the editor checks for offline files) - a file's metadata from ffprobe:size,fps,codec,audioCodec,audioChannels,sampleRate,bitrate,duration,width,heightandprobe("ffprobe", or"stat"when only the size could be read).infoisnullwhen the src names no file under/media/or/library/. Cached per file version.POST /api/proxy {srcs:[…], height?:540|720|1080}→{status:{src: status}}queues proxies (ffmpeg, one at a time);POST /api/proxy/status {srcs}→{status:{src: status | null}};POST /api/proxy/delete {srcs}→{deleted:{src: n}}. A status is{state:"ready", src:"/proxies/…", width, height, size},{state:"queued"|"running", progress, height},{state:"failed", error}or{state:"none"}(null= no such source file). Progress and results are also pushed as SSE eventproxy({src, status}).GET /proxies/<name>serves a proxy file fromanalysis/proxies/./api/proxyanswers 503 without ffmpeg.POST /api/relink/candidates {media:[{id, name, src?, size?, kind?}]}→{candidates:{id:[{src, name, size, score, why}]}}- where a missing file might be now: files undermedia/andlibrary/(subfolders included) of the same kind, ranked by name (same name, same name with another extension, similar name) and size, best 5 each.GET /api/projects[?archived=only|all]→{active, projects:[{id, name, active, archived, notes, tags, created, opened, modified, width, height, fps, duration, clips, media, thumb}], templates:[…]}·POST /api/projects {action, …}- the actions offablecut_projects(plusimport {project, name?}). The answer depends on the action:{id, project}for open / new / duplicate / update / archive / unarchive / import (plusfromwhen the open project changed,alreadywhen it was open),{id, deleted}for delete,{templates}for templates,{template}for save_template (the saved one) and{template, deleted}for delete_template ·GET/PUT /api/projects/thumb?id=- a project's JPEG thumbnail (1 MB max). SSE eventprojects({action, id, active}) after each change. See Projects.GET /api/versions[?project=]→{project, versions:[{v, name, note, kind, created, revision, clips, duration, final}]}(newest first; default the open project) ·POST /api/versions {action, project?, version?, …}-save {name?, note?, final?}→{project, version}·diff {version, to?}→{same, lines, added, removed, changed, mediaAdded, mediaRemoved, settings}·restore {version}→{restored, saved}·update {version, name?, note?, final?}→{version}·delete {version}→{deleted}.GET /api/versions/file?project=&version=is that version's document;GET /api/projects/backup?id=a project with all its versions (import it withPOST /api/projects {action:"import", project:<the backup>}). SSE eventversions({project, action}). See Versions.GET /api/library?dir=sfx|elements|svg|fonts|presets|templates- list library assetsGET /api/presets·GET /api/templates- the saved presets / templates, validated (bad files skipped);POST /api/presets {preset}·POST /api/templates {template}- validate one and save it aslibrary/presets/<id>.json/library/templates/<id>.json, returning{id, name, file}POST /api/upload?name=foo.mp4- raw body saved into ./media, returns{src}. MP4/MOV/M4V uploads are auto-remuxed with+faststart(needs ffmpeg on PATH). Files copied straight into ./media by external tools skip this - remux big ones yourself (ffmpeg -i in.mp4 -c copy -movflags +faststart out.mp4) or playback stalls.POST /api/import-url- body{url:"https://…"}downloads the file into./media/and returns{src,name}(same-origin/media/…, like upload). HTTPS only; rejects localhost, private/link-local/CGNAT addresses, and redirects to those. Remote SVG is refused (/media/*.svgis served same-origin asimage/svg+xml- a scripted SVG opened as a document would run on the editor origin). Does not writeproject.json- register the media afterwards (UI andfablecut_import_mediado this). Do not put a rawhttps://URL (or another origin, includinglocalhoston a different port) inmedia.src: canvas CORS would taint the compositor and Fast / WebCodecs export cannot JPEG-encode. Import into./mediasosrcis/media/…, or serve the remote withAccess-Control-Allow-Origin.GET /api/transcript?src=/media/x.mp4→{transcript}(nullwhen the file has none yet; 404 when the file is missing) ·POST /api/transcript?src=(a transcript document) →{src, words}·POST /api/transcript/status {srcs}→{status:{src: {words, language, model, duration, created} | false | null}}·POST /api/transcript/delete {src}→{deleted}. SSE eventtranscript({src}) after each save or delete.POST /api/transcribe/request {transcribe:{srcs, model?, language?, again?}, where?}queues a transcription on the export-job queue (pollGET /api/export/job?id=); the tab saves each transcript, and the job'sresultlistsdone,skippedandfailed. See Transcripts and text-based editing.POST /api/denoise- body{src:"/media/…", amount:"light"|"medium"|"strong"}: writes a noise-reduced FLAC of the whole file into./media/(reused if it exists) and returns{src, name, duration, channels}. Does not touchproject.json(see Audio mix).POST /api/analyze- body{src:"/media/ref.mp4", threshold?, music?}: analyze a reference video into an edit blueprint (see Remake a reference video); extracts its music into ./media.GET /api/analyze?src=…returns the cached blueprint.GET /api/frame?src=/media/x.mp4&t=3.2- one JPEG still of a source clip, the "eyes" to go with/api/analyze's ears (see Look at the footage). Optionalw(pixel width, default 768) andq(ffmpeg-q:v1–31, default 4).&frames=N[&cols=][&from=][&to=][&w=320]instead returns one N-up contact sheet across the clip (N 2–60;colsomitted → the squarest layout, so 6 frames give 3×2 rather than 4×2 with two black cells).srcmust name an existing file under/media/or/library/(404 otherwise).?path=reads any file on the machine but only with theX-FableCut-Client: mcprequest header, which only the local MCP client sends - without it the request is refused (403) before the path is even resolved. Results are cached inanalysis/frames/keyed by source path, size and mtime; the cache is capped (500 files / 128 MB, oldest first). The response carriesX-FableCut-Frame-Times(comma-separated sample times, so a sheet's cells can be mapped back to timestamps) andX-FableCut-Frame-Source.POST /api/scopes/requestalso takes{"image":true}, which makes the editor return the measured still as a JPEG data URL inresult.image- howfablecut_frame {time}shows an agent the composed program picture.GET /api/events- SSE: named eventchangewhen project.json, ./media or ./library changes; named eventprofileswhenencoding-profiles.jsonchanges (UI refreshes the export-profile list only - no project reload); named eventproxyas a proxy is queued, makes progress, finishes or is deleted- Fast / WebCodecs export (browser compositor → server ffmpeg):
GET /api/export/ffmpeg→{available}·GET /api/export/profiles[?detail=1]→{default, profiles, issues}·POST /api/export/begin{fps,name,mode?,profile?,hasAudio?,pixelFormat?,width?,height?}→{id,mode,profile?,label,summary}(fpsis required - passproject.fps, no server-side default;modeis"jpeg"(default, Fast) or"annexb"(WebCodecs H.264 elementary stream); jpeg 400 ifprofileis not a defined id, or if ffmpeg rejects its args in the dry run; optionalpixelFormat:"rgba"pluswidth/heightpipes raw canvas frames instead of JPEG) ·POST /api/export/frame?id=(JPEG body for jpeg mode; raw RGBA ifbeginusedpixelFormat:"rgba"; Annex-B for annexb - one POST may concatenate frames or AUs. Must be after audio; ffmpeg is spawned on the first frame in both modes) ·POST /api/export/audio?id=(WAV body - must be sent before the first frame) ·POST /api/export/end?id=[&discard=1]→{src}under/exports/ - Export jobs (what
fablecut_exportdrives):POST /api/export/request{range?, profile?, where?:"auto"|"tab"|"headless"}→{id, status, via, …}(409 if one is already running, or nothing can render it) ·GET /api/export/job?id=→{status: pending|running|done|failed|cancelled, progress 0–1, src, error}·POST /api/export/job/cancel?id=. The editor tab side: SSE eventexport(a ticket),GET /api/export/job/ticket?id=,POST /api/export/job/claim?id=(first tab wins, else 409),POST /api/export/job/report?id={progress} | {status:"done", src} | {status:"failed", error}→{cancel}.