Skip to main content

Recordings

The Recordings endpoints let you capture everything that happens in a terminal — output, input, and resizes — then list, inspect, delete, and export those captures over TermFlow's local REST API. Exports come in four formats, including asciinema .cast.

All paths below are relative to the API base http://127.0.0.1:42031/api (the default production port). Every route returns JSON unless you export to text or html.

Auth on localhost

In the default localhost-only mode these endpoints are unauthenticated — safe because the server is bound to 127.0.0.1 and never leaves your machine. A bearer token is enforced only when you turn on Expose on local network in Settings → Connections; then every route (except /health and /api/health, which are always exempt) requires the Authorization: Bearer <token> header, where the token is the authToken shown in Settings → Connections. See API overview & auth for the full model.

Endpoint summary

MethodPathPurpose
POST/api/recordings/startBegin recording a terminal
POST/api/recordings/stop/:idStop an active recording
GET/api/recordingsList saved recordings
GET/api/recordings/:idGet one recording (full, with events)
GET/api/recordings/:id/infoGet metadata + counts, no events
DELETE/api/recordings/:idDelete a recording
POST/api/recordings/:id/exportExport as json / text / html / asciinema
GET/api/recordings/status/:terminalIdIs a given terminal being recorded?
GET/api/recordings/activeList all in-progress recording ids

Note the two different id parameters: :id is a recording id (a UUID), while :terminalId on the status route is a terminal id (a term_… value).

How recording works

A recording is keyed by its own id, not by the terminal. You start one against a live terminal, events stream into an in-memory buffer, and when you stop it the recording is flushed and persisted to disk as a JSON file. Only then does it appear in GET /api/recordings.

A terminal can hold one recording at a time

Starting a second recording on a terminal that is already being recorded returns HTTP 409. Stop the first one, or check GET /api/recordings/status/:terminalId before starting.

The recording object

GET /api/recordings/:id returns the full recording — the same shape written to the on-disk file. Its top-level keys are snake_case:

{
"id": "9b1c7e42-3f8a-4d6b-8a21-1e5c9f0b7d33",
"terminal_id": "term_a1b2c3",
"start_time": "2026-07-04T10:15:00Z",
"end_time": "2026-07-04T10:17:32.480Z",
"events": [
{ "event_type": "output", "data": "$ ", "timestamp": 0 },
{ "event_type": "input", "data": "ls\r", "timestamp": 1840 },
{ "event_type": "output", "data": "README.md\r\n", "timestamp": 1902 }
],
"metadata": {
"title": null,
"shell_type": "bash",
"initial_size": { "cols": 80, "rows": 24 }
},
"size": 20544,
"compressed": true
}
FieldTypeMeaning
idstring (UUID)The recording's unique id. Use it wherever a :id is required.
terminal_idstringThe terminal that was recorded.
start_timeISO 8601When recording began.
end_timeISO 8601 | nullWhen it stopped. null while still active.
eventsarrayOrdered event stream (always present on this route).
metadataobjectDescriptive info (see the metadata table).
sizeintegerTotal recorded payload in bytes (sum of event data lengths).
compressedbooleanReflects whether gzip compression was requested (default true).

Event (events[]) entries:

FieldTypeMeaning
event_typestringOne of output, input, resize.
datastringThe payload (terminal bytes for output/input).
timestampintegerMilliseconds since start_time.

Metadata (metadata) fields:

FieldTypeNotes
titlestring | nullOptional label; null unless set.
shell_typestring | nullThe shell captured from the terminal at start.
initial_size{ cols, rows }Terminal dimensions at capture start.
Field casing

GET /:id returns the raw on-disk object with snake_case keys (terminal_id, start_time, end_time). The list and info routes return camelCase top-level keys (terminalId, startTime, endTime) instead, but the nested metadata object always keeps its snake_case fields (shell_type, initial_size).


Start a recording

POST /api/recordings/start

Body parameters

FieldTypeRequiredNotes
terminal_idstringyesThe terminal to record.
optionsobjectnoCapture options — see below.

options object:

FieldTypeDefaultNotes
include_inputbooleantrueCapture keystrokes / input events.
include_outputbooleantrueCapture terminal output.
include_resizebooleantrueCapture resize events.
compressionstringgzipnone or gzip.
auto_stopbooleanfalseStored on the recording; auto-stop is not yet enforced.

The options object is parsed as a whole — include every field when you provide it, or omit options entirely to accept the defaults above.

Response — HTTP 201

{
"recordingId": "9b1c7e42-3f8a-4d6b-8a21-1e5c9f0b7d33",
"terminalId": "term_a1b2c3",
"status": "recording"
}

Returns HTTP 404 with {"error": "Terminal not found"} if the terminal id does not exist, and HTTP 409 if that terminal is already being recorded.

Example

curl -X POST http://127.0.0.1:42031/api/recordings/start \
-H "Content-Type: application/json" \
-d '{ "terminal_id": "term_a1b2c3" }'

Stop a recording

POST /api/recordings/stop/:id

:id is the recording id returned by start. Flushes buffered events and persists the file.

Response

{
"recordingId": "9b1c7e42-3f8a-4d6b-8a21-1e5c9f0b7d33",
"status": "stopped",
"size": 20544,
"eventCount": 148
}

Returns HTTP 404 with {"error": "Recording not found or not active"} if the id is unknown or already stopped.

curl -X POST http://127.0.0.1:42031/api/recordings/stop/9b1c7e42-3f8a-4d6b-8a21-1e5c9f0b7d33

List recordings

GET /api/recordings

Returns saved recordings, newest first. Events are omitted for performance — each entry carries an eventCount instead.

This route takes no query parameters and returns every saved recording.

Response

{
"recordings": [
{
"id": "9b1c7e42-3f8a-4d6b-8a21-1e5c9f0b7d33",
"terminalId": "term_a1b2c3",
"startTime": "2026-07-04T10:15:00Z",
"endTime": "2026-07-04T10:17:32.480Z",
"size": 20544,
"eventCount": 148
}
]
}

Each entry carries only id, terminalId, startTime, endTime, size, and eventCount. Use GET /api/recordings/:id/info for compressed, duration, and metadata, or GET /api/recordings/:id for the full event stream.

curl http://127.0.0.1:42031/api/recordings

Get a recording

GET /api/recordings/:id

Returns the complete recording, including the full events array, in the recording-object shape above (snake_case keys). Returns HTTP 404 if the id is unknown.

curl http://127.0.0.1:42031/api/recordings/9b1c7e42-3f8a-4d6b-8a21-1e5c9f0b7d33

Recording info

GET /api/recordings/:id/info

A metadata-and-counts view that never includes events. Ideal when you just need the size, timing, duration, or compressed flag.

Response

{
"id": "9b1c7e42-3f8a-4d6b-8a21-1e5c9f0b7d33",
"terminalId": "term_a1b2c3",
"startTime": "2026-07-04T10:15:00Z",
"endTime": "2026-07-04T10:17:32.480Z",
"eventCount": 148,
"size": 20544,
"compressed": true,
"duration": 152480,
"metadata": { "title": null, "shell_type": "bash", "initial_size": { "cols": 80, "rows": 24 } }
}

duration is milliseconds (endTime − startTime), or null when the recording has no endTime. Note the mixed casing: top-level keys are camelCase, but the nested metadata object keeps its snake_case fields. Returns HTTP 404 for an unknown id.


Delete a recording

DELETE /api/recordings/:id

Removes the recording's JSON file from disk.

Response — HTTP 204 No Content on success, or HTTP 404 if the recording does not exist.

curl -X DELETE http://127.0.0.1:42031/api/recordings/9b1c7e42-3f8a-4d6b-8a21-1e5c9f0b7d33

Export a recording

POST /api/recordings/:id/export

Converts a recording into a downloadable file. The response sets Content-Disposition: attachment with a format-specific filename.

Body parameters

FieldTypeRequiredNotes
formatstringyesjson, text, html, or asciinema.

Any other body fields are currently ignored — metadata and timestamps are handled automatically per format (see below).

Format → output

formatContent-TypeFilenameContents
jsonapplication/jsonrecording-<id>.jsonThe full recording object (events + metadata), pretty-printed.
texttext/plainrecording-<id>.textOutput events only, concatenated, with a #-comment header.
htmltext/htmlrecording-<id>.htmlSelf-contained dark-terminal HTML page of the output.
asciinemaapplication/jsonrecording-<id>.castasciinema v2 cast (see below).

An unsupported format returns HTTP 400 with {"error": "Unsupported format"}; an unknown id returns HTTP 404.

Example — export as an asciinema cast

curl -X POST http://127.0.0.1:42031/api/recordings/9b1c7e42-3f8a-4d6b-8a21-1e5c9f0b7d33/export \
-H "Content-Type: application/json" \
-d '{ "format": "asciinema" }' \
-o session.cast

You can then replay it with asciinema play session.cast.

Format details

  • json — the whole recording object (the same shape as GET /:id), pretty-printed, including events and metadata.
  • text — only output events, in order, concatenated. A #-comment header (id, terminal, start, end, event count) is always prepended.
  • html — a standalone page (inline CSS, monospace, black background) rendering the output events, HTML-escaped, with a metadata block at the top.
  • asciinema — asciinema v2 format: a JSON header line (version 2, width/height from metadata.initial_size, timestamp, a title, and an env with SHELL/TERM) followed by one [time, "o", data] line per output event. Input and resize events are not included.

Recording status

GET /api/recordings/status/:terminalId

Cheap check for whether a terminal currently has an active recording. :terminalId is a terminal id, not a recording id.

Response

{ "terminalId": "term_a1b2c3", "isRecording": true, "status": "recording" }

status is "recording" or "not_recording".

curl http://127.0.0.1:42031/api/recordings/status/term_a1b2c3

Active recordings

GET /api/recordings/active

Lists the ids of every recording currently in progress across all terminals.

Response

{ "activeRecordings": ["9b1c7e42-3f8a-4d6b-8a21-1e5c9f0b7d33"], "count": 1 }
curl http://127.0.0.1:42031/api/recordings/active

A worked example

# 1. Start
REC=$(curl -s -X POST http://127.0.0.1:42031/api/recordings/start \
-H "Content-Type: application/json" \
-d '{ "terminal_id": "term_a1b2c3" }' | jq -r .recordingId)

# 2. ...do work in the terminal via the execute/input API...

# 3. Stop
curl -s -X POST http://127.0.0.1:42031/api/recordings/stop/$REC

# 4. Export to an asciinema cast
curl -s -X POST http://127.0.0.1:42031/api/recordings/$REC/export \
-H "Content-Type: application/json" \
-d '{ "format": "asciinema" }' -o session.cast

An honest note: a recording only becomes visible to list, get, info, and export after it is stopped and flushed to disk — while active it appears solely in GET /api/recordings/active. The on-disk file is written as plain JSON; the compressed flag reflects the requested compression option rather than the file actually being gzipped. Recording is driver-level, independent of the interactive playback viewer in the app.

Next steps