HTTP API
Project association review:
GET /api/projects/associations?source=…&project=…&limit=200returns the explicit workspace, known projects, canonical sources and versioned evidence for suggested/accepted/rejected links. Optional source and primary-project filters narrow the review;limitis 1–1,000.POST /api/projects/associationsaccepts{workspace, source, project, target, status}with an explicit review workspace and statusacceptedorrejected.projectis the canonical primary owner, including an empty string for legacy unassigned sources. Decisions share the ingestion lock and update related retrieval membership without changing IDs or duplicating points.POST /api/commands/associate-projectsqueues a scan of existing snapshots. Optional{model: true}uses the installed local helper for section analysis; it never sends content to a remote provider or downloads a model. Follow the ordinary queue status/log endpoints for completion or errors.
GET /api/sources now includes optional canonical project and linked fields
on entries. Linked sources can be read through the related project; readers
return the canonical owner. Destructive operations still target primary owners.
See project organization.
Experimental voice routes are documented in Echo voice companion:
GET /api/voice/status, POST /api/voice/sessions, WS /api/voice/stream,
GET /api/voice/conversation and POST /api/voice/conversation/end.
Voice requires exact same-origin loopback access and a one-use session capability;
the native speech listener is private to the API. The capability pins the
workspace, the reasoning engine (chat or lfm) and the voice of the
voice.avatar look current when the session was created. The stream’s ready
event reports that voice and its effect (robot for Echo, otherwise
null), which the dashboard applies to reply audio. A different look or engine
takes a new session. A turn may carry "wake": true (a conversation “Hey Echo”
started); a whole utterance like “stop” then ends it with an end event instead
of an answer.
The conversation belongs to the API, not to the socket
(ADR 0145): a closed socket detaches it and its reply
goes on, text kept and audio skipped. The next session attaches to it; ready
then carries resumed, busy, last_id (the next turn id must be greater)
and turns (recent turns: id, user, answer, state of queued,
running, done, cancelled or failed, detail, error, and away when it
finished with no socket). A session for another engine or look carries the
conversation on with it; one for another workspace starts a new conversation,
after the reply in progress if one runs (a second ready).
A turn sent during a reply waits: queued (position), later start. A spoken
turn may carry "overlap": true (speech began while Echo’s audio played); after
transcription it can come back ignored (reason: unheard for noise, echo for
Echo’s own words) or, said alone as “stop” or “cancel”, stop the reply and what
waits (cancelled for each, then stopped). {"type": "cancel"} stops the reply
and every waiting turn; with an id, only that turn. {"type": "replay", "id", "of"}
reads finished turn of aloud again. {"type": "end"} ends and forgets the
conversation and closes the socket. A completed reply is kept without the client’s
played acknowledgement, which is accepted and ignored. GET /api/voice/conversation
returns {"conversation": null} or the conversation’s attached, busy,
engine, workspace, voice, last_id, turns, chat and saving, without
starting the worker; POST /api/voice/conversation/end (same-origin, empty body)
ends one no socket holds and answers 409 while one does. The API forgets a
conversation 30 minutes after its socket left with nothing running.
While voice.save_conversations is on, each answered exchange is also written to
Chat history as a chat of kind voice, numbered as Chat stores messages, with
its running summary (ADR 0146).
ready and the conversation report saving (whether this conversation is
saved). POST /api/voice/sessions takes an optional chat (a Chat history
session id, at most 64 characters) to carry that chat on: its messages and
summary become the conversation’s, ready.chat is {"id", "title"} and
ready.turns holds its last exchanges with ids below zero, so last_id stays
the client’s own. An unknown chat answers 404; another chat takes over an
idle conversation at once and a busy one after its reply.
The desktop app’s “Hey Echo” listener (ADR 0137)
uses GET /api/voice/wake/status (whether the pinned tiny.en model is
installed, its download progress, whether a listener is connected, and
idle_seconds), POST /api/voice/wake/setup and POST /api/voice/wake/setup/cancel
(the model download, only on the user’s Turn on; same-origin, empty body),
POST /api/voice/wake/sessions (a one-use 30 s capability, body {"battery": bool},
409 until the model is installed) and WS /api/voice/wake/stream. After the
capability, the socket answers {"type": "ready"} once its worker has loaded;
{"type": "probe", "id", "audio", "quiet"} (a base64 16 kHz mono PCM16 WAV of at
most 2.5 s, ids increasing) gets {"type": "probe", "id", "wake": bool}, with
"skipped": true when the per-minute CPU budget is spent;
{"type": "utterance", "id", "audio"} (at most 30 s) is accepted only for a probe
that woke, within 35 s, and gets {"type": "question", "id", "text"};
{"type": "power", "battery": bool} halves or restores the budget. One listener
connects at a time; audio is never written and no transcript of a probe that
did not wake is returned.
The Imprint container runs a FastAPI server on port 8420. Everything the
built-in dashboard does — search, ingest, retag, graph walks, sync, chat — is
available over local HTTP for clients on the same machine: a backend service pushing
data, a CI job retagging after a merge, your own UI, or a quick curl to see
what’s in memory.
Get started
Section titled “Get started”{{base_url}} variable preset./openapi.json →/docs for interactive Swagger UI.Quick tour
Section titled “Quick tour”# 1. Confirm the server is upcurl -s http://localhost:8420/api/version
# 2. Search the corpuscurl -s "http://localhost:8420/api/search?q=chunking+system&limit=5"
# 3. Enqueue an ingest job — returns {job_id, position, merged}curl -s -X POST http://localhost:8420/api/commands/ingest \ -H "Content-Type: application/json" \ -d '{"args": ["/workspace"], "project": "my-project"}'
# 4. Tail job output live (Server-Sent Events)curl -N http://localhost:8420/api/jobs/<job_id>/streamIn Docker, publish 127.0.0.1:8420:8420 and use the same local URL. Arbitrary remote Host/Origin values are rejected.
Endpoint reference
Section titled “Endpoint reference”Endpoints are grouped by responsibility. The Postman collection mirrors this structure folder-for-folder so you can scan the reference and the collection side by side.
Overview & stats
Section titled “Overview & stats”Dashboard statistics, summary, overview and graph requests against a collection with a mismatched embedding profile, or nonempty unversioned embeddings, return HTTP 409 with {"error": "embedding_profile_conflict", "collection": "memories", "detail": "…"}. The detail explains how to rebuild explicitly; the stored collection is preserved. /api/ping can still return 200 because the API itself is healthy. Workspace creation and switching remain available so the dashboard can recover in a fresh workspace. See rebuilding an index, ADR 0010 and ADR 0086.
| Method | Path | Purpose |
|---|---|---|
| GET | /api/version |
Version of this installation’s imprint CLI (bin/imprint beside the package, else PATH); unknown when it can’t be run. |
| GET | /api/update |
Latest release, update_available, how this installation updates (method, or unsupported with the reason), plus running, failed (the updater exited with an error; a successful exit hands over to the installer or the server restart) and the updater log tail. The release check is cached for six hours; ?refresh=true repeats it. |
| POST | /api/update |
Start imprint update -y detached; it restarts this server once the release is installed. 400 when this installation cannot update or is current, 409 while an update or job is running. |
| POST | /api/restart |
Finish in-flight requests and exit with status 75; the desktop app or imprint ui that started the server starts it again with the installed code (imprint ui restart uses this, ADR 0050). 409 when the app is not served by Imprint’s launcher. |
| GET | /api/stats |
Aggregate counts per project, type, and language. |
| GET | /api/dashboard |
The Home summary of the active workspace from its snapshot: totals, real counts of projects, sources, topics and languages, their largest groups, memories per month, memories in the last 7 days, the tagging backlog and recently added sources, with workspace, built_at, age, stale and refreshing. A stale snapshot is answered at once while a newer one is built; ?fresh=1 waits for an up-to-date one (ADR 0152). |
| GET | /api/projects |
Every project of the active workspace, largest first, as {name, count, color} (a source linked to several projects counts in each), and the workspace’s total. What the dashboard’s project rails, filters and pickers read: one facet, plus one exact count per project with linked sources, so it stays cheap while the workspace is written. Cached until the next write. |
| GET | /api/overview |
Bubble-chart overview: each project’s types, top domains and languages, plus global facets. Repeat ?type= for multi-filter. Three facets per project, so pages that only need names and counts read /api/projects. |
| GET | /api/timeline |
Recent activity feed. ?project=, ?limit=. |
| GET | /api/cross-project |
Project-to-project similarity matrix. |
Search & retrieval
Section titled “Search & retrieval”| Method | Path | Purpose |
|---|---|---|
| GET | /api/search |
Hybrid search. Filters: q (1–4,000 chars), project, type, limit (1–50), mode (hybrid/semantic). |
| GET | /api/neighbors |
KNN over embeddings. ?id=<memory_id>&k=10. |
| GET | /api/memory/{id} |
Full payload for a single memory. |
| GET | /api/nodes |
Graph node list (positions + clusters) for visualization. |
# Search only decisions in one projectcurl -s "http://localhost:8420/api/search?q=chunking&project=imprint&type=decision&limit=10"Sources
Section titled “Sources”| Method | Path | Purpose |
|---|---|---|
| GET | /api/sources |
Indexed sources with chunk counts, most chunks first, up to ?limit= (default 100; the dashboard asks for every one). ?project= narrows to one project and its linked sources. Each entry has a conversation’s title (its first question), the canonical project (null without a snapshot or with several owners), linked, the snapshot’s kind (conversation, text, extracted, url, inline; "" without one) and updated, its snapshot time (0 without one). See ADR 0139. |
| GET | /api/sources/read |
A source’s full redacted snapshot in readable blocks: a conversation’s events, or a file cut where its chunks end. ?source=&project=&topic=; each block lists the chunks covering it and is marked match where one carries topic. 404 when the source has no snapshot; 409 when project is needed. See ADR 0094. |
| GET | /api/sources/summary/{source_key} |
Summary metadata for one source. |
| GET | /api/source/{source_key} |
Chunks + tags + neighbors for one source. |
Projects & topics
Section titled “Projects & topics”| Method | Path | Purpose |
|---|---|---|
| GET | /api/project/{name} |
Counts by type, domain, language and topic for one project. |
| POST | /api/projects/move |
Preview, or apply, moving a project’s memories (sources: [keys], or all of them) to target. Apply needs apply: true and the workspace the preview returned. See ADR 0036. |
| GET | /api/topics |
One page of topics with their counts and top projects. ?q= keeps names containing it (any case), sort=count|name, offset, limit (1–500, default 100); the response adds matched, topic_count, offset, limit, sort and q (ADR 0150). |
| GET | /api/topic/{name} |
Every memory tagged with one topic, grouped by the source it came from (titled, largest group first), its projects and source_count. ?project= limits it to one project; limit (1–200) and offset return one page of sources, and q keeps sources whose key or title contains it (ADR 0150). |
| POST | /api/migrate/preview |
What copying or moving values (1–200 names) of one mode (project, topic or source) from workspace from to to takes: per value and in all (total, each memory once), its sources and projects, and how many the target already holds (existing). Changes nothing; the migrate command applies (ADR 0150). |
| GET | /api/graph |
Graph walk. ?scope=root|project:X|topic:X|source:X|chunk:X&depth=1..3. Returns nodes, edges, center and more (kinds listed only up to their cap); cached until the workspace’s data changes (ADR 0148). The dashboard’s search:X scope is built from /api/search in the browser. |
Core memories, Organize and recordings
Section titled “Core memories, Organize and recordings”| Method | Path | Purpose |
|---|---|---|
| GET | /api/core |
Every core memory, guidelines and records, each with its tier (guideline or record), amends (the guideline a proposed rewrite replaces, or empty) and covers (the records a guideline generalises); budget and used are the session’s estimated tokens and those the guidelines for every project take (ADR 0129, ADR 0143). |
| POST | /api/core |
A guideline the user wrote: {text, kind, scope, covers?}, active at once; covers lists the records it was made from. |
| PATCH | /api/core/{id} |
Edit text, kind or scope, or set state to active (approve, restore) or retired. state: "proposed" returns a guideline an agent or Organize proposed to the proposals, unapproved (an undo); one the user wrote is refused with 400 (ADR 0136), and so is any record. Approving a rewrite retires the guideline it amends, and undoing that approval restores it. |
| POST | /api/core/{id}/resolve |
Settle a newer record’s conflicts with older records: {keep: "newer"} retires the older ones, "older" retires this one, "both" dismisses them, and "open" undoes a settlement, restoring what it retired. Returns {changed: [...]} (ADR 0144). A rewrite that contradicts an approved guideline is settled by approving or rejecting it. |
| GET | /api/organize |
Project profiles, placement proposals (moved automatically ones as auto; a conversation’s carries its first question as title), the last organize run and background (organize.background, organize.auto_move, the last background pass) (ADR 0130). The job itself is POST /api/commands/organize ({no_model: true} to skip the local model). |
| POST | /api/organize/decide |
{workspace, items: [{origin, source}], accept}: move (a placement rule) or keep proposals. |
| POST | /api/organize/undo |
{workspace, items: [{origin, source}]}: move sources Organize moved by itself back, and keep them there (ADR 0133). |
| POST | /api/organize/reopen |
{workspace, items: [{origin, source}]}: return moved (accepted) or kept (rejected) proposals to review. A move is taken back first, holding the ingestion lock without waiting, so it fails while another job holds it. Returns {reopened, failed: [{origin, source, error}]}; pending proposals and moves Organize made by itself (use undo) are refused in failed (ADR 0136). |
| GET | /api/recordings |
Recordings, newest first (ADR 0131). |
| POST | /api/recordings |
Start one: {title, project, others, consent: true}; refused without consent. |
| POST | /api/recordings/{id}/audio |
The next piece of its audio (raw WebM/Opus bytes). |
| POST | /api/recordings/{id}/stop |
Stop it ({duration} seconds), give its audio its length and seek index (ADR 0184) and queue its transcription. |
| POST | /api/recordings/{id}/transcribe |
Queue its transcription again. |
| GET | /api/recordings/{id}/transcript, /api/recordings/{id}/audio |
The transcript text; the raw audio (?download=1 sends it as a file named after the recording). |
| DELETE | /api/recordings/{id}/audio, /api/recordings/{id} |
Delete the audio only, or the recording with its transcript and memories. |
Reminders, timers and scheduled jobs
Section titled “Reminders, timers and scheduled jobs”Device-local entries the dashboard API’s scheduler fires (ADR 0158). Refused requests answer 400 (409 for done or snooze with nothing waiting) with {error} saying why.
| Method | Path | Purpose |
|---|---|---|
| GET | /api/schedule |
One page of entries: view=upcoming (scheduled, soonest first) or past (fired, done, cancelled, missed; latest first), limit (at most 100), before (the previous page’s next). Returns {entries, next, total}. Each entry has id, kind (reminder, timer, job, prompt), title, message, due_at (epoch seconds), timezone, recurrence, repeat and when (readable), workspace, payload, status, created_by (chat, voice, dashboard), last_fired_at, snoozed_until, ended_at and alert (its waiting firing, or null). |
| POST | /api/schedule |
Create: {kind, title?, message?, at? | in?, recurrence?, payload?, timezone?}. at is a local ISO-8601 date-time or a time of day, in a duration (10m, 1h30m); a time already past is refused. recurrence: none, daily, weekdays, weekly:mon,thu or every 30m (at least 5 minutes). A job’s payload is {command, ...} with command one of ingest, refresh (both with an absolute dir inside the workspace’s approved roots, optional force), learn (desktop), organize (no_model), refresh-urls, retag (pending, project); a question’s is {question}. Returns the entry with note when its time was adjusted (a time the clocks skip). |
| GET | /api/schedule/{id} |
One entry. |
| PATCH | /api/schedule/{id} |
Change an upcoming entry’s title, message, at/in, recurrence, payload or timezone. |
| POST | /api/schedule/{id}/snooze |
{minutes} (1–1440): its waiting reminder or timer alert rings again then. |
| POST | /api/schedule/{id}/done |
Put its waiting alert away; a one-time entry becomes done. |
| POST | /api/schedule/{id}/cancel |
It no longer fires, and its waiting alert is put away. |
| GET | /api/schedule/alerts |
Firings waiting for the user, latest first: {alerts: [{id, entry_id, kind, title, message, occurrence_at, fired_at, alerted_at, late, missed, ring, repeats, when, outcome}]}. ring grows each time a snoozed alert rings again; outcome holds a job’s job_id, a question’s chat, or error. |
| GET | /api/schedule/options |
The kinds, the job allow-list with each job’s fields, this device’s time zone and the snooze choices. |
Knowledge graph
Section titled “Knowledge graph”| Method | Path | Purpose |
|---|---|---|
| GET | /api/kg |
Facts list. ?subject= partial match. |
| GET | /api/kg/entity/{entity} |
Facts about one entity. |
Workspaces
Section titled “Workspaces”| Method | Path | Purpose |
|---|---|---|
| GET | /api/device |
Current device ID/name/OS, process working directory and workspace. |
| GET | /api/workspaces |
List all workspaces + active one. |
| GET | /api/workspace/roots?workspace=NAME |
Approved roots and purge exclusions for one workspace. |
| POST | /api/workspace/roots |
Add/remove a root or restore an exclusion. JSON: workspace, path, action; add accepts explicit boolean allow_overlap. |
| POST | /api/sources/purge |
Preview by default. JSON: target, workspace, kind (path/source), recursive, apply. Apply requires an explicit workspace and boolean confirmation. |
| POST | /api/workspace/switch |
Switch the active workspace, registering it if new. Body: {"workspace": "..."}. Invalid names return 400. |
| POST | /api/workspace/create |
Register a workspace without switching to it. Body: {"name": "..."}. Invalid names return 400. |
CLI commands (async jobs)
Section titled “CLI commands (async jobs)”Every queue-safe imprint CLI command is reachable here. The endpoint enqueues
a background job, returns {job_id, position, merged} immediately, and the job runs
through the same single-slot queue the dashboard uses. merged is true when
an identical job (same command, workspace and normalized arguments) was
already waiting: job_id is that job and nothing new is queued
(ADR 0068).
| Method | Path | Purpose |
|---|---|---|
| GET | /api/wsl |
WSL distributions and, per WSL target, the client’s default-user profile and whether it is enrolled (imprint wsl list --json). Probing can start stopped distributions. On a runtime that is not Windows: {"supported": false, "platform"} without probing. 502 when the CLI fails. |
| GET | /api/commands |
The Commands page catalog (imprint/commands.py): each command’s label, group, fields and confirmation, plus clients from imprint clients --json. 502 when the CLI cannot list them. |
| POST | /api/commands/{command} |
Enqueue a command. See below for the allowlist. |
| GET | /api/queue |
Active + queued + recent jobs, and background: background LLM tagging’s state, the memories waiting for it and its running or latest run (queue). Each job carries label, params and workspace: its name and arguments in the command catalog’s words (commands.describe). |
| POST | /api/queue/background/resume |
Start background work (tagging, then background Organize) again after a cancel or a failure. |
| GET | /api/queue/history |
Finished jobs a page at a time, newest first (limit, before cursor, status), with counts per period for bounds and each job’s waiting or running twin (queue). |
| GET | /api/agents |
The MCP sessions of every client (ADR 0117), one page at a time (ADR 0151): running (connected sessions, on the first page only), sessions (the others, most recently active first), next (a cursor, or null on the last page), total (finished sessions that match) and clients (every session totalled by client). ?limit= (default 50, at most 500), ?before=<next> for the following page (400 when malformed), ?client= (a name as clients gives it, Unknown client for none) and ?q= (text in the folder, workspace or client). |
| GET | /api/agents/{id}/calls |
One session and its tool calls, newest first: tool, start, duration, outcome, shortened and redacted arguments. ?limit= (default 200) and ?before=<call id> page back. |
| GET | /api/jobs/{id} |
Job status + exit code, named like the queue’s jobs. |
| GET | /api/jobs/{id}/stream |
SSE stream of live stdout/stderr. |
| POST | /api/jobs/{id}/cancel |
SIGTERM the job (SIGKILL after 3 s). |
| POST | /api/jobs/{id}/retry |
Queue a finished job again with the same command, arguments and workspace; an identical waiting job absorbs it (ADR 0068). |
Allowlisted commands: the catalog’s (learn, ingest, refresh,
ingest-url, refresh-urls, retag, scrub, sync, connect, disconnect,
status, doctor, config) plus the dashboard dialogs’ wipe, migrate,
workspace and wsl (Settings → WSL profiles: action setup or remove,
distribution, target). connect and disconnect take target (a client name or
all); doctor takes probe_mcp; learn takes desktop; scrub takes apply; retag takes
project, all, dry_run and pending (only the memories waiting for background tagging). Commands that
install into or stop the running dashboard (setup, enable, profile,
disable, ui, server, uninstall) are refused with 404; see
queue.
migrate takes from, to, one of project / topic / source, and the
booleans copy (keep the source’s memories; --copy) and dry_run. The
dashboard counts a selection with POST /api/migrate/preview first and then
queues one migrate job per selected project, topic or source.
ingest and refresh take the path as dir. A dir the workspace’s approved
roots would refuse is rejected before anything is queued, with 409
{"error", "code": "root_not_approved", "workspace", "root", "overlaps"}:
root is the directory to approve (the path, or a file’s folder) and
overlaps lists other workspaces whose roots it shares. A workspace without
roots binds its first ingest as usual. The dashboard offers to add root
through POST /api/workspace/roots (confirming any sharing) and submits the
job again. CLI argv sent as args is checked when the job runs, like a direct
CLI call. See workspaces.
# Ingest a URL in the "docs" projectcurl -s -X POST http://localhost:8420/api/commands/ingest-url \ -H "Content-Type: application/json" \ -d '{"args": ["https://example.com/article"], "project": "docs"}'| Method | Path | Purpose |
|---|---|---|
| POST | /api/sync/export |
Write a snapshot bundle to a server-side path. |
| POST | /api/sync/import |
Restore from a server-side bundle path. |
| GET | /api/sync/export/download |
Stream a zipped snapshot to the browser. |
| POST | /api/sync/import/upload |
Multipart upload of a zip bundle. |
| POST | /api/sync/serve |
SSE stream: serve one workspace for live sync. JSON body (all optional): workspace (an existing workspace; default: active, captured at start), direction = both | send | receive (default both) and relay (host, default: the public relay). Invalid values return 400. A POST with a JSON body so a cross-site page cannot start a session. |
| POST | /api/sync/receive |
SSE stream: join a served room. Body: {"code"} — the share code the serving side shows, <room>-<pin>, on its own or behind <host>/ or wss://<host>/ — plus optional workspace (default: active; a new name is created when data arrives) and direction = receive (default) | both | send. A send-only receive needs an existing workspace. |
| POST | /api/sync/cancel |
Cancel an active peer-sync session by session_id. |
| POST | /api/sync/approve |
Resolve a pending sync decision. Body: {"session_id": "...", "decision": "accept" | "trust" | "reject"}. Serving: answers approval_required (trust also remembers the device). Joining: answers confirm_push (accept sends the workspace, anything else skips the push). |
| GET | /api/desktop-learn/history |
Previously-indexed Claude Desktop / ChatGPT Desktop export zips (SHA-keyed). |
| POST | /api/desktop-learn/scan |
One-shot Downloads scan for new desktop-app conversation exports. Body (optional): {"paths": ["/extra/dir", ...]}; extra folders must be inside the workspace’s approved roots (ADR 0034). Returns the structured scan result used by the dashboard. |
Live peer sync runs the same sessions as imprint sync (one implementation,
imprint/sync_ws.py), end-to-end encrypted — see sync. Beyond
status events, serve emits room (code, target — what the other
machine enters: the code, behind the relay address unless it is the default —
room_id, pin, relay, workspace and the pull/push grant peers
receive), approval_required / auto_accepted / trusted, peer_connected
and refused (request: pull | push) when its direction policy turns a
peer’s request down; receive emits session (session_id, workspace),
peer_connected (the serving device), confirm_push (workspace and the
serving device; answer through /api/sync/approve), plan (pull, push,
skipped reasons), workspace_created and warning. progress events carry
phase = send | receive | pull | push | store. The session uses
the workspace passed explicitly and never changes the active one. Snapshot
endpoints cover export → transfer zip → import when a relay connection isn’t
available.
Config
Section titled “Config”| Method | Path | Purpose |
|---|---|---|
| GET | /api/config |
All resolved config values + their source, and each setting’s choices; model.device’s say what each value does on this device, and model.gpu_device’s list the GPUs llama.cpp finds. |
| PUT | /api/config/{key} |
Set a config key. Body: {"value": ...}. |
| DELETE | /api/config/{key} |
Remove the config.json override. |
| GET | /api/ui |
{mode, tutorials}: the dashboard this device shows (ui.mode) and whether tutorials start by themselves (ui.tutorials), read by the dashboard before it draws its navigation (ADR 0182). |
| GET | /api/settings/simple |
The simple dashboard’s settings: groups of items, each a switch (on), a choice (options, selected or null when the stored values match none), a link, the theme or the report of what leaves the computer. locked names the environment variable that holds an item’s setting. |
| PUT | /api/settings/simple/{item} |
Apply one item: {"on": bool} for a switch, {"option": n} for a choice, which writes every setting of that option. Returns the keys written and the groups again. 400 for a choice the item does not offer or an option that is unavailable here, 404 for an unknown item. |
PUT answers {"ok": true, "value": ...} and DELETE {"ok": true, "removed": ...}. No setting installs anything: a change to model.device or model.gpu_device takes effect in each process once it restarts (ADR 0086).
# Turn on LLM taggingcurl -s -X PUT http://localhost:8420/api/config/tagger.llm \ -H "Content-Type: application/json" \ -d '{"value": true}'Audio file ingestion
Section titled “Audio file ingestion”The dashboard’s Audio action uses POST /api/commands/ingest with the ordinary
{"dir": "/server/path/meeting.m4a"} body. The existing queue pins its workspace,
reports model preparation/download bytes and transcription seconds, and cancels
its owned process group. Meeting recordings are the one raw media upload and
media-serving path (see above). Settings are exposed through /api/config under
audio.*; see audio ingestion.
Completed audio is read from /api/sources/read as kind="conversation" with
details.media="audio". Segment blocks have role="speaker", kind="message",
audio_start/audio_end seconds, language and speaker=null. These are ASR
estimates; they do not imply a recognized speaker or an exact recording transcript.
| Method | Path | Purpose |
|---|---|---|
| GET | /api/chat/status |
Configured provider + model + readiness. For local, error reports a llama.cpp binding that does not load (a damaged installation). |
| GET | /api/chat/sessions |
List chat sessions, each with its kind (chat, or voice for a conversation with Echo) and replying while a reply is being written in it. |
| POST | /api/chat/sessions |
Create a new session. |
| GET | /api/chat/sessions/{sid} |
Session transcript (messages, each with its seq) and summary: the running summary (text, covers: the last message seq it stands in for, exchanges: user messages covered, retry_after, updated_at), or null before the first one; replying while a reply is being written, and question: the question that reply waits on (the question event), or null. |
| GET | /api/chat/sessions/{sid}/reply |
The reply being written, as the same SSE stream as /api/chat, from its first event; 404 when none is. |
| POST | /api/chat/sessions/{sid}/stop |
Stop the reply being written ({"ok": false} when none is). The only thing that cancels it. |
| POST | /api/chat/sessions/{sid}/answer |
Answer the question the reply waits on: {"id", "answer"}, from any window (ADR 0159). 404 when no reply is being written, 409 when no question with that id waits, 400 for an empty answer, one over 2,000 characters, or a choice outside options when allow_other is false. |
| POST | /api/chat/sessions/{sid}/rename |
Rename session. |
| DELETE | /api/chat/sessions/{sid} |
Delete session. |
| POST | /api/chat |
Send a message. Returns an SSE stream of tokens + tool calls. The reply runs in the API: closing the stream only stops watching it (ADR 0145). 409 while a reply is being written in the session. Replies are written one at a time; one that waits for another chat’s first sends analysis “Waiting for the reply in another chat to finish…”. |
/api/chat events, in order of appearance:
download_start(file,repo,path),download_progress(downloaded,totalbytes),download_complete: the model download.load_start: the local model is being loaded into memory.analysis(text): transient section-worker/synthesis progress for oversized questions or evidence, andSummarizing the earlier conversation…while the running summary is written. Worker findings are not streamed as assistant text.summary(text,covers,exchanges,retry_after): the session’s running summary changed, before the reply’s research starts. It is stored with the session. After a summary that failed,textandcoversare unchanged andretry_afteris the message seq to wait for before trying again. See ADR 0141.notice(text, optionaltitleandcode): an informational status, not a failed reply. Research completion usestitle="Finishing the answer"andcode="research_complete", followed by final answer generation without tools. Research now ends on resource budgets or evidence stagnation, with no fixed tool-round cutoff; the notice text names the reason. See ADR 0102. On a conversation’s first reply, the local model may have no tool-call format (chat.tool_formatisautoand its template matches none), so the reply runs without tools (ADR 0061).token(text): reply text. Tool-call markup is never streamed, so the tokens of a reply concatenate to what the user sees.tool_call(name,args),tool_result(name,result). One model turn may call several tools; each call is followed by its result before the next. Calls use each model’s native tool protocol (ADR 0059).question(id,question,options,allow_other), between anask_usercall and its result: the reply waits forPOST /api/chat/sessions/{sid}/answer, then sendsanswer(id,text;nullwhen none came within 30 minutes). The exchange is stored as theask_usertool call and its result (ADR 0159).assistant_message(text): the complete reply, stored in the session.error(error), thendone.
Closing the stream early stops the reply; the text already streamed is stored in the session as the assistant’s message.
chat.analysis_workers is exposed through the ordinary config endpoints, with
default 2 and choices 0 (off) through 8. Section workers run sequentially.
When analysis captures a source, tool-capable chat replies may call the
reply-local read_chat_section(source_id, start=0, max_chars=2000) function;
max_chars is limited to 1–4,000 and offsets are characters. Originals and IDs
are scoped to that reply, with no filesystem or cross-workspace read authority.
See large chat questions and evidence.
When the session has earlier messages, tool-capable replies may also call the
read-only conversation_history(query?, message?, start=0) function: query
searches the session’s earlier messages, tool results included, and message
reads one by its seq, 4,000 characters from start. It reads only the session
the reply belongs to. See long conversations.
Streams & long-polling
Section titled “Streams & long-polling”These endpoints return Server-Sent Events:
/api/jobs/{id}/stream— live stdout/stderr for a background job/api/chat— agent tokens + tool call events/api/sync/serveand/api/sync/receive— peer-sync progress events/api/stream— anupdateevent when the active workspace’s data changes (any process: ingest jobs, agents, sync). Overview and project views are checked against the data on every read, so refetching after an event, or at any time, returns current data.
Use EventSource in the browser, sseclient in Python, or curl -N to
consume. Postman shows raw bytes — useful for inspecting events but not for
interactive streaming.
MCP vs HTTP
Section titled “MCP vs HTTP”The HTTP API and the MCP server expose similar capabilities through different transports. Pick the one that matches your integration:
| MCP (stdio) | HTTP API | |
|---|---|---|
| Consumer | Coding agents (Claude Code, Cursor, Copilot, Cline, OpenClaw) | Anything that speaks HTTP |
| Transport | JSON-RPC over stdio | REST + SSE |
| Auth | None (local process) | Local access only; no supported authenticated remote mode |
| Ingest content | ingest_content |
POST /api/commands/ingest* (job queue) |
| Search | search |
GET /api/search |
| Streaming | Native | SSE |
| Best for | Agent integrations | Local services, CI, dashboards |
Browser capture (WIP branch)
Section titled “Browser capture (WIP branch)”The experimental Chrome extension uses a separate local
router. All requests require Authorization: Bearer TOKEN and
X-Imprint-Extension-Id: ID; any supplied Origin must exactly match the paired
chrome-extension://ID. Pairings grant one existing workspace. This router does
not expose CLI commands, arbitrary filesystem access, or general dashboard data.
| Method | Path | Purpose |
|---|---|---|
| GET | /api/browser/status |
Paired workspace and maximum request bytes. |
| POST | /api/browser/captures |
JSON {project, documents:[{kind, url, title, text, account?, message_id?}]}. Returns 202 {job_id, status, workspace}. Kinds: page, selection, gmail. Gmail requires account identity and a message ID. |
| GET | /api/browser/jobs/{id} |
Only this pairing’s capture job status, without payload or logs. |
Captures use the shared queue and inline ingestion pipeline. Request limit:
1,000,000 UTF-8 bytes; 1–50 documents; 10–800,000 characters per document.
Indexed provenance uses source_type=browser, source_url and doc_metadata.
See ADR 0029 for the boundary and WIP limits.
The downloadable extension currently sends pages and selections. Gmail support
is marked Coming soon; its original backend payload scaffold remains for
future development. See ADR 0030.
Versioning
Section titled “Versioning”The API follows the same release channel as the rest of Imprint — see
installation. The
Postman collection in this repo is versioned alongside the code; the live
OpenAPI schema at /openapi.json always matches the running container.
Paired sync controls
Section titled “Paired sync controls”GET /api/sync/peers returns public pair status without credentials.
POST /api/sync/peers/{pair_id} accepts {"action":"enabled","enabled":false},
{"action":"forget","confirm":true} or {"action":"remove","confirm":true}.
Forget and remove require prior user confirmation (the literal true) and cover
every paired workspace with that device. Forget revokes and reports remote
removal as pending until the other device acknowledges. Remove deletes this
device’s pairing, credentials and trust for that device at once without
contacting it, for a device that is gone. Existing memories remain. Both answer
with the updated peers list; an unknown pairing is a 400. The local-origin
boundary applies. Serve/receive SSE requests accept optional boolean remember (default
false); both sides must opt in. A paired event confirms completion. The backend
runs automatic reconnects while alive. See sync.