Skip to content
Download

HTTP API

Project association review:

  • GET /api/projects/associations?source=…&project=…&limit=200 returns the explicit workspace, known projects, canonical sources and versioned evidence for suggested/accepted/rejected links. Optional source and primary-project filters narrow the review; limit is 1–1,000.
  • POST /api/projects/associations accepts {workspace, source, project, target, status} with an explicit review workspace and status accepted or rejected. project is 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-projects queues 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.

Terminal window
# 1. Confirm the server is up
curl -s http://localhost:8420/api/version
# 2. Search the corpus
curl -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>/stream

In Docker, publish 127.0.0.1:8420:8420 and use the same local URL. Arbitrary remote Host/Origin values are rejected.

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.

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.
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.
Terminal window
# Search only decisions in one project
curl -s "http://localhost:8420/api/search?q=chunking&project=imprint&type=decision&limit=10"
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.
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.
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.

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.
Method Path Purpose
GET /api/kg Facts list. ?subject= partial match.
GET /api/kg/entity/{entity} Facts about one entity.
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.

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.

Terminal window
# Ingest a URL in the "docs" project
curl -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.

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

Terminal window
# Turn on LLM tagging
curl -s -X PUT http://localhost:8420/api/config/tagger.llm \
-H "Content-Type: application/json" \
-d '{"value": true}'

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, total bytes), 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, and Summarizing 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, text and covers are unchanged and retry_after is the message seq to wait for before trying again. See ADR 0141.
  • notice (text, optional title and code): an informational status, not a failed reply. Research completion uses title="Finishing the answer" and code="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_format is auto and 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 an ask_user call and its result: the reply waits for POST /api/chat/sessions/{sid}/answer, then sends answer (id, text; null when none came within 30 minutes). The exchange is stored as the ask_user tool call and its result (ADR 0159).
  • assistant_message (text): the complete reply, stored in the session.
  • error (error), then done.

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.

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/serve and /api/sync/receive — peer-sync progress events
  • /api/stream — an update event 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.

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

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.

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.

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.