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 pathBodydata
GET /api/health{ version, pid }: the serving daemon's process id
GET /api/jobs{ items: Job[] }
GET /api/projectsProjects: { 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/jobsCreateJob{ job: Job }
POST /api/videosCreateFilm{ job: Job }
GET /api/jobs/:jobGraph: { job, nodes, edges, artifacts }
GET /api/jobs/:job/formatJobFormat: { 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/nodesAddNodes{ 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/reviseFilmRevision: { 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/marksNewMark{ mark: Mark }
GET /api/artifacts/:artifactthe artifact's bytes: posters, sheets, MP4s; Range supported
GET /api/eventsthe event stream, below

Bodies

CreateFilm, what rapid video sends:

FieldTypeDefault
projectstringrequired; an absolute folder
stylestringa name, a pinned package, or a folder; this or look
lookstringa draft style folder from rapid style extract, which the job finishes, checks, and pins first; this or style
formatstring"reel"
review"agent" or "none""agent"
roundsinteger 1–104
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"
rebuildbooleanfalse
musicstringnone; 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:

FieldType
id, job_idstring
typestoryboard, scene, check, review, blender, render, scan, listen, mix, mux, phone, approval, extract, custom
lanethink, gpu, web, encode, cpu
labelstring, 1–120
groupstring or null: the scene it belongs to
statewaiting, queued, running, done, failed, stale, needs_you, cancelled
fingerprintsha256 or null
needs, outputsarrays of ids
posterartifact id or null
attemptinteger
progress0–1 or null
errorstring or null
timings{ queued_at, started_at, ended_at, wait_ms, run_ms }
specobject: 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":[]}}
QueryMeans
after=<seq>resume after this sequence number; the Last-Event-ID header does the same
job=<id>only this job's events
once=1send what's there, then end

Every event has seq (strictly increasing across the daemon), at (an ISO time), job_id, type, and data:

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