RheoclesREE-oh-kleez

API reference

Generated from docs/openapi.yaml, version 0.1.0. CI validates the running daemon against the same file.

Conventions

HTTP + SSEhttp://127.0.0.1:7447
WebSocketws://127.0.0.1:7448
AuthAuthorization: Bearer <token> on every HTTP request; ?access_token= also accepted on GET /events; on the WebSocket, a first frame of { "auth": "<token>" }
BodiesJSON in, JSON out; Content-Type: application/json
Pathsthe take folder is relative to outputRoot from GET /; each file is relative to the take folder
Timeshost times are ISO 8601 UTC with milliseconds; t is seconds from the cue; timecode is HH:MM:SS:FF
Absencean unmeasurable value is absent, or null where the field is always present — never 0
the error shape — error for humans, code for programs
{ "error": "destination takes/2026-09-11/210433-episode-12 is not empty", "code": "conflict" }
statuscodewhen
400bad_requestmalformed JSON, missing body, a frame without method/path
401unauthorizedno token, wrong token, WebSocket before the auth frame
403permission_deniedmacOS has not granted the device class this stream needs
404not_foundno such route, stream or take
405method_not_allowedthe path exists, the method does not
409conflictdestination already exists; a take is not in the state the verb needs
409take_activea take is recording; stop it first
500internala handler threw
501unsupportedthis kind cannot be captured yet
503device_unavailablethe device is gone, busy, or refused the configuration
503no_framepreview: the device delivered no frame
507insufficient_storagethe disk pre-flight refused the take

Discovery

GET/

Who this daemon is, where files go, how much room there is.

401
No token, or the wrong one.

Discovery.

over WebSocket
→ {"id":1,"method":"GET","path":"/"}
← {"id":1,"status":200,"body":{"name":"Rheocles","version":"0.1.0","hostname":"lens-macbook-pro.local","machineId":"CD3B7EE5-5E6C-5155-854A-72E4728555F7","outputRoot":"/Users/gopher/Movies/Rheocles","freeBytes":44878079167,"auth":"bearer","ports":{"http":7447,"ws":7448}}}
response fieldtype
name*"Rheocles"
version*string
hostname*string
machineId*stringThe kernel host UUID; stable across renames and reboots.
outputRoot*stringThe one absolute path in the API. Every other path is relative to it.
freeBytesintegerBytes available for important usage on the output root's volume. Absent when unmeasurable — absent means unknown, never zero.
auth*"bearer"
ports*object
ports.http*integer
ports.ws*integer

Streams

Every input on the machine, with a stable id. Arming makes a device live; it never writes.

GET/streams

Every stream with its armed state, plus what macOS lets this process see.

401
No token, or the wrong one.

Streams, in a fixed order — displays, windows, cameras, microphones, system audio.

over WebSocket
→ {"id":2,"method":"GET","path":"/streams"}
← {"id":2,"status":200,"body":{"streams":[],"permissions":{"camera":"authorized","microphone":"authorized","screen":"authorized"}}}
response fieldtype
streams*object[]
streams[].id*stringStable and URL-safe, <kind>:<identifier>. Clients hold this.
streams[].kind*"display" | "window" | "camera" | "microphone" | "systemAudio"
streams[].name*stringWhat the user calls it.
streams[].model*stringWhat the hardware calls itself, or the owning application for a window.
streams[].capabilities*object
streams[].capabilities.videoobject
streams[].capabilities.audioobject
streams[].armed*booleanWhether the capture session is live.
streams[].activeobjectWhat the device is actually delivering; present only while armed.
streams[].active.videoobject
streams[].active.audioobject
streams[].framesSeenintegerFrames or audio buffers delivered since arming; present only while armed.
permissions*objectWhat macOS has let this process do. Screen Recording gates displays and windows both; with screen anything but authorized the list has none.
permissions.camera*"authorized" | "denied" | "restricted" | "notDetermined"
permissions.microphone*"authorized" | "denied" | "restricted" | "notDetermined"
permissions.screen*"authorized" | "denied" | "restricted" | "notDetermined"

POST/streams/{id}/arm

Make a device live (frames flow and are discarded) or release it. Never stamps, never writes.

fieldtype
armed*boolean
400
Malformed or missing JSON body.
401
No token, or the wrong one.
403
macOS has not granted this process the device class.
404
No such stream or take.
503
The device is gone, busy, or refused the configuration.

The stream as it now is. Idempotent in both directions.

over WebSocket
→ {"id":3,"method":"POST","path":"/streams/camera:0x2300000fd9009c/arm","body":{"armed":true}}
← {"id":3,"status":200,"body":{"id":"camera:0x2300000fd9009c","kind":"camera","name":"Elgato 4K X","model":"UVC Camera VendorID_4057 ProductID_156","capabilities":{"video":{"width":3840,"height":2160,"maxFrameRate":30}},"armed":true,"active":{"video":{"width":1280,"height":720,"maxFrameRate":120}},"framesSeen":0}}
response fieldtype
id*stringStable and URL-safe, <kind>:<identifier>. Clients hold this.
kind*"display" | "window" | "camera" | "microphone" | "systemAudio"
name*stringWhat the user calls it.
model*stringWhat the hardware calls itself, or the owning application for a window.
capabilities*object
capabilities.videoobject
capabilities.video.width*integerNative pixels.
capabilities.video.height*integer
capabilities.video.maxFrameRate*numberThe highest advertised rate; the signal's real rate can be lower.
capabilities.audioobject
capabilities.audio.sampleRate*number
capabilities.audio.channels*integer
armed*booleanWhether the capture session is live.
activeobjectWhat the device is actually delivering; present only while armed.
active.videoobject
active.video.width*integer
active.video.height*integer
active.video.maxFrameRate*number
active.audioobject
active.audio.sampleRate*number
active.audio.channels*integer
framesSeenintegerFrames or audio buffers delivered since arming; present only while armed.

Takes

Create reserves the paths and writes the manifest. Start is the cue. Stop finalises.

POST/takes

Create a take — snapshot the armed set, reserve paths, pre-flight the disk, write the manifest. Nothing records.

fieldtype
namestring
destinationstringTake folder relative to the output root. Default takes/<yyyy-MM-dd>/<HHmmss>[-<name>].
filesobjectFile name per stream id, relative to the take folder.
codec"hevc" | "prores"
expectedDurationnumberSeconds, for the disk pre-flight. Default 1800.
overwritebooleanReuse a destination that already exists. Never silently suffixed.
400
Malformed or missing JSON body.
401
No token, or the wrong one.
409
Destination already exists (code conflict), or a take is recording (code take_active).
507
The disk pre-flight refused the take.

The manifest (state `created`) and any pre-flight warnings.

over WebSocket
→ {"id":4,"method":"POST","path":"/takes","body":{"name":"Episode 12","expectedDuration":3600}}
← {"id":4,"status":201,"body":{"take":{"id":"20260912T040433-fd9q","name":"Episode 12","state":"created","created":"2026-09-12T04:04:33.235Z","outputRoot":"/Users/gopher/Movies/Rheocles","destination":"takes/2026-09-11/210433-episode-12","version":"0.1.0","machine":{"hostname":"lens-macbook-pro.local","machineId":"CD3B7EE5-5E6C-5155-854A-72E4728555F7"},"streams":[{"id":"microphone:AppleUSBAudioEngine_Focusrite_Scarlett_2i2_USB_Y8CABR91C1CA8A_1_2","kind":"microphone","name":"Scarlett 2i2 USB","model":"Scarlett 2i2 USB:1235:8210","path":"scarlett-2i2-usb.wav","codec":"pcm_s24le","format":{"audio":{"sampleRate":48000,"channels":2}},"framesWritten":0,"events":[]}],"markers":[],"settings":{"codec":"hevc","expectedDuration":3600}},"warnings":[]}}
response fieldtype
take*objectThe manifest is the take. Rewritten atomically on every state change.
take.id*string
take.namestring
take.state*"created" | "recording" | "complete" | "incomplete"
take.reasonstringPresent only when incomplete.
take.created*string
take.startedstring
take.stoppedstring
take.outputRoot*stringAbsolute, as it was at create.
take.destination*stringTake folder, relative to the output root.
take.version*string
take.machine*object
take.machine.hostname*string
take.machine.machineId*string
take.streams*object[]
take.streams[].id*string
take.streams[].kind*"display" | "window" | "camera" | "microphone" | "systemAudio"
take.streams[].name*string
take.streams[].model*string
take.streams[].path*stringRelative to the take folder.
take.streams[].codec*stringhevc, prores, or pcm_s24le.
take.streams[].format*object
take.streams[].startedstring
take.streams[].stoppedstring
take.streams[].timecodestringStart timecode HH:MM:SS:FF — the LOCAL time of day (the machine's own midnight) of the first written frame, nearest frame. The manifest's created/started/stopped are UTC.
take.streams[].timeReferenceintegerAudio only. BWF TimeReference: samples since LOCAL midnight at the first sample — the sample-exact form of timecode.
take.streams[].framesDroppedintegerVideo only. Frames the encoder was not ready for. Absent when zero.
take.streams[].framesWritten*integer
take.streams[].driftnumberFrames × frame duration versus host elapsed, seconds.
take.streams[].events*object[]
take.streams[].errorstring
take.markers*object[]
take.markers[].t*numberSeconds from the cue.
take.markers[].label*string
take.settings*object
take.settings.codec*"hevc" | "prores"
take.settings.expectedDurationnumber
warnings*string[]

POST/takes/{id}/start

The cue. Every armed stream's writer starts on frames that are already flowing.

401
No token, or the wrong one.
404
No such stream or take.
409
The take is not created, or is not the active take.

The manifest, now `recording`.

over WebSocket
→ {"id":7,"method":"POST","path":"/takes/20260912T040433-fd9q/start"}
← {"id":7,"status":200,"body":{"id":"20260912T040433-fd9q","state":"recording","started":"2026-09-12T04:04:33.347Z"}}
response fieldtype
id*string
namestring
state*"created" | "recording" | "complete" | "incomplete"
reasonstringPresent only when incomplete.
created*string
startedstring
stoppedstring
outputRoot*stringAbsolute, as it was at create.
destination*stringTake folder, relative to the output root.
version*string
machine*object
machine.hostname*string
machine.machineId*string
streams*object[]
streams[].id*string
streams[].kind*"display" | "window" | "camera" | "microphone" | "systemAudio"
streams[].name*string
streams[].model*string
streams[].path*stringRelative to the take folder.
streams[].codec*stringhevc, prores, or pcm_s24le.
streams[].format*object
streams[].format.videoobject
streams[].format.audioobject
streams[].startedstring
streams[].stoppedstring
streams[].timecodestringStart timecode HH:MM:SS:FF — the LOCAL time of day (the machine's own midnight) of the first written frame, nearest frame. The manifest's created/started/stopped are UTC.
streams[].timeReferenceintegerAudio only. BWF TimeReference: samples since LOCAL midnight at the first sample — the sample-exact form of timecode.
streams[].framesDroppedintegerVideo only. Frames the encoder was not ready for. Absent when zero.
streams[].framesWritten*integer
streams[].driftnumberFrames × frame duration versus host elapsed, seconds.
streams[].events*object[]
streams[].events[].t*number
streams[].events[].type*"join" | "leave"
streams[].errorstring
markers*object[]
markers[].t*numberSeconds from the cue.
markers[].label*string
settings*object
settings.codec*"hevc" | "prores"
settings.expectedDurationnumber

POST/takes/{id}/stop

Finalize every writer and the manifest. Streams stay armed.

401
No token, or the wrong one.
404
No such stream or take.
409
The take is not recording.

The final manifest — `complete`, or `incomplete` with a reason.

over WebSocket
→ {"id":8,"method":"POST","path":"/takes/20260912T040433-fd9q/stop"}
← {"id":8,"status":200,"body":{"id":"20260912T040433-fd9q","state":"complete","stopped":"2026-09-12T04:04:35.402Z"}}
response fieldtype
id*string
namestring
state*"created" | "recording" | "complete" | "incomplete"
reasonstringPresent only when incomplete.
created*string
startedstring
stoppedstring
outputRoot*stringAbsolute, as it was at create.
destination*stringTake folder, relative to the output root.
version*string
machine*object
machine.hostname*string
machine.machineId*string
streams*object[]
streams[].id*string
streams[].kind*"display" | "window" | "camera" | "microphone" | "systemAudio"
streams[].name*string
streams[].model*string
streams[].path*stringRelative to the take folder.
streams[].codec*stringhevc, prores, or pcm_s24le.
streams[].format*object
streams[].format.videoobject
streams[].format.audioobject
streams[].startedstring
streams[].stoppedstring
streams[].timecodestringStart timecode HH:MM:SS:FF — the LOCAL time of day (the machine's own midnight) of the first written frame, nearest frame. The manifest's created/started/stopped are UTC.
streams[].timeReferenceintegerAudio only. BWF TimeReference: samples since LOCAL midnight at the first sample — the sample-exact form of timecode.
streams[].framesDroppedintegerVideo only. Frames the encoder was not ready for. Absent when zero.
streams[].framesWritten*integer
streams[].driftnumberFrames × frame duration versus host elapsed, seconds.
streams[].events*object[]
streams[].events[].t*number
streams[].events[].type*"join" | "leave"
streams[].errorstring
markers*object[]
markers[].t*numberSeconds from the cue.
markers[].label*string
settings*object
settings.codec*"hevc" | "prores"
settings.expectedDurationnumber

POST/takes/{id}/join

Start one stream's writer inside a running take, arming it first if cold. Stamped with the time it began.

fieldtype
stream*string
401
No token, or the wrong one.
404
No such stream or take.
409
The take is not recording, or the stream is already recording in it.

The manifest with the joined stream.

over WebSocket
→ {"id":10,"method":"POST","path":"/takes/20260912T045007-5kqn/join","body":{"stream":"microphone:BuiltInMicrophoneDevice"}}
← {"id":10,"status":200,"body":{"id":"20260912T045007-5kqn","state":"recording"}}
response fieldtype
id*string
namestring
state*"created" | "recording" | "complete" | "incomplete"
reasonstringPresent only when incomplete.
created*string
startedstring
stoppedstring
outputRoot*stringAbsolute, as it was at create.
destination*stringTake folder, relative to the output root.
version*string
machine*object
machine.hostname*string
machine.machineId*string
streams*object[]
streams[].id*string
streams[].kind*"display" | "window" | "camera" | "microphone" | "systemAudio"
streams[].name*string
streams[].model*string
streams[].path*stringRelative to the take folder.
streams[].codec*stringhevc, prores, or pcm_s24le.
streams[].format*object
streams[].format.videoobject
streams[].format.audioobject
streams[].startedstring
streams[].stoppedstring
streams[].timecodestringStart timecode HH:MM:SS:FF — the LOCAL time of day (the machine's own midnight) of the first written frame, nearest frame. The manifest's created/started/stopped are UTC.
streams[].timeReferenceintegerAudio only. BWF TimeReference: samples since LOCAL midnight at the first sample — the sample-exact form of timecode.
streams[].framesDroppedintegerVideo only. Frames the encoder was not ready for. Absent when zero.
streams[].framesWritten*integer
streams[].driftnumberFrames × frame duration versus host elapsed, seconds.
streams[].events*object[]
streams[].events[].t*number
streams[].events[].type*"join" | "leave"
streams[].errorstring
markers*object[]
markers[].t*numberSeconds from the cue.
markers[].label*string
settings*object
settings.codec*"hevc" | "prores"
settings.expectedDurationnumber

POST/takes/{id}/leave

Finalize one stream's file; the stream stays armed and the take continues for the others.

fieldtype
stream*string
401
No token, or the wrong one.
404
No such stream or take.
409
The take is not recording, or the stream is not recording in it.

The manifest with that stream finalized.

over WebSocket
→ {"id":11,"method":"POST","path":"/takes/20260912T045007-5kqn/leave","body":{"stream":"microphone:BuiltInMicrophoneDevice"}}
← {"id":11,"status":200,"body":{"id":"20260912T045007-5kqn","state":"recording"}}
response fieldtype
id*string
namestring
state*"created" | "recording" | "complete" | "incomplete"
reasonstringPresent only when incomplete.
created*string
startedstring
stoppedstring
outputRoot*stringAbsolute, as it was at create.
destination*stringTake folder, relative to the output root.
version*string
machine*object
machine.hostname*string
machine.machineId*string
streams*object[]
streams[].id*string
streams[].kind*"display" | "window" | "camera" | "microphone" | "systemAudio"
streams[].name*string
streams[].model*string
streams[].path*stringRelative to the take folder.
streams[].codec*stringhevc, prores, or pcm_s24le.
streams[].format*object
streams[].format.videoobject
streams[].format.audioobject
streams[].startedstring
streams[].stoppedstring
streams[].timecodestringStart timecode HH:MM:SS:FF — the LOCAL time of day (the machine's own midnight) of the first written frame, nearest frame. The manifest's created/started/stopped are UTC.
streams[].timeReferenceintegerAudio only. BWF TimeReference: samples since LOCAL midnight at the first sample — the sample-exact form of timecode.
streams[].framesDroppedintegerVideo only. Frames the encoder was not ready for. Absent when zero.
streams[].framesWritten*integer
streams[].driftnumberFrames × frame duration versus host elapsed, seconds.
streams[].events*object[]
streams[].events[].t*number
streams[].events[].type*"join" | "leave"
streams[].errorstring
markers*object[]
markers[].t*numberSeconds from the cue.
markers[].label*string
settings*object
settings.codec*"hevc" | "prores"
settings.expectedDurationnumber

POST/takes/{id}/markers

Append a marker at the current cue offset. Rheocles stamps when; the client says what.

fieldtype
label*string
400
Malformed or missing JSON body.
401
No token, or the wrong one.
404
No such stream or take.
409
The take is not recording.

The manifest with the marker appended.

over WebSocket
→ {"id":12,"method":"POST","path":"/takes/20260912T045007-5kqn/markers","body":{"label":"chapter 1"}}
← {"id":12,"status":200,"body":{"id":"20260912T045007-5kqn","markers":[{"t":2.042,"label":"chapter 1"}]}}
response fieldtype
id*string
namestring
state*"created" | "recording" | "complete" | "incomplete"
reasonstringPresent only when incomplete.
created*string
startedstring
stoppedstring
outputRoot*stringAbsolute, as it was at create.
destination*stringTake folder, relative to the output root.
version*string
machine*object
machine.hostname*string
machine.machineId*string
streams*object[]
streams[].id*string
streams[].kind*"display" | "window" | "camera" | "microphone" | "systemAudio"
streams[].name*string
streams[].model*string
streams[].path*stringRelative to the take folder.
streams[].codec*stringhevc, prores, or pcm_s24le.
streams[].format*object
streams[].format.videoobject
streams[].format.audioobject
streams[].startedstring
streams[].stoppedstring
streams[].timecodestringStart timecode HH:MM:SS:FF — the LOCAL time of day (the machine's own midnight) of the first written frame, nearest frame. The manifest's created/started/stopped are UTC.
streams[].timeReferenceintegerAudio only. BWF TimeReference: samples since LOCAL midnight at the first sample — the sample-exact form of timecode.
streams[].framesDroppedintegerVideo only. Frames the encoder was not ready for. Absent when zero.
streams[].framesWritten*integer
streams[].driftnumberFrames × frame duration versus host elapsed, seconds.
streams[].events*object[]
streams[].events[].t*number
streams[].events[].type*"join" | "leave"
streams[].errorstring
markers*object[]
markers[].t*numberSeconds from the cue.
markers[].label*string
settings*object
settings.codec*"hevc" | "prores"
settings.expectedDurationnumber

GET/takes/{id}

The manifest — live while recording, from disk afterwards.

401
No token, or the wrong one.
404
No such stream or take.

The manifest.

over WebSocket
→ {"id":6,"method":"GET","path":"/takes/20260912T040433-fd9q"}
← {"id":6,"status":200,"body":{"id":"20260912T040433-fd9q","state":"recording"}}
response fieldtype
id*string
namestring
state*"created" | "recording" | "complete" | "incomplete"
reasonstringPresent only when incomplete.
created*string
startedstring
stoppedstring
outputRoot*stringAbsolute, as it was at create.
destination*stringTake folder, relative to the output root.
version*string
machine*object
machine.hostname*string
machine.machineId*string
streams*object[]
streams[].id*string
streams[].kind*"display" | "window" | "camera" | "microphone" | "systemAudio"
streams[].name*string
streams[].model*string
streams[].path*stringRelative to the take folder.
streams[].codec*stringhevc, prores, or pcm_s24le.
streams[].format*object
streams[].format.videoobject
streams[].format.audioobject
streams[].startedstring
streams[].stoppedstring
streams[].timecodestringStart timecode HH:MM:SS:FF — the LOCAL time of day (the machine's own midnight) of the first written frame, nearest frame. The manifest's created/started/stopped are UTC.
streams[].timeReferenceintegerAudio only. BWF TimeReference: samples since LOCAL midnight at the first sample — the sample-exact form of timecode.
streams[].framesDroppedintegerVideo only. Frames the encoder was not ready for. Absent when zero.
streams[].framesWritten*integer
streams[].driftnumberFrames × frame duration versus host elapsed, seconds.
streams[].events*object[]
streams[].events[].t*number
streams[].events[].type*"join" | "leave"
streams[].errorstring
markers*object[]
markers[].t*numberSeconds from the cue.
markers[].label*string
settings*object
settings.codec*"hevc" | "prores"
settings.expectedDurationnumber

GET/takes

Recent takes, newest first.

401
No token, or the wrong one.

Up to 50 summaries.

over WebSocket
→ {"id":5,"method":"GET","path":"/takes"}
← {"id":5,"status":200,"body":[{"id":"20260912T040433-fd9q","name":"Episode 12","state":"complete","created":"2026-09-12T04:04:33.235Z","destination":"takes/2026-09-11/210433-episode-12","streams":5}]}
response fieldtype

Record

POST/record

Create and start in one call — the popover's Record button.

fieldtype
namestring
destinationstringTake folder relative to the output root. Default takes/<yyyy-MM-dd>/<HHmmss>[-<name>].
filesobjectFile name per stream id, relative to the take folder.
codec"hevc" | "prores"
expectedDurationnumberSeconds, for the disk pre-flight. Default 1800.
overwritebooleanReuse a destination that already exists. Never silently suffixed.
400
Malformed or missing JSON body.
401
No token, or the wrong one.
409
Destination already exists (code conflict), or a take is recording (code take_active).
507
The disk pre-flight refused the take.

The manifest (state `recording`) and any pre-flight warnings.

over WebSocket
→ {"id":9,"method":"POST","path":"/record","body":{"name":"quick"}}
← {"id":9,"status":201,"body":{"take":{"id":"20260912T040500-k2pm","state":"recording"},"warnings":[]}}
response fieldtype
take*objectThe manifest is the take. Rewritten atomically on every state change.
take.id*string
take.namestring
take.state*"created" | "recording" | "complete" | "incomplete"
take.reasonstringPresent only when incomplete.
take.created*string
take.startedstring
take.stoppedstring
take.outputRoot*stringAbsolute, as it was at create.
take.destination*stringTake folder, relative to the output root.
take.version*string
take.machine*object
take.machine.hostname*string
take.machine.machineId*string
take.streams*object[]
take.streams[].id*string
take.streams[].kind*"display" | "window" | "camera" | "microphone" | "systemAudio"
take.streams[].name*string
take.streams[].model*string
take.streams[].path*stringRelative to the take folder.
take.streams[].codec*stringhevc, prores, or pcm_s24le.
take.streams[].format*object
take.streams[].startedstring
take.streams[].stoppedstring
take.streams[].timecodestringStart timecode HH:MM:SS:FF — the LOCAL time of day (the machine's own midnight) of the first written frame, nearest frame. The manifest's created/started/stopped are UTC.
take.streams[].timeReferenceintegerAudio only. BWF TimeReference: samples since LOCAL midnight at the first sample — the sample-exact form of timecode.
take.streams[].framesDroppedintegerVideo only. Frames the encoder was not ready for. Absent when zero.
take.streams[].framesWritten*integer
take.streams[].driftnumberFrames × frame duration versus host elapsed, seconds.
take.streams[].events*object[]
take.streams[].errorstring
take.markers*object[]
take.markers[].t*numberSeconds from the cue.
take.markers[].label*string
take.settings*object
take.settings.codec*"hevc" | "prores"
take.settings.expectedDurationnumber
warnings*string[]

Settings

Daemon-owned defaults, persisted to ~/Library/Application Support/Rheocles/settings.json.

GET/settings

The daemon's output root and default codec.

401
No token, or the wrong one.

Settings.

over WebSocket
→ {"id":13,"method":"GET","path":"/settings"}
← {"id":13,"status":200,"body":{"outputRoot":"/Users/gopher/Movies/Rheocles","codec":"hevc"}}
response fieldtype
outputRoot*stringAbsolute path new takes land under.
codec*"hevc" | "prores"

PATCH/settings

Change the output root and/or default codec. The root cannot move while a take is active.

fieldtype
outputRootstringAbsolute path.
codec"hevc" | "prores"
400
Malformed or missing JSON body.
401
No token, or the wrong one.
409
The output root cannot change while a take is active.

The full settings after the change.

over WebSocket
→ {"id":14,"method":"PATCH","path":"/settings","body":{"codec":"prores"}}
← {"id":14,"status":200,"body":{"outputRoot":"/Users/gopher/Movies/Rheocles","codec":"prores"}}
response fieldtype
outputRoot*stringAbsolute path new takes land under.
codec*"hevc" | "prores"

Token

POST/token/rotate

Issue a new bearer token; the old one stops working after this response.

401
No token, or the wrong one.

The new token.

over WebSocket
→ {"id":15,"method":"POST","path":"/token/rotate"}
← {"id":15,"status":200,"body":{"token":"9f3c00112233445566778899aabbccddeeff00112233445566778899aabbccdd"}}
response fieldtype
token*string

Events

State, levels, drift, joins, errors — pushed as they happen.

GET/events

The Server-Sent Events stream — every event, one-way.

text/event-stream; each event is one data: <json> line (see x-events). EventSource cannot set headers, so the bearer token is accepted as ?access_token= here as well as in the Authorization header. The same events reach every authenticated WebSocket client.

fieldtype
?access_tokenstringThe bearer token, for EventSource which cannot set a header.
401
No token, or the wrong one.

The event stream.

Preview

GET/preview/{stream}

One preview frame, on demand. Nothing is captured when nobody is looking; one preview at a time.

A polite second opener grabs a single frame and closes the device, so preview never disturbs an armed or recording take. Video streams answer a small JPEG (longest side 640); audio streams answer the current peak level as JSON, for the popover's meter.

401
No token, or the wrong one.
404
No such stream or take.
503
The device delivered no frame to preview (code no_frame), or is unavailable.

A JPEG for video streams, or a JSON level for audio streams.

Answers image/jpeg or application/json.

over WebSocket
→ {"id":16,"method":"GET","path":"/preview/display:56A96CFC-7F21-168E-0857-D6964E3302DB"}
← {"id":16,"status":200,"contentType":"image/jpeg","base64":"/9j/4AAQSk…"}
response fieldtype
levelDb*numberPeak dBFS of a short sample.

WebSocket

Everything above, full duplex, on port 7448.

WSws://127.0.0.1:7448/

One socket carries every command and every event.

The first frame must authenticate; until it does, every command is answered 401 and no event is delivered. A browser WebSocket cannot set an Authorization header, so the frame is the only way. One JSON object per text frame. id is echoed untouched; method, path, query and body are the HTTP request's. status is the HTTP status the same command would give. A JSON body is inline in body; a binary body (a preview JPEG) comes as base64 with its contentType instead. Unsolicited frames with an event key and no id; the same objects GET /events delivers (see x-events).

frames
→ {"auth":"<token>"}
← {"id":null,"status":200,"body":{"authenticated":true}}
→ {"id":4,"method":"POST","path":"/takes","query":{},"body":{"name":"Episode 12"}}
← {"id":4,"status":201,"body":{"take":{"id":"20260912T040433-fd9q","state":"created"}}}
← {"id":16,"status":200,"contentType":"image/jpeg","base64":"/9j/4AAQSk…"}

Event types

Delivered on GET /events as data: lines, and on the WebSocket as objects with an event key and no id — the same bytes on both.

eventwhendata
levelsPer-stream peak level (audio, dBFS), frames written and drift, ~4x/s while recording.{"event":"levels","take":"20260912T045007-5kqn","streams":[{"id":"microphone:BuiltInMicrophoneDevice","levelDb":-18.3,"framesWritten":96000,"drift":0}]}
markerA marker was added to the active take.{"event":"marker","take":"20260912T045007-5kqn","marker":{"t":2.042,"label":"chapter 1"}}
stalledAn armed or recording stream stopped delivering frames.{"event":"stalled","stream":{"id":"camera:0x2300000fd9009c","kind":"camera","name":"Elgato 4K X","model":"UVC Camera VendorID_4057 ProductID_156","capabilities":{"video":{"width":3840,"height":2160,"maxFrameRate":30}},"armed":true,"active":{"video":{"width":1280,"height":720,"maxFrameRate":30}},"framesSeen":0}}
settingsThe daemon's settings changed.{"event":"settings","settings":{"outputRoot":"/Users/gopher/Movies/Rheocles","codec":"prores"}}
takeA take changed state — created, recording, complete or incomplete. Carries the manifest.{"event":"take","take":{"id":"20260912T040433-fd9q","name":"Episode 12","state":"recording","created":"2026-09-12T04:04:33.235Z","started":"2026-09-12T04:04:33.347Z","outputRoot":"/Users/gopher/Movies/Rheocles","destination":"takes/2026-09-11/210433-episode-12","version":"0.1.0","machine":{"hostname":"lens-macbook-pro.local","machineId":"CD3B7EE5-5E6C-5155-854A-72E4728555F7"},"streams":[],"markers":[],"settings":{"codec":"hevc"}}}
streamA stream's armed state changed.{"event":"stream","stream":{"id":"microphone:BuiltInMicrophoneDevice","kind":"microphone","name":"MacBook Pro Microphone","model":"Digital Mic","capabilities":{"audio":{"sampleRate":48000,"channels":1}},"armed":true,"active":{"audio":{"sampleRate":48000,"channels":1}},"framesSeen":0}}