Skip to main content
The HQ Management API lets you monitor and control the HQ programmatically. It’s the same API Desktop uses to deploy agents, connect messaging apps, store credentials, and install plugins. It binds to 127.0.0.1 on port 9300 by default. Change the port with --management-port. This page documents the administrative routes. Phones, tablets, and the web app never receive administrative access. They use the versioned /mobile/v1 namespace instead, documented in the HQ Mobile API.

Overview

The HQ runs up to three HTTP listeners: The mobile LAN listener forwards each /mobile/v1 request to the management listener’s own handlers in the same process. A request for any other path, including every route on this page, never reaches them. The hosted relay works the same way: it forwards only /mobile/v1 traffic, which the HQ replays against its management listener. Administrative routes are therefore reachable only from the HQ machine itself. Each route below is marked Loopback only or Also under /mobile/v1. A route that is also under /mobile/v1 has the same handler at /mobile/v1 plus the path, authenticated with the mobile bearer. Some mobile variants accept fewer fields; the differences are noted in each section.

Authentication

Send the management bearer in the Authorization header on every request:
The HQ reads its tokens from startup flags: The two scopes never overlap. The management bearer is rejected under /mobile/v1, and the mobile bearer is rejected everywhere else. The HQ refuses to start when both tokens are set to the same value. Relay mode requires both. Three paths skip the bearer check: A missing or wrong bearer returns HTTP 401:
If you start the HQ with neither --token nor --chat-token, authentication is disabled and every route accepts unauthenticated requests. Once either token is set, a namespace whose token is unset rejects every request with 401.

Error shapes

Route groups return one of three error shapes. Each section below says which one it uses. The management API sets no global body-size limit. The speech routes add their own limits. Administrative routes send no CORS headers. CORS applies only to /mobile/v1; see the HQ Mobile API.

Route index

Conversation routes, POST /mobile/v1/ws-ticket, and the chat WebSocket are covered on other pages. See Conversations and chat.

Health and status

GET /health

Returns HQ status and the capabilities clients may use. No authentication is required. Also under /mobile/v1 as GET /mobile/v1/health.
conversation-sync-v1, chat-resume-v1, and conversation-control-v2 are always present. speech-v1 appears only while a speech provider can both transcribe and speak. A failure while checking the speech provider leaves it out rather than failing the request. Ignore capability strings you don’t recognize.

GET /runtime/status

Returns a current operational snapshot. Loopback only.
GET /health remains the lightweight liveness check. Runtime status reports process state; it does not test provider availability or imply that queued work has finished. It contains no credentials, conversation text, or pending-message contents.

POST /lifecycle/shutdown

Asks the HQ to shut down gracefully. Loopback only. Desktop calls this before it falls back to a signal. No body is required. The HQ replies first, then starts shutdown about 100 ms later so the response is not cut off:

GET /identity

Returns the HQ’s stable ID and Ed25519 public key. Also under /mobile/v1, where native clients call it with the mobile bearer.
The public key identifies the HQ and is safe to share. The private key never leaves the HQ machine.

GET /lan-tls

Returns the SHA-256 fingerprint of the HQ’s LAN HTTPS certificate. Loopback only. Desktop reads it to build the pairing QR code, which carries the fingerprint to the phone out of band. The route exists only when LAN TLS is configured; otherwise it returns 404.

Agents

Create and manage agents. Agents are saved to ~/.dash/gateway/agents.json. Every agent route is also under /mobile/v1, where create and update accept fewer fields (see below). Agent routes use the structured error shape. Responses never include providerApiKeys.

Agent object

status is registered, active, or disabled. config holds every configuration field you set, except providerApiKeys.

Configuration fields

Any other key returns 400. Numeric caps must be positive integers, except swarm.maxSteersPerWorker and subagents.maxDepth, which also accept 0. modelAliases maps a bare alias (no /) to a model ID. Memory settings have their own routes; see Memory.

GET /agents

Lists every agent as an array of agent objects.

POST /agents

Creates an agent and returns it with HTTP 201. name, model, and systemPrompt are required. Under /mobile/v1, only those three fields are accepted.

GET /agents/:id

Returns one agent object.

PUT /agents/:id

Updates an agent’s configuration and returns the updated agent. Send only the fields to change; the body must not be empty. name cannot be changed. Under /mobile/v1, only model and systemPrompt are accepted.
A change to swarm or subagents restarts the agent’s warm runtimes so the next turn uses the new settings. Other changes are read on the next turn.

DELETE /agents/:id

Deletes an agent and returns {"ok": true}. The HQ cancels the agent’s running turns and swarm runs, removes it from messaging-channel routing, and deletes its browser sessions. Its conversations are archived, not deleted: their messages stay readable through the conversation routes.

POST /agents/:id/disable

Disables an agent without deleting it and returns {"ok": true}. The HQ stops admitting new turns for it, cancels its running turns and swarm runs, and releases its runtimes.

POST /agents/:id/enable

Re-enables a disabled agent and returns {"ok": true}.

POST /agents/:agentId/conversation-title

Generates a short title for a conversation from its first message. Loopback only. The title is produced by one small completion on the agent’s own model. When projects exist, the same call also picks the active project the conversation best fits.
project is null when no project fits. Errors use the plain shape: 400 for a missing text, 404 for an unknown agent, and 502 when the completion fails.

Memory

Read and edit what an agent remembers across conversations. See Memory. The list, read, and delete routes are also under /mobile/v1. Writing a memory and the settings routes are loopback only. Memory routes use the plain error shape, including under /mobile/v1:

GET /agents/:id/memory

Lists an agent’s memories without their bodies. It lists what is on disk even when memory is disabled for the agent.
type is user, feedback, project, or reference. source records who wrote it: agent, sweep, user, or import. size is the body length in characters.

GET /agents/:id/memory/:name

Returns one memory, including its content, with the same fields as the list minus size.

PUT /agents/:id/memory/:name

Creates or replaces a memory. The name comes from the path. Memories written here are recorded with source: "user". The name must be lowercase letters, digits, and hyphens, starting with a letter or digit, at most 64 characters.
action is created or updated.

DELETE /agents/:id/memory/:name

Deletes a memory and returns its name. This works while memory is disabled.

GET /agents/:id/memory/config

Returns the agent’s memory settings. An agent that has never set them reads as enabled with sweep: "auto".

PATCH /agents/:id/memory/config

Changes memory settings and returns the merged result. Omitted fields keep their current values. Changing enabled restarts the agent’s warm runtimes so the next turn gains or loses the memory tools. A sweep change applies on the next turn.

Skills

Manage the skills an agent can load. See Skills. Only GET /agents/:id/skills is also under /mobile/v1, as a read-only view; every other skill route is loopback only. Skill routes use the plain error shape:

GET /agents/:id/skills

Lists every skill available to the agent, from plugins and from the agent itself.
source is managed, agent (learned by the agent), remote, or plugin. Under /mobile/v1, each entry has only name, description, trigger, source, and content.

POST /agents/:id/skills

Creates a skill and returns HTTP 201 with its name and file location.

GET /agents/:id/skills/:name

Returns one skill with the same fields as the list.

PUT /agents/:id/skills/:name

Replaces a skill’s body and keeps its frontmatter. Send {"content": "..."}. Returns the skill’s name and location.

DELETE /agents/:id/skills/:name

Removes a skill and returns {"name": "..."}. Plugin skills cannot be removed here.

POST /agents/:id/skills/install

Installs a skill from a git repository, URL, or local path. The skill is security-scanned before it is installed, and installation fails closed.
verdict.verdict is safe or suspicious. A dangerous skill is rejected with 422.

GET /agents/:id/skills/config

Returns the agent’s skills configuration, or {} when none is set.

PATCH /agents/:id/skills/config

Merges fields into the agent’s skills configuration and returns the result.

GET /agents/:id/skills/:name/lessons

Returns the lesson book for a skill the agent learned. Returns 404 when the skill has no lesson book.
augments names other skills this book rides along with. retired holds lessons removed from the active list, each with retiredAt and retiredReason (harmful or capacity).

DELETE /agents/:id/skills/:name/lessons/:lessonId

Retires one lesson and returns the updated lesson book. The lesson moves to retired; it is not deleted, and a later review will not propose it again.

Sub-agents

Manage the helper agents an agent can start, and the sub-agents already running. Every route in this section is also under /mobile/v1. Definitions. The five definition routes read and write the per-agent sub-agent types. On the loopback API they use the plain error shape; under /mobile/v1 they use the structured shape. A definition :name must be lowercase letters, digits, and hyphens, starting with a letter or digit. Any other name returns 400. An unknown agent returns 404.

GET /agents/:id/subagent-types

Returns the agent’s resolved roster: built-in types plus workspace, plugin, and per-agent definitions.
Each type can also carry location, disallowedTools, skills, maxTurns, isolation (worktree), and shadowedBy. The system prompt is left out; read it from the definition route. unknownAllowedTypes lists names in the agent’s subagents.allowedTypes that match no type, which usually means a typo.

GET /agents/:id/subagent-definitions

Lists the definition files in the agent’s own writable directory. Workspace and plugin definitions appear only in the roster.

GET /agents/:id/subagent-definitions/:name

Returns one definition’s raw Markdown.

PUT /agents/:id/subagent-definitions/:name

Writes a definition and returns {"ok": true, "name": "reviewer"}. Send {"raw": "..."} with the full Markdown file, frontmatter included. The HQ validates the definition before writing it. The agent picks up the change on its next model turn.

DELETE /agents/:id/subagent-definitions/:name

Deletes a definition file and returns {"ok": true, "name": "reviewer"}. Returns 404 when the file doesn’t exist. Running sub-agents. The conversation, stop, and resume routes use the structured error shape on both mounts.

GET /conversations/:id/subagents

Lists the sub-agents started from a conversation. The list is read from saved state, so it still works after an HQ restart.
status is running, waiting_input, done, failed, cancelled, interrupted, or max_turns. report is scanned before it is returned. oneShot types, such as Explore and Plan, cannot be resumed. An unknown conversation returns 404 not_found.

POST /subagents/:id/stop

Stops a sub-agent and every sub-agent it started. No body is required.
Returns 404 not_found when :id isn’t a sub-agent, and 409 when the sub-agent has already finished.

POST /subagents/:id/resume

Sends a sub-agent a follow-up message, exactly as its parent would with the send_message tool.
mode is resumed when the message starts a new turn, or queued when it is delivered to a running sub-agent. Returns 400 for an invalid body, 404 for an unknown sub-agent, and 409 when the sub-agent can’t take the message: a one-shot type, a spent steer cap, or no parent conversation.

Swarm panel

Inspect and steer an agent’s swarm runs. A run is the set of sub-agents one parent turn started. Every route here is loopback only and uses the plain error shape; an unknown agent or run returns 404.

GET /agents/:id/swarm/runs

Lists the agent’s recent runs, including runs from before the last HQ restart.
startedAt and endedAt are epoch milliseconds.

GET /agents/:id/swarm/runs/:runId

Returns one run with the same fields plus a workers array of per-worker snapshots.

POST /agents/:id/swarm/runs/:runId/workers/:workerId/cancel

Cancels one worker. Returns {"ok": true}, or 409 with {"ok": false, "reason": "..."} when the worker can’t be cancelled. This also works for a background worker whose spawning turn has ended.

POST /agents/:id/swarm/runs/:runId/workers/:workerId/send

Sends one worker a message. Send {"message": "..."}; an empty message returns 400. Returns {"ok": true}, or 409 with {"ok": false, "reason": "..."} when the worker can’t accept it.

POST /agents/:id/conversations/:conversationId/swarm/cancel

Cancels every unfinished worker in the conversation’s live swarm turn. Workers keep running after the chat stream ends, so this is how the stop button reaches them.
cancelled is false when there was no live turn. Calling it again is safe.

Browser

Manage the browser agents use and the saved logins in their browser profiles. See Browser. Every route here is loopback only. Browser routes use the plain error shape. An invalid profile name returns 400, a profile in use by a running browser returns 409, and an unknown agent returns 404.

GET /browser/status

Returns which browser the HQ will launch, the managed-download progress, and the number of live browser sessions.

POST /browser/install

Starts downloading the managed browser and returns HTTP 202 immediately with the current progress. Poll GET /browser/status to follow it. Calling it again while a download is running returns the same 202 and does not start a second download. A failed download shows up as install.state: "failed".

GET /agents/:id/browser/profiles

Lists the agent’s saved browser profiles.

DELETE /agents/:id/browser/profiles/:name

Deletes a saved profile, including its cookies and logins, and returns {"name": "work"}. Returns 409 while the profile is in use.

Channels

Connect and manage messaging apps such as Telegram and WhatsApp. See Messaging Apps. Channels are saved to ~/.dash/gateway/channels.json. Every route here is loopback only and uses the plain error shape.

Channel object

adapter is telegram or whatsapp. A routing condition is {"type": "default"}, {"type": "sender", "ids": [...]}, or {"type": "group", "ids": [...]}.

GET /channels

Lists every channel as an array of channel objects.

POST /channels

Creates a channel, starts its adapter, and returns HTTP 201 with {"ok": true}. Store the channel’s credential first with POST /credentials: a Telegram bot token under channel:<name>:token, or WhatsApp auth JSON under channel:<name>:whatsapp-auth. Without it, creation fails with 400.

GET /channels/:name

Returns one channel object. URL-encode the name.

PUT /channels/:name

Updates a channel and returns the updated channel object. Send any of routing, globalDenyList, and allowedUsers; omitted fields are unchanged. Each must be an array.

DELETE /channels/:name

Stops the channel’s adapter, removes the channel, and returns {"ok": true}.

Credentials

Store provider API keys and messaging tokens in the HQ’s encrypted store (~/.dash/gateway/credentials.enc). See Secrets. Every route here is loopback only and uses the plain error shape. Two key formats have special meaning:

GET /credentials

Lists stored credential keys. Values are never returned.

POST /credentials

Sets one credential and returns HTTP 201 with {"ok": true}. Both key and value are required and must be nonempty.
Running agents pick up the new value on their next turn.

DELETE /credentials/:key

Deletes a credential and returns {"ok": true}. URL-encode the key. Phones and the web app manage provider API keys, and sign in with a Claude or ChatGPT subscription, through the Mobile API’s AI providers routes.

Models

List the models agents can use. See AI Providers. GET /models is also under /mobile/v1 as a read-only view; POST /models/refresh is loopback only. The HQ fetches each configured provider’s model list, filters it through its bundled provider catalogs, and caches the result for six hours. A provider that fails is retried after five minutes, and its last good models are kept meanwhile. Setting or deleting a provider API key clears the cache. With no provider keys at all, the response lists every catalog’s built-in models with source: "bootstrap".

GET /models

On the loopback API, ?debug=true adds bootstrap (the catalogs’ built-in models), patterns, providersConfigured, and providersAvailable. Under /mobile/v1, any query parameter returns 400 with the structured validation_failed error.

POST /models/refresh

Fetches the model list again, ignoring the cache, and returns the same shape as GET /models.

Speech

Dictation and read-aloud. Every speech route is also under /mobile/v1, with an identical handler. Hands-free voice mode runs over the chat WebSocket instead; see HQ Chat WebSocket. Speech errors use the structured shape and pass the provider-level code through:

GET /speech/config

Returns the current speech configuration and each provider’s availability.
An unavailable provider carries a reason: no_credential or no_provider_offers_realtime. stt.language is present only when a language is pinned, and tts.speed only when set.

PATCH /speech/config

Merges a partial configuration, saves it, and returns the same shape as GET /speech/config. Omitted keys keep their current values. An unknown key anywhere in the body returns 400.
A declared Content-Length above 64 KiB returns 413 too_large before the body is read.

GET /speech/models

Lists the models the configured provider offers for one kind.

POST /speech/transcriptions

Transcribes one recorded clip. Audio is sent inline as base64 JSON, not multipart.
durationSeconds is present only when the provider reports it. A declared Content-Length above 12 MiB returns 413 too_large before the body is read.

POST /speech/speech

Synthesizes speech from text. Send {"text": "..."}, 1 to 4,000 characters.
The success body is audio, not JSON. It streams as audio/mpeg, except for a PCM-only model (currently Google’s Gemini TTS), which returns a complete audio/wav file. An error raised before the first audio chunk is still JSON, so accept audio/mpeg, audio/wav, and application/json. A declared Content-Length above 64 KiB returns 413 too_large.

MCP servers

Connect Model Context Protocol servers whose tools agents can use. Every route here is loopback only and uses the plain error shape. A server’s tools are named <server>__<tool>. Add a server to an agent by listing its name in the agent’s mcpServers.

MCP server object

transport is one of: status is connected, disconnected, reconnecting, error, or needs_reauth.

GET /runtime/mcp/servers

Lists every saved MCP server as an array of MCP server objects.

POST /runtime/mcp/servers

Connects a new MCP server, saves it, and returns HTTP 201.

GET /runtime/mcp/servers/:name

Returns one MCP server object.

DELETE /runtime/mcp/servers/:name

Disconnects and removes a server, removes it from every agent’s mcpServers, and returns {"ok": true}.

POST /runtime/mcp/servers/:name/reconnect

Disconnects and reconnects a saved server.

POST /runtime/mcp/servers/:name/reauthorize

Restarts sign-in for a server whose status is needs_reauth. Returns {"ok": true, "status": "..."}, 404 for an unknown server, or 500 when the server is in any other state.

GET /runtime/mcp/allowlist

Returns the URL patterns that remote MCP servers must match, as an array of strings. An empty list allows every URL. stdio servers are not checked.

PUT /runtime/mcp/allowlist

Replaces the allowlist. Send a JSON array of strings; * matches any run of characters.

Plugins

Install and manage plugins. See Plugins. Every route here is loopback only and uses the plain error shape. Changes are saved first and then applied by reloading every plugin.

GET /plugins

Lists every plugin, including disabled and failed ones, sorted by name.
status is loaded, disabled, or error. activated lists the component kinds in use; noop lists kinds present but not active, such as code components of an untrusted plugin. A built-in plugin has builtin: true. A failed plugin has failure with phase (discovery, manifest, or route), error, and failedAt.

POST /plugins/install

Fetches, scans, and installs a plugin, then reloads. The plugin is enabled but not trusted: its code components stay inactive until you trust it with PUT /plugins/:name.
Returns HTTP 201:
If the install succeeds but the reload fails, the plugin is still installed. The HQ returns HTTP 200 with {"ok": true, "installed": {...}, "note": "...", "error": "..."}, and the next reload picks it up.

PUT /plugins/:name

Enables, disables, trusts, or untrusts a plugin, then reloads and returns its updated record.
Returns 400 for a non-boolean field and 404 for an unknown plugin. If the reload fails, or the plugin is missing afterward, it returns 409 with error, plugin, and note: the setting was saved, but the running plugins are unchanged.

DELETE /plugins/:name

Removes a plugin’s saved settings and, for a plugin installed through this API, deletes its directory. Then reloads.
path is present only when a directory was deleted. A built-in plugin returns 400; disable it instead. An unknown plugin returns 404. If the reload fails after removal, the response is 409 with ok: true, removed: true, error, and note: the removal stuck.

POST /plugins/reload

Rediscovers plugins without changing any settings.
A failed reload returns 500 with {"error": "..."}.

GET /runtime/plugins

Returns a compact view for model and credential pickers: providers contributed by plugins, and every plugin that isn’t disabled.

Projects, issues and inbox

Track work in projects and issues. See Projects. Every route here is loopback only and uses the plain error shape. Wherever a path takes a project or issue :id, you can pass its ID or its key, such as WEB or WEB-12. Body fields and query filters, such as project_id, take IDs only. Writes through this API are recorded as made by the local human user. Live updates are pushed over the /projects/ws WebSocket; see HQ Chat WebSocket.

Project object

status is active, paused, completed, or cancelled.

Issue object

status is backlog, todo, in_progress, review, done, or cancelled. sub_status is waiting_on_human, agent_working, blocked, or null. An issue with no project gets a TASK- key.

GET /projects

Lists projects as an array. Filter with ?status=; an invalid status returns 400.

POST /projects

Creates a project and returns it with HTTP 201. A rejected project, such as a duplicate key, returns 400.

GET /projects/:id

Returns a project plus issue_counts_by_status, a count for every issue status.

PATCH /projects/:id

Updates any of name, description, status, and archived_at (a timestamp or null), and returns the project. Other keys are ignored.

GET /projects/:id/issues

Lists a project’s issues as an array.

GET /issues

Lists issues as an array. Every query parameter is an optional filter:

POST /issues

Creates an issue and returns it with HTTP 201.

GET /issues/:id

Returns an issue plus comments, events, linked_sessions, and subtasks.

PATCH /issues/:id

Updates any of title, description, status, sub_status, assignee_user_id, project_id, and completed_at, and returns the issue. Other keys are ignored. An invalid status or sub_status returns 400.

DELETE /issues/:id

Deletes an issue and returns {"ok": true}.

POST /issues/:id/comments

Adds a comment and returns it with HTTP 201. Send {"body": "..."}; an empty body returns 400.

PATCH /issues/:id/comments/:commentId

Edits a comment’s text and returns the comment. Send {"body": "..."}. An unknown comment returns 404.

DELETE /issues/:id/comments/:commentId

Soft-deletes a comment and returns {"ok": true, "issue_id": "..."}.

GET /issues/:id/events

Returns the issue’s history as an array. Each event has id, issue_id, type, actor_type (human, agent, or system), actor_id, data, and created_at.

GET /issues/:id/sessions

Lists the chat sessions linked to an issue.

POST /issues/:id/sessions

Links a chat session to an issue and returns the link with HTTP 201.

GET /inbox

Lists the issues waiting for the local user.
issue and project are full objects, abbreviated here; project is null for a standalone issue. reason is waiting_on_human or new_activity.

POST /inbox/:issue_id/mark-read

Marks an inbox item read and returns {"ok": true}.

Events

GET /events

Streams HQ changes as server-sent events. Also under /mobile/v1, which streams the same events. Clients use it to know when to refetch cached data.
Each event’s name is its type, and its data is the JSON event:
A : keepalive comment is sent every 30 seconds. Ignore event types you don’t recognize.

GET /agents/:agentId/conversations/:conversationId/events

Replays a conversation’s durable chat events after a sequence number, so a client can catch up after a dropped WebSocket without resending the prompt. Also under /mobile/v1. Errors use the structured shape.
A deleted conversation returns {"entries": []}. Any query parameter other than a single sinceSeq returns 400 validation_failed. An unknown agent, or a conversation that belongs to a different agent, returns 404 not_found. See HQ Chat WebSocket for the event payloads and how to resume a turn.

Conversations and chat

Conversations, messages, and chat are documented on their own pages:
  • HQ Mobile API covers conversation lists, messages, titles, deletion, and POST /mobile/v1/ws-ticket. The conversation routes are also available unprefixed on this loopback API with the management bearer.
  • HQ Chat WebSocket covers /ws/chat: sending messages, resumable turns, answers, cancellation, voice mode, and browser live view.
The loopback management API on port 9300 and the channel server on port 9200 use separate management and chat credentials. Native clients never receive the management bearer: the pinned mobile listener and the relay accept only the mobile bearer, on /mobile/v1 and /ws/chat.