HTTP API
The daemon's local API and its event stream.
The daemon's local API, used by the CLI, the pane, and the screening page. Schemas: contracts/src/api.ts.
The daemon
The daemon listens on http://127.0.0.1:4317 (or the port rapid serve --port gives it), on 127.0.0.1 only. Routes live under /api/; everything else is the screening page.
Every JSON answer is one of:
{ "ok": true, "data": { } }
{ "ok": false, "error": { "code": "not_found", "message": "no job nope" } }
Error codes are bad_request, not_found, conflict, and internal. A POST must send content-type: application/json, or it gets bad_request.
Routes
| Method and path | Body | data |
|---|---|---|
GET /api/health | { version, pid }: the serving daemon's process id | |
GET /api/jobs | { items: Job[] } | |
GET /api/projects | Projects: { roots: { path, exists, projects: { path, title, gone, jobs: Job[] }[] }[], elsewhere: Job[], missing: Job[] }: each folder config.json names (default ~/rapid) with the folders directly inside it as projects and their jobs, newest first; elsewhere, the jobs under none of them; missing, the jobs whose project folder is gone | |
POST /api/projects/roots | { path } | { path, changed, roots }: adds a folder to find projects in, as rapid projects add does; it must exist |
POST /api/projects/roots/remove | { path } | { path, changed, roots }: stops finding projects in a folder, which is left as it is |
POST /api/projects/videos | { root, brief } | { job, project }: starts a video from the brief in a new project folder inside root, which must be one of the folders, in the default style and format with no stops |
POST /api/jobs | CreateJob | { job: Job } |
POST /api/videos | CreateFilm | { job: Job } |
GET /api/jobs/:job | Graph: { job, nodes, edges, artifacts } | |
GET /api/jobs/:job/format | JobFormat: { name, width, height, fps }, the job's format as its video finds it; 404 when it names none or none is found | |
GET /api/jobs/:job/choices | { items: Choice[] }: what the job's video chose without a person, oldest first | |
POST /api/jobs/:job/nodes | AddNodes | { nodes: Node[] } |
POST /api/jobs/:job/cancel | { job: Job } | |
POST /api/videos/:job/replan | { note } (1 to 4000) | { job: Job }: asks the storyboarder again; the new storyboard waits for approval when the video has stops |
POST /api/videos/:job/revise | FilmRevision: { note, scene?, t?, t_end?, severity? } | { job, revision: { route, scene?, round?, mark_id? } }: routes a note on a video, done or not (mark: a scene and a time; redo: a scene; replan: the video), then runs it to the video again with no stops |
POST /api/nodes/:node/approve | { node: Node } | |
POST /api/nodes/:node/redo | { note? } (up to 4000) | { node: Node } |
GET /api/lanes | { items: LaneStatus[] } | |
GET /api/scenes/:scene/marks | { items: Mark[] } | |
POST /api/marks | NewMark | { mark: Mark } |
GET /api/artifacts/:artifact | the artifact's bytes: posters, sheets, MP4s; Range supported | |
GET /api/events | the event stream, below |
Bodies
CreateFilm, what rapid video sends:
| Field | Type | Default |
|---|---|---|
project | string | required; an absolute folder |
style | string | a name, a pinned package, or a folder; this or look |
look | string | a draft style folder from rapid style extract, which the job finishes, checks, and pins first; this or style |
format | string | "reel" |
review | "agent" or "none" | "agent" |
rounds | integer 1–10 | 4 |
keys | "agent", "person", or "off" | "agent"; with review: "none", agent means off |
director | "agent" or "none" | the director agent; none with review: "none" |
stops | "all", "storyboard", or "none" | "all" |
rebuild | boolean | false |
music | string | none; a track to cut the video to, an absolute path or one in the project |
CreateJob, what rapid new sends: title (1–200, required), brief (up to 20000, default ""), project (required), style and format (string or null, default null).
AddNodes is { nodes: [...] }. Each new node has a key (the caller's name for it, so later nodes in the same request can name it in needs), a type, a lane, a label, and optionally a group, needs (keys or ids of the job's earlier nodes), and a spec. A spec with argv runs a command, in cwd relative to the project (default the project itself); a spec with a prompt runs an agent session, with optional agent, model, and allowed_tools.
{
"nodes": [
{ "key": "a", "type": "custom", "lane": "encode", "label": "transcode", "spec": { "argv": ["ffmpeg", "-i", "in.mov", "out.mp4"] } },
{ "key": "b", "type": "approval", "lane": "cpu", "label": "look at it", "needs": ["a"] }
]
}
NewMark is a Mark without id and created_at, with an optional job_id.
Shapes
Job: id, title, brief, project, style (or null), format (or null), state (running, needs_you, done, failed, cancelled), error (why the job failed when no node says, or null), created_at.
Node:
| Field | Type |
|---|---|
id, job_id | string |
type | storyboard, scene, check, review, blender, render, scan, listen, mix, mux, phone, approval, extract, custom |
lane | think, gpu, web, encode, cpu |
label | string, 1–120 |
group | string or null: the scene it belongs to |
state | waiting, queued, running, done, failed, stale, needs_you, cancelled |
fingerprint | sha256 or null |
needs, outputs | arrays of ids |
poster | artifact id or null |
attempt | integer |
progress | 0–1 or null |
error | string or null |
timings | { queued_at, started_at, ended_at, wait_ms, run_ms } |
spec | object: what the worker runs |
Edge: { from, to }.
Artifact: id, node_id, kind (storyboard, scene_code, frames, still, sheet, boxes, cues, review, clip, wav, mp4, style, json, log), path (absolute or relative to the project), sha256, bytes, created_at.
LaneStatus: { lane, slots, running: string[], queued: string[] }.
The event stream
GET /api/events is a Server-Sent Events stream. It opens with retry: 2000; each event is an id: line with its sequence number and a data: line with the event as JSON, on the default event type; comment lines are heartbeats.
retry: 2000
id: 1
data: {"seq":1,"at":"2026-10-28T20:35:41.591Z","job_id":"j_819b0fb4","type":"job.created","data":{"job":{...}}}
id: 2
data: {"seq":2,"at":"2026-10-28T20:35:41.594Z","job_id":"j_819b0fb4","type":"node.created","data":{"node":{...},"edges":[]}}
| Query | Means |
|---|---|
after=<seq> | resume after this sequence number; the Last-Event-ID header does the same |
job=<id> | only this job's events |
once=1 | send what's there, then end |
Every event has seq (strictly increasing across the daemon), at (an ISO time), job_id, type, and data:
| Type | data |
|---|---|
job.created | { job } |
job.updated | { job } |
node.created | { node, edges } |
node.updated | { node } |
node.output | { node_id, artifact } |
node.log | { node_id, stream, line }; stream is stdout, stderr, or agent |
lane.updated | { lane }; job_id may be null |
mark.created | { mark }; job_id may be null |
review.done | { node_id, review } |
rapid events --json prints the same events, one per line.