REST API
The daemon binds to 127.0.0.1:17989 only — LAN clients cannot reach it. The port is fixed and not configurable.
The same daemon serves the web dashboard at /. The dashboard is embedded into the binary at build time, so http://127.0.0.1:17989 is the whole surface — there is no hosted front-end. AGENVOY_PAGE_DIR swaps the embedded copy for files on disk during development (make dev).
Since v1.0.1 the dashboard also runs offline. GET /sw.js serves a service worker that precaches the embedded assets under a versioned cache; GET /vendor/* serves the third-party assets (QuickUI, Font Awesome, Material Symbols, and the nanomd / voice scripts) from ~/.config/agenvoy/vendor/, which the daemon downloads once at startup and re-downloads when the binary version changes. Nothing is fetched from a CDN at page load. In AGENVOY_PAGE_DIR mode /sw.js instead serves a teardown worker that clears its caches and unregisters itself, so a dev build is never served from a stale cache.
Endpoints marked local additionally require the request to originate from 127.0.0.1/::1 (the localhostOnly() guard). They manage credentials, config files, or process lifecycle and are meant for a same-machine dashboard, not remote clients. Unmatched routes carry the same guard: an unknown /v1/ path returns a JSON 404, and any other unknown path falls back to the dashboard index.
Agent execution
| Method | Path | Description |
|---|---|---|
POST |
/v1/send |
Run an agent request |
POST |
/v1/chat/completions |
Stateless OpenAI-compatible chat completion |
GET |
/v1/info/version |
Build version stamped at compile time ({version, dev}); dev is true for an untagged build |
GET |
/v1/log |
SSE stream. With no query it carries daemon slog records only (EventDaemonLog frames, source = level) — the same feed the TUI header shows. It includes new-chat verification codes, so daemon frames are attached only for loopback callers. ?sessions=a,b adds those sessions' events on the same connection; replay=0 skips the backlog, daemon=0 drops the daemon frames. A remote caller must pass sessions |
GET |
/v1/daemon |
local — raw daemon.log content |
GET |
/v1/mcp/tools |
List the tools registered from connected MCP servers (mcp__*) |
POST /v1/send semantics
The body is {content, session_id?, sse?, model?, skill?, work_dir?, system_prompt?, exclude_tools?, persist?, chat?}; content is required. A request that arrives while the session is still running is appended to that run as a steer message instead of starting a new one.
chat |
persist |
session_id |
Result |
|---|---|---|---|
false (default) |
false (default) |
empty | Creates temp-<uuid>, removed after 30 min without changes |
false |
true |
empty | Creates http-<uuid>, retained |
true |
any | empty | Creates chat-<uuid>, retained |
| any | any | provided | Uses the supplied session_id (chat and persist are ignored) |
curl --fail-with-body -sS \
-H 'Content-Type: application/json' \
-d '{"content":"List the available tools","persist":false}' \
http://127.0.0.1:17989/v1/send
/v1/chat/completions is stateless: include prior messages in every request when continuity is needed. reasoning_effort accepts none low medium high xhigh max (plus the aliases minimal extra ultra); an omitted or unrecognized value falls back to the session's reasoning setting.
Models
| Method | Path | Description |
|---|---|---|
GET |
/v1/models |
List registered models (OpenAI {data:[...]} shape, auto included) |
GET |
/v1/models/*id |
Read one registered model |
POST DELETE |
/v1/models · /v1/models/*name |
local — add / remove a model |
GET POST |
/v1/model |
local — model routing in one object: dispatcher, summary, image, stt, tts, plus image_options, image_providers and audio_providers on read. The fields hold two different kinds of value. dispatcher, summary, stt and tts name a registered model (prefix@model) — stt and tts are drawn from GET /v1/model/audio, not from the session model registry. image names a provider endpoint (openai, codex, grok, grok-oauth, gemini) because each provider's image model is fixed inside go-llm-router; image_options lists only the providers that currently hold credentials, while image_providers and audio_providers are the full catalogs (audio_providers is openai, gemini). POST is a partial update — a field left out (or null) is untouched, "" clears it, and off is an alias of "" for image. An unregistered model, an unknown provider, or a provider with no credentials is rejected and nothing is written |
GET |
/v1/model/audio |
local — stt_options and tts_options: the speech-to-text and text-to-speech models reachable with the credentials stored right now, fetched live from OpenAI and Gemini and returned as provider@model. These are the only values POST /v1/model accepts for stt / tts |
GET POST |
/v1/model/priority |
local — fallback order of the registered models. GET returns models (names in order), tiers (the model_tag map, model → tier) and tier_options ({tier, detail} rows, "" first for no tier). POST {models} moves the listed names to the front in that order and keeps the rest after them; an unknown name returns 400 |
POST |
/v1/model/tier |
local — {model, tier}. Sets one registered model's tier: S, A, B, C, or pass (never picked by auto routing or subagents, last in fallback); "" clears it. An unregistered model or unknown tier returns 400 |
Sessions
| Method | Path | Description |
|---|---|---|
GET |
/v1/sessions |
List sessions and status |
GET |
/v1/usage |
local — 24 h / 7 d / 28 d total token usage across sessions |
POST |
/v1/session |
local — create a session; {prefix} defaults to cli- |
GET POST DELETE |
/v1/session/:id |
local — one session's full state in one call: id, self_id, name, rule, state, model, reasoning, levels, count. POST is a partial update — self_id / name / rule / model / reasoning are all optional and a field left out (or null) is untouched; model: "" resets to auto, reasoning must be one of levels. A duplicate self_id returns 409. DELETE removes the session directory, history, state, and vectors. GET also takes ?chat=1 to append the raw action log under chat and ?usage=1 to append per-model token usage under usage; both are off by default because the log can be large |
POST |
/v1/session/:id/event |
local — publish an event into a session's stream |
POST |
/v1/session/:id/memory |
local — one memory operation, picked by action: summary rebuilds the rolling summary and returns count; compact drops older messages and returns removed; reset clears the conversation and returns removed, and requires mode — summary keeps the rolling summary, all wipes it too |
POST |
/v1/session/:id/cancel/:task_hash |
Cancel one running task. Returns {ok, cancelled:true} when that task hash is running in this process; otherwise (or for current) it records a canceled event on the session stream and returns {ok, cancelled:false, stale:true} |
POST |
/v1/session/:id/confirm/:confirm_hash |
Resolve an outstanding tool confirmation: {approve, remember?, allow_turn?, abort?, reason?, password?}. 410 when the confirmation was already resolved or expired. Approving a confirmation that lists restricted paths must come from loopback (403 otherwise) and passes the OS password check with password (401 on failure); the check is skipped while the sudo ticket is still cached, which the confirm event reports as password_cached |
Pending and completed tasks
| Method | Path | Description |
|---|---|---|
GET |
/v1/session/:id/task |
List resumable pending (ask_user / confirm) tasks. Tasks whose run is still live are excluded — a run refreshes action:<session_id>:<task_hash> in ToriiDB every 55 s with a 60 s TTL (v0.35.3), so a task left behind by a closed window or a killed process reappears here within a minute |
GET |
/v1/session/:id/task/:task_hash/questions |
Get a pending task's questions |
POST |
/v1/session/:id/task/:task_hash/resume |
Answer a pending task and resume |
DELETE |
/v1/session/:id/task/:task_hash |
Discard a pending task without answering |
GET |
/v1/session/:id/task/history |
local — completed tasks of this session, newest first: {task_hash, end_at, objective, model, reasoning} per row. ?keyword= filters on the objective and the recorded action text |
GET |
/v1/session/:id/task/:task_hash/history |
local — the full action record of one completed task, returned as a JSON string under content. 404 when that hash has no record |
Channels
| Method | Path | Description |
|---|---|---|
GET |
/v1/channel |
local — every channel read in one object: telegram and discord each carry {enabled, username, has_token}, and admin carries {channel, authorized, chats:[{value,type,id,name}]}. chats comes from the .telegram / .discord auth files (tg first, then dc) and each value can be posted back as-is; authorized says whether the current relay target is still on that list (a hand-typed ID reads false) |
POST |
/v1/channel/telegram · /v1/channel/discord |
local — {action:"enable"|"disable", token?}. Enable stores the token and flips the config flag only; the GetMe verification the TUI performs is intentionally skipped, since the daemon's config-file watcher already reconnects the bot and fills in its username |
GET |
/v1/channel/:channel/chats |
local — chats that finished verification for telegram / discord. Only meaningful while the bot runs, so ask for it after the channel reports enabled |
DELETE |
/v1/channel/:channel/chat |
local — {id}. Drops one chat from that auth file; the chat has to verify again before the bot answers it. 404 when the id is not on the list |
POST |
/v1/channel/admin |
local — {value:"tg@<chatID>"|"dc@<channelID>"|""}. Sets where new-chat verification codes are relayed; an empty string clears it. value is required (omitting it returns 400 so an empty body cannot silently clear the setting). Only the format is validated — an ID that is not authorized makes the relay log a warning and keep the code log-only |
Files & credentials
| Method | Path | Description |
|---|---|---|
GET PUT |
/v1/file |
local — read / write a file |
GET |
/v1/file/open |
local — open a file or URL with the OS default handler |
GET |
/v1/file/locate |
local — find candidate paths for a bare file name (name, plus optional dir=1, child, size, mtime filters) |
GET |
/v1/workdir |
local — resolve and validate a work directory (?path=), returning the absolute path |
GET DELETE |
/v1/key |
local — check / delete a single credential in the keychain |
GET POST |
/v1/keys |
local — list / set credentials |
Providers
| Method | Path | Description |
|---|---|---|
GET |
/v1/providers |
local — list providers and their available auth methods. Each row also carries logged_in, true only for the three OAuth providers (codex, copilot, grok-oauth) that currently hold a token |
GET |
/v1/providers/quota |
local — remaining quota for codex, grok-oauth, copilot, ollama-cloud (kind:"percent") and remaining credit for openrouter, deepseek (kind:"balance"), returned under quota keyed by provider and fetched in parallel with a 10 s ceiling. Renamed from /v1/providers/usage in v0.35.3. Successful reads are cached in ToriiDB for 3 minutes and come back flagged cached:true; ?refresh=1 drops the cache and re-reads, and saving a key or finishing an OAuth login drops that provider's entry on its own. Providers without a credential come back with error instead of value and are never cached |
POST |
/v1/provider/:provider/key |
local — set an API key |
GET |
/v1/provider/:provider/oauth |
local — SSE device-code OAuth flow. An existing token is left in place while the login runs, so an abandoned or failed re-login no longer logs the provider out |
DELETE |
/v1/provider/:provider/oauth |
local — clear a stored provider login (codex, copilot, grok-oauth). The token keys belong to the OAuth libraries, so this goes through their own ClearToken rather than DELETE /v1/key |
GET |
/v1/provider/:provider/models |
local — list models available to this provider. For a custom compatible provider instance the model list is probed live from its endpoint (502 when the probe fails) |
MCP
| Method | Path | Description |
|---|---|---|
GET POST |
/v1/mcp |
local — list / add MCP servers. The GET also returns oauth: {name: bool} for the HTTP servers, saying which ones already hold a token |
POST |
/v1/mcp/remove |
local — remove an MCP server |
GET |
/v1/mcp/status |
local — connection status per server |
POST |
/v1/mcp/reconnect |
local — reconnect all MCP clients and re-register tools |
GET |
/v1/mcp/oauth?name=X |
local — SSE OAuth login for one HTTP MCP server, mirroring the provider flow: emits {"url":...} for the browser, then {"done":true,"ok":...} (plus reconnect_error when the post-login reconnect fails). Times out after 10 minutes, or when the client disconnects |
POST |
/v1/mcp/oauth/callback |
local — {name, url}. Hands the redirect URL back when the browser cannot reach the daemon's loopback listener on localhost:17988; the code is parsed out of the URL's query. 400 if no login is waiting for that server |
POST |
/v1/mcp/oauth/client |
local — {name, client_id, client_secret?, redirect_uri?}. Stores a pre-registered OAuth client for servers that reject dynamic registration; redirect_uri defaults to http://localhost:17988/callback and must match the provider console exactly. Clears any existing token first |
DELETE |
/v1/mcp/oauth |
local — {name}. Clears both the stored token and the client registration for that server |
Rules, notes & skills
| Method | Path | Description |
|---|---|---|
GET |
/v1/rules |
local — list session-prompt rules stored as .md files under prompts/ |
GET |
/v1/rule/*name |
local — read one rule |
POST PATCH DELETE |
/v1/rule |
local — create / update (with optional rename) / delete a rule |
GET |
/v1/notes |
local — list operator notes under notes (name, size, updated_at); records live in the SQLite note table, not on disk |
GET |
/v1/note/*name |
local — read one note (name, content, updated_at) |
POST PATCH DELETE |
/v1/note |
local — create / update (with optional rename) / delete a note. The name defaults to the first line when omitted, and is capped at 32 runes |
GET |
/v1/skills |
local — list installed skills |
GET |
/v1/skill/*name |
local — read one installed skill: name, description, path, source, content, deletable, plus files — every UTF-8 file under the skill's scripts/ references/ assets/ directories as {path, content}, sorted by path, dotfiles skipped and anything over 256 KiB omitted (v1.0.2) |
DELETE |
/v1/skill |
local — remove one installed skill |
The /v1/knowledge* endpoints were renamed to /v1/note* in v0.35.4; the store moved from ToriiDB to SQLite in v0.35.3. Existing notes are migrated in at daemon startup.
Automation
Cron entries and one-off tasks are one surface. The separate /v1/cron* and /v1/task* endpoints were replaced by /v1/schedule in v0.33.6, with type=cron|task selecting between them.
| Method | Path | Description |
|---|---|---|
GET |
/v1/schedule |
local — list cron entries and one-off tasks as one schedules array, each tagged type=cron|task; ?type= narrows to one |
GET |
/v1/schedule/*skill |
local — read a scheduler skill: name plus body, which since v1.0.2 is the raw SKILL.md including its frontmatter rather than the parsed-off body. files carries the skill's scripts/ references/ assets/ contents on the same terms as GET /v1/skill/*name. The description field was removed |
POST PATCH |
/v1/schedule |
local — create / update a scheduler skill from name / content and rebind its whole entry set to type=cron|task; switching type drops the entries the skill held under the other one. description was removed in v1.0.2. A content that already opens with its own --- frontmatter is written verbatim; otherwise the server composes a minimal ---\nname: <name>\n--- header |
DELETE |
/v1/schedule |
local — delete a skill's entries (type narrows to one, omitted removes both); trashes the skill when nothing else binds it |
POST |
/v1/schedule/run |
local — fire a schedule now (202 Accepted) |
Allowlists
| Method | Path | Description |
|---|---|---|
GET POST |
/v1/allowlist |
local — both allowlists in one object, skill and tool. GET reads ?scope=global|project (with ?work_dir= required for project) for the skill block and ?prefix= to narrow the tool block. POST takes {skill: {name, scope?, work_dir?}} to toggle one skill and/or {tool: {prefix, entries}} to replace just that prefix's auto-approve entries (the same call the TUI's /mcp → permission makes), so unrelated rules survive; every entry must start with prefix, and prefix* collapses the rest. A block left out is untouched |
Configuration
| Method | Path | Description |
|---|---|---|
GET POST |
/v1/config/startup |
local — launch-on-login. GET returns enabled (the recorded setting) and installed (whether the launchd agent or systemd user unit exists). POST {enable} (required) writes or removes the launchd agent (macOS) or systemd user unit (Linux) and returns {ok, enabled, installed, detail}; it never starts or stops the running daemon, and takes effect at the next login |
GET POST |
/v1/config/system |
local — reply language. GET returns reply_lang (auto by default) and languages ({code, label} options, auto first). POST {reply_lang} (required) canonicalizes the code, rejects values over 64 bytes or containing a line break, and writes reply_lang to config.json |
GET POST |
/v1/config/output_dir |
local — output directory. GET returns output_dir (the raw setting) and resolved (the directory in use; ~/Downloads when unset and that folder exists). POST {output_dir} (required) expands ~, creates the directory, and writes it to config.json; "" restores the default |
Any other :target returns 404.
Inspection
| Method | Path | Description |
|---|---|---|
GET |
/v1/torii/error |
local — read the tool-error memory store. keyword alone decides the mode: without it the store is listed (?tool= narrows that listing to one tool, ?limit= defaults to 50); with it the records are searched (?limit= defaults to 16). Before v1.0.1 a bare ?tool= fell through to the search path and came back empty |
PATCH |
/v1/torii/error |
local — {id, action}, both required. Replaces the recorded action of one error-memory record and returns it under record; 404 when the id does not exist |
The /v1/toriidb HTTP gate was removed in v0.35.3. Since ToriiDB v0.6.2 every process talks to the store over ToriiDB's own socket, so tool cache, chat vectors, error memory and pending liveness are shared without an HTTP hop.