Local API
The desktop app listens on http://127.0.0.1:47321 while it is running. It binds to the loopback interface only, rejects any other Host header, and requires a bearer token on every request. The MCP gateway and the Chrome extension are both clients of this API; you can be one too.
Authentication
Authorization: Bearer <contents of api_token>
The token is written to api_token in the data folder on first launch, readable by your user only. It is local and personal: never commit it, share it, or send it anywhere. Requests without a valid token receive 401.
Endpoints
All requests and responses are JSON. Errors return { "error": { "code": "…", "message": "…" } } with an HTTP status.
| Method | Path | Purpose |
|---|---|---|
| GET | /v1/health |
{ ok, version, recording: { active, meeting_id? } } |
| GET | /v1/meetings?q=&limit=&offset=&from=&to= |
List meetings; q runs a full-text search |
| GET | /v1/meetings/:id |
One meeting with notes, action items and markers |
| GET | /v1/meetings/:id/transcript?format=json|md|srt|vtt|txt |
Transcript in the format you choose |
| POST | /v1/meetings/:id/notes |
{ kind, template_id? } regenerates notes (Pro) |
| PATCH | /v1/meetings/:id |
Update title, tags, starred, attendees |
| DELETE | /v1/meetings/:id |
Delete the meeting: database rows and audio |
| GET | /v1/search?q= |
Full-text search across transcript segments and notes, with snippets |
| POST | /v1/recording/prompt |
{ source_app, title?, url?, attendees? } shows the confirm-to-record card; returns { prompt_id } |
| POST | /v1/recording/start |
{ source_app, title? } starts a recording (Pro; requires Allow remote start) |
| POST | /v1/recording/stop |
Stops the current recording |
| POST | /v1/recording/marker |
{ label? } bookmarks the current moment |
| POST | /v1/import |
{ path, title? } transcribes an existing audio file (WAV natively, other formats through ffmpeg; regular files up to 4 GB with a known audio extension). Requires Allow remote start |
| GET | /v1/ledger |
The Privacy Ledger: every outbound request the app has made |
| POST | /v1/pair |
{ code } exchanges a 6-digit pairing code for a token (see Extension pairing) |
| GET | /v1/events |
Server-Sent Events stream |
Nothing here can make the app record without a person having chosen it: the prompt endpoint only asks, and the start endpoint is refused unless Allow remote start is on.
Meeting JSON
{
"id": "01J…", "title": "Weekly sync", "source_app": "meet",
"started_at": 1725270000000, "ended_at": 1725273600000, "duration_ms": 3600000,
"status": "ready", "language": "en", "attendees": [{ "name": "Ana" }], "tags": ["sales"],
"starred": false,
"notes": [{ "id": "…", "kind": "summary", "content_md": "…" }],
"action_items": [{ "id": "…", "text": "Send deck", "owner": "You", "done": false }],
"markers": [{ "id": "…", "at_ms": 120000, "label": "pricing" }]
}
status moves through recording, transcribing, summarising and ready.
Transcript JSON
{ "meeting_id": "…", "segments": [{ "speaker": "You", "channel": "mic", "start_ms": 0, "end_ms": 2100, "text": "Hi all" }] }
channel is mic (you) or sys (everyone else). The remote channel is one speaker today.
Events
GET /v1/events streams Server-Sent Events. The event name is the type; the data is { type, data, at }.
| Type | Data |
|---|---|
recording.started |
{ meeting_id, source_app, title, started_at } |
recording.stopped |
{ meeting_id, status } |
recording.paused |
{ paused } |
segment.new |
a transcript segment |
meeting.updated, meeting.ready, meeting.deleted |
{ meeting_id } |
prompt.shown |
the pending prompt |
prompt.answered |
{ prompt_id, accepted, answer } |
marker.added |
the marker |
notes.progress |
{ meeting_id, stage, provider? } |
models.loading, models.loaded |
{ model } |
pair.completed |
{} |
error |
{ message } |
Changing the port
If something else already uses port 47321, the app logs “address in use” (Settings → Diagnostics → Logs). Set general.api_port in settings.json and restart; the gateway reads the same file. See the settings reference.