HQ Management API
Loopback HTTP API to monitor and control a dashsquad HQ.
The HQ Management API gives you programmatic control of the HQ. Desktop uses the same API to
deploy agents, connect Telegram and WhatsApp channels, store credentials and install plugins. The API listens on
127.0.0.1, port 9300 by default. To change the port, use --management-port.
This page documents the administrative routes. Phones, tablets and the web app never get
administrative access. They use the mobile namespace /mobile/v1. The
HQ Mobile API page documents this namespace.
Overview
The HQ runs two HTTP listeners. Both listen on the loopback address only:
| Listener | Default address | Flag | Serves |
|---|---|---|---|
| Management | http://127.0.0.1:9300 | --management-port | All routes on this page, plus /mobile/v1 |
| Channel server | http://127.0.0.1:9200 | --channel-port | The /ws/chat WebSocket. See HQ Chat WebSocket |
Ports 9300 and 9200 and the folder ~/.dash/gateway on this page apply to a standalone HQ. An HQ that Desktop starts uses free ports and the folder ~/.dashsquad/<hq id>. It lists its ports in ~/.dashsquad/<hq id>/ports.json. See Data folders.
The HQ has no LAN listener. The HQ accepts the old --lan-port flag, but it ignores the flag and
logs a warning. Remote clients connect through the hosted relay. The relay forwards only HTTP
requests for /mobile/v1, and the /ws/chat WebSocket. Thus, only a process on
the HQ machine can reach the administrative routes.
Each route section has one of these marks:
| Mark | Meaning |
|---|---|
| Loopback only | The route is on the management listener only, with the management bearer |
Also under /mobile/v1 | The same handler is also at /mobile/v1 plus the path, with the mobile bearer |
Some mobile variants accept fewer fields. Each section gives the differences.
Authentication
Send the management bearer in the Authorization header with each request:
Authorization: Bearer <your-management-token>The HQ reads its tokens from environment variables when it starts:
| Variable | Token | Accepted on |
|---|---|---|
DASHSQUAD_HQ_TOKEN | Management bearer | All routes outside /mobile/v1 |
DASHSQUAD_HQ_CHAT_TOKEN | Mobile bearer | On this listener, only /mobile/v1 and the paths below it. It is also the /ws/chat token |
The old --token and --chat-token flags still work in this release, but the HQ writes a warning. If you set a variable and its flag, the variable wins.
The two scopes do not overlap. The HQ rejects the management bearer under the mobile namespace. It rejects the mobile bearer on all other paths. If the two tokens have the same value, the HQ does not start. Relay mode needs the two tokens.
Two paths have no bearer check:
| Path | Purpose |
|---|---|
GET /health | Liveness probe |
GET /mobile/v1/health | Liveness probe for native clients |
If the bearer is missing or wrong, the HQ returns HTTP 401:
{ "code": "unauthorized", "error": "Unauthorized", "retryable": false }Set a token
Always start the HQ with DASHSQUAD_HQ_TOKEN and DASHSQUAD_HQ_CHAT_TOKEN. If you set neither
token, the HQ turns off authentication and accepts all requests. If you set only one token, the HQ rejects all requests
to the namespace that has no token.
Error shapes
Route groups use one of three error shapes. Each section tells you which shape it uses.
| Shape | Example | Used by |
|---|---|---|
| Structured | {"code":"not_found","error":"Agent not found","retryable":false} | Agents, event replay, sub-agent runtime routes, message routing, request checks, 401 responses |
| Plain | {"error":"not found"} | Most other administrative routes |
| Speech | {"code":"unavailable","error":"...","retryable":true} | /speech/* |
Request checks
The HQ checks each request before the route handler gets it. These checks apply to all routes:
| Status | Body | When |
|---|---|---|
400 | {"code":"validation_failed","error":"Invalid request target","retryable":false} | The request path has a bad format. The HQ does this check before authentication |
503 | {"code":"gateway_offline","error":"Gateway is shutting down","retryable":true} | The HQ is in shutdown and the method is not GET, HEAD or OPTIONS |
409 | {"code":"validation_failed","error":"Agent is pending deletion","retryable":false} | A write names an agent that the HQ has started to delete |
The management API sets no global body-size limit. The speech routes have their own limits. Administrative routes send no CORS headers. CORS applies only to the mobile namespace. See the HQ Mobile API.
Route index
| Method | Path | Mobile | Purpose |
|---|---|---|---|
GET | /health | Yes | HQ status and capabilities (no auth) |
GET | /info | No | Agent summary and supported API versions |
GET | /runtime/status | No | Snapshot of execution, pool, channels and relay |
POST | /lifecycle/shutdown | No | Stop the HQ cleanly |
GET | /identity | Yes | HQ ID and public key |
GET | /agents | Yes | List agents |
POST | /agents | Yes | Create an agent |
GET | /agents/:id | Yes | Get one agent |
PUT | /agents/:id | Yes | Update an agent |
DELETE | /agents/:id | Yes | Delete an agent |
POST | /agents/:id/disable | Yes | Disable an agent |
POST | /agents/:id/enable | Yes | Enable an agent |
POST | /agents/:agentId/conversation-title | No | Make a conversation title |
GET | /agents/:id/memory | Yes | List the memories of an agent |
GET | /agents/:id/memory/:name | Yes | Read one memory |
PUT | /agents/:id/memory/:name | No | Create or replace a memory |
DELETE | /agents/:id/memory/:name | Yes | Delete a memory |
GET | /agents/:id/memory/config | No | Read memory settings |
PATCH | /agents/:id/memory/config | No | Change memory settings |
GET | /agents/:id/skills | Yes (read-only view) | List the skills of an agent |
POST | /agents/:id/skills | No | Create a skill |
GET | /agents/:id/skills/:name | No | Read one skill |
PUT | /agents/:id/skills/:name | No | Replace the body of a skill |
DELETE | /agents/:id/skills/:name | No | Remove a skill |
POST | /agents/:id/skills/install | No | Install a skill from a source |
GET | /agents/:id/skills/config | No | Read skill settings |
PATCH | /agents/:id/skills/config | No | Change skill settings |
GET | /agents/:id/skills/:name/lessons | No | Read the lessons of a learned skill |
DELETE | /agents/:id/skills/:name/lessons/:lessonId | No | Retire one lesson |
GET | /agents/:id/subagent-types | Yes | Resolved sub-agent roster |
GET | /agents/:id/subagent-definitions | Yes | List the definition files of an agent |
GET | /agents/:id/subagent-definitions/:name | Yes | Read one definition |
PUT | /agents/:id/subagent-definitions/:name | Yes | Write one definition |
DELETE | /agents/:id/subagent-definitions/:name | Yes | Delete one definition |
GET | /conversations/:id/subagents | Yes | List the sub-agents of a conversation |
POST | /subagents/:id/stop | Yes | Stop a sub-agent |
POST | /subagents/:id/resume | Yes | Send a follow-up message to a sub-agent |
GET | /agents/:id/swarm/runs | No | List swarm runs |
GET | /agents/:id/swarm/runs/:runId | No | Get one swarm run |
POST | /agents/:id/swarm/runs/:runId/workers/:workerId/cancel | No | Cancel one worker |
POST | /agents/:id/swarm/runs/:runId/workers/:workerId/send | No | Send a message to one worker |
POST | /agents/:id/conversations/:conversationId/swarm/cancel | No | Cancel the swarm turn of a conversation |
GET | /browser/status | No | Browser status and install progress |
POST | /browser/install | No | Start the download of the managed browser |
GET | /agents/:id/browser/profiles | No | List the browser profiles of an agent |
DELETE | /agents/:id/browser/profiles/:name | No | Delete a browser profile |
GET | /channels | No | List channels |
POST | /channels | No | Create a channel |
GET | /channels/:name | No | Get one channel |
PUT | /channels/:name | No | Update a channel |
DELETE | /channels/:name | No | Remove a channel |
GET | /credentials | No | List credential keys |
POST | /credentials | No | Set a credential |
DELETE | /credentials/:key | No | Delete a credential |
POST | /credentials/provider-oauth | No | Save a complete provider subscription credential |
PUT | /credentials/provider/:provider/:name | No | Replace a provider credential with an API key |
DELETE | /credentials/provider/:provider/:name | No | Remove a provider credential and its refresh data |
GET | /models | Yes (read-only view) | List available models |
POST | /models/refresh | No | Get a new model list now |
GET | /speech/config | Yes | Speech settings and provider status |
PATCH | /speech/config | Yes | Change speech settings |
GET | /speech/models | Yes | List speech models |
POST | /speech/transcriptions | Yes | Transcribe a clip |
POST | /speech/speech | Yes | Make speech from text |
GET | /message-routing/config | No | Read the Steer and Follow Up routing settings |
PATCH | /message-routing/config | No | Change the Steer and Follow Up routing settings |
GET | /runtime/mcp/servers | No | List MCP servers |
POST | /runtime/mcp/servers | No | Add an MCP server |
GET | /runtime/mcp/servers/:name | No | Get one MCP server |
DELETE | /runtime/mcp/servers/:name | No | Remove an MCP server |
POST | /runtime/mcp/servers/:name/reconnect | No | Connect an MCP server again |
POST | /runtime/mcp/servers/:name/reauthorize | No | Start the sign-in of an MCP server again |
POST | /runtime/mcp/servers/:name/cancel-authorization | No | Cancel a pending connector authorization |
GET | /runtime/mcp/allowlist | No | Read the MCP URL allowlist |
PUT | /runtime/mcp/allowlist | No | Replace the MCP URL allowlist |
GET | /plugins | No | List plugins |
POST | /plugins/install | No | Install a plugin |
PUT | /plugins/:name | No | Enable, disable, trust or untrust a plugin |
DELETE | /plugins/:name | No | Remove a plugin |
POST | /plugins/reload | No | Find plugins again |
GET | /runtime/plugins | No | Plugin providers and names for pickers |
GET | /events | Yes | Server-sent event stream |
GET | /agents/:agentId/conversations/:conversationId/events | Yes | Replay the durable events of a conversation |
GET, PATCH | /agents/:id/autonomy | Yes | Daily limit and unattended usage of an agent |
GET | /conversations/:id/goal | Yes | The latest goal of a conversation |
POST | /conversations/:id/goal/commands | Yes | Create, pause, resume, clear or change a goal |
GET | /agents/:id/goals | Yes | The goals of an agent |
GET, POST | /agents/:id/routines | Yes | List routines and wake-ups, or create a routine |
GET, PATCH, DELETE | /routines/:routineId | Yes | Read, change or delete one routine |
POST | /routines/:routineId/run | Yes | Start one run of a routine now |
GET | /routines/:routineId/runs | Yes | The run history of a routine |
GET | /agents/:id/threads | Yes | Squad message threads of an agent |
GET | /threads/:threadId | Yes | One thread and its messages |
POST | /threads/:threadId/pause, /resume, /close | Yes | Pause, resume or close a thread |
GET | /needs-you | Yes | Items that wait for the user, for all agents |
The routes from /agents/:id/autonomy to /needs-you exist only while /health lists
long-horizon-v1. The HQ Mobile API page documents them.
Both listeners use the same handlers and the same error shape.
The HQ also serves conversation routes on this listener with the management bearer. The HQ Mobile API documents them. See Conversations and chat.
Health and status
GET /health
Returns the HQ status, the mobile contract version and the runtime capabilities. This route needs no
authentication. Also under /mobile/v1, as GET /mobile/v1/health.
Only loopback /health adds version, the HQ software version captured at startup.
/mobile/v1/health keeps its existing fields and does not include version.
The software version is separate from apiVersion and contractVersion.
curl http://127.0.0.1:9300/health{
"status": "healthy",
"startedAt": "2026-07-12T00:00:00.000Z",
"pid": 12345,
"agents": 2,
"channels": 1,
"apiVersion": 1,
"contractVersion": 1,
"version": "0.9.0",
"capabilities": [
"providers-v1",
"speech-v1",
"long-horizon-v1",
"provider-recovery-v1",
"chat-archive-v1",
"cancel-step-v1",
"agent-rename-v1",
"chat-tools-v1"
]
}| Field | Type | Description |
|---|---|---|
status | "healthy" | Always "healthy" while the HQ serves requests |
startedAt | string | ISO timestamp of the start of this HQ process |
pid | number | HQ process ID |
agents | number | Number of registered agents |
channels | number | Number of configured channels |
apiVersion | 1 | The same on /health and /mobile/v1/health |
contractVersion | 1 | The mobile contract version. An older HQ does not send this field |
version | string | HQ software version at startup; loopback /health only |
capabilities | string[] | Features that depend on the HQ's runtime state |
Capabilities:
| Capability | When present |
|---|---|
providers-v1 | When the HQ loads plugin providers, so the mobile /providers routes exist |
speech-v1 | When a speech provider can transcribe and speak. If the provider check fails, the HQ omits this value. The request does not fail |
long-horizon-v1 | When long-horizon work is on. It is on by default. The goal, routine, thread, autonomy and Needs you routes exist only with this value |
provider-recovery-v1 | Always on a current HQ. The HQ attributes provider failures (providerFailure) and accepts retries (retryOf) |
chat-archive-v1 | Always on a current HQ. The archive and restore routes for one chat exist |
cancel-step-v1 | Always on a current HQ. The chat WebSocket accepts cancel_step |
agent-rename-v1 | Always on a current HQ. PUT /agents/:id accepts name |
chat-tools-v1 | Always on a current HQ. Agents can rename their chats and start new chats |
Ignore capability values that you do not know. Some values also change the shape of responses. The HQ Mobile API page tells how a client declares the capabilities that it can read.
GET /info
Returns a short summary of the agents. Loopback only.
{
"agents": [
{ "name": "assistant", "model": "anthropic/claude-sonnet-4-20250514", "tools": ["bash"] }
]
}GET /runtime/status
Returns a snapshot of the current HQ state. Loopback only.
curl http://127.0.0.1:9300/runtime/status \
-H "Authorization: Bearer $DASH_TOKEN"{
"execution": {
"accepting": true,
"activeCanonicalTurns": 2,
"activeLegacyTurns": 1,
"quiescingAgents": 0
},
"pool": { "size": 3, "maxSize": 64, "pinned": 3, "agents": { "a1": 3 } },
"channels": [{ "name": "telegram", "health": "connected" }],
"relay": { "connection": "disabled", "activeStreams": 0 }
}| Field | Description |
|---|---|
execution.accepting | true when the HQ accepts new work |
execution.activeCanonicalTurns | Active conversation turns. This count includes turns that still clean up after a cancel |
execution.activeLegacyTurns | Active channel turns or legacy turns. This count includes cleanup after a cancel |
execution.quiescingAgents | Number of agents that cannot start new turns now |
pool | Current runtime count, capacity, pinned runtime count, and counts by agent ID |
channels | Name and health of each active channel: connected, connecting, disconnected or needs_reauth |
relay.connection | disabled, connecting, connected, disconnected or stopped |
relay.activeStreams | Number of streams that the relay client tracks now |
Use GET /health as the liveness check. The status route reports the process state only. It
does not test the providers, and it does not tell you that queued work is complete. It contains
no credentials, conversation text or queued message text.
POST /lifecycle/shutdown
Stops the HQ cleanly. Loopback only. Desktop calls this route before it sends a stop signal to the process. The request needs no body.
The HQ sends the reply first. Then it closes its servers and stops:
{ "ok": true }If the HQ cannot start the shutdown, it returns 500 with the structured gateway_offline error.
GET /identity
Returns the stable ID and the Ed25519 public key of the HQ. Also under /mobile/v1, where native clients call it with the mobile bearer.
{
"gatewayId": "gateway-01",
"publicKey": "<base64-public-key>"
}The public key identifies the HQ. You can share it safely. The private key stays on the HQ machine.
Agents
Create and manage agents. The HQ saves agents in ~/.dash/gateway/agents.json. All agent routes
are also under /mobile/v1. The mobile create and update routes accept
fewer fields (see below).
Agent routes use the structured error shape. Responses never include providerApiKeys.
| Status | code | When |
|---|---|---|
400 | validation_failed | The body is not valid JSON, or has an unknown or incorrect field. A name that is not valid also gives details (see PUT /agents/:id) |
404 | not_found | No agent has this ID |
409 | validation_failed | The HQ has started to delete the agent. Or the name is in use: details is { "field": "name", "reason": "taken" } |
500 | gateway_offline | Unexpected HQ error, "Internal HQ error" (retryable: true) |
Agent object
{
"id": "3f9c2a1b",
"name": "assistant",
"config": {
"name": "assistant",
"model": "anthropic/claude-sonnet-4-20250514",
"systemPrompt": "You are a helpful assistant.",
"tools": ["bash", "browser"],
"workspace": "/Users/you/.dash/workspaces/3f9c2a1b"
},
"status": "registered",
"registeredAt": "2026-07-12T00:00:00.000Z",
"effortLevels": ["low", "medium", "high"]
}status is registered, active or disabled. config holds each configuration field that you
set, but not providerApiKeys. effortLevels lists the effort levels that the current model
supports. It is empty when the model does not think, or when no level reaches
the provider. See
Extended thinking.
Configuration fields
| Field | Type | Notes |
|---|---|---|
name | string | Necessary on create. Not blank, and unique. The HQ compares names without case. See PUT /agents/:id to rename an agent |
model | string | Necessary on create. Not blank, in the form provider/model |
systemPrompt | string | Necessary on create. Can be empty |
fallbackModels | string[] | Strings that are not blank |
tools | string[] | Tool names that are not blank. See Tools |
skills | object | Only paths, urls, learning (auto, on, off), minToolCalls (integer, 0 or more) |
providerApiKeys | object | Provider ID to key string. The HQ stores it and never returns it |
workspace | string | If you omit it or send an empty string, the HQ sets a default workspace |
maxTokens | integer | 1 or more |
mcpServers | string[] | Names of the MCP servers that this agent can use |
swarm | object | Only enabled, maxConcurrentWorkers, maxWorkersPerRun, maxSteersPerWorker, maxRunSeconds, allowedModels |
subagents | object | Only enabled, delegation (auto or explicit), allowedTypes, allowedModels, maxConcurrent, maxPerTurn, maxRunSeconds, maxDepth, modelAliases. See Configuration |
plugins | string[] or null | Plugins that this agent can use. null clears the list |
providers | string[] or null | Providers that this agent can use. null clears the list |
messaging | object | Only send and receive (booleans): the agent can send messages to other agents, and can get messages from them. If the block is absent, both are on. On update, a switch that you omit keeps its value |
The HQ returns 400 for all other keys. Numeric limits must be positive integers. Two limits,
swarm.maxSteersPerWorker and subagents.maxDepth, also accept 0. modelAliases maps a short
alias (no /) to a model ID. Memory settings have their own routes. See Memory.
GET /agents
Lists all agents as an array of agent objects.
curl http://127.0.0.1:9300/agents \
-H "Authorization: Bearer $DASH_TOKEN"POST /agents
Creates an agent and returns it with HTTP 201. name, model and systemPrompt are necessary.
The mobile namespace accepts only these three fields.
curl -X POST http://127.0.0.1:9300/agents \
-H "Authorization: Bearer $DASH_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "assistant",
"model": "anthropic/claude-sonnet-4-20250514",
"systemPrompt": "You are a helpful assistant.",
"tools": ["bash"]
}'GET /agents/:id
Returns one agent object.
PUT /agents/:id
Updates the configuration of an agent and returns the updated agent. Send only the fields to
change. The body must not be empty. The mobile namespace accepts only name, model and
systemPrompt. It also accepts messaging while /health lists long-horizon-v1.
Send name to rename the agent. The agent keeps its ID and its folders. The HQ removes spaces at
the start and end of the name. Then it checks these rules:
| Status | details.reason | When |
|---|---|---|
400 | empty | The name is empty |
400 | too_long | The name has more than 64 characters. details.maxLength is 64 |
400 | invalid | The name is not a string, or has a line break or a control character |
409 | taken | Another agent has this name. The HQ compares names without case |
Each of these errors has code: "validation_failed" and details.field: "name". If the HQ refuses
the name, it changes no other field. After a rename, the HQ adds an agent_renamed notice to the
newest chat of the agent, if it has one.
curl -X PUT http://127.0.0.1:9300/agents/3f9c2a1b \
-H "Authorization: Bearer $DASH_TOKEN" \
-H "Content-Type: application/json" \
-d '{"model": "anthropic/claude-opus-4-5"}'A change to swarm or subagents restarts the warm runtimes of the agent. Thus, the next turn
uses the new settings. The HQ reads other changes on the next turn.
DELETE /agents/:id
Deletes an agent and returns {"ok": true}. The HQ cancels the active turns and swarm runs of the
agent. It removes the agent from channel routing and deletes its browser sessions. The
HQ archives the conversations of the agent and does not delete them. You can still read their
messages through the conversation routes.
POST /agents/:id/disable
Disables an agent but does not delete it, and returns {"ok": true}. The HQ stops new turns for
the agent. It cancels the active turns and swarm runs of the agent and releases its runtimes.
POST /agents/:id/enable
Enables a disabled agent again and returns {"ok": true}.
POST /agents/:agentId/conversation-title
Makes a short title for a conversation from its first message. Loopback only. The HQ makes the title with one small completion on the model of the agent.
| Field | Type | Description |
|---|---|---|
text | string | Necessary. The message to make a title for. The HQ removes spaces at the start and end |
{ "title": "Release planning" }Errors use the plain shape:
| Status | When |
|---|---|
400 | Invalid JSON, or text is missing or empty |
404 | Unknown agent ({"error":"agent not found"}) |
502 | The completion failed |
Memory
Read and edit the facts that an agent keeps from one conversation to the next. See
Memory. The list, read and delete routes are also under
/mobile/v1. The write route and the settings routes are loopback only.
Memory routes use the plain error shape, also in the mobile namespace:
| Status | When |
|---|---|
400 | Invalid JSON, a body that is not an object, or an incorrect name, description, type or content |
404 | Unknown agent or memory |
409 | The agent has 200 memories, and this write adds one more |
503 | Memory is off for this agent (writes only), or the HQ has no memory directory |
GET /agents/:id/memory
Lists the memories of an agent without their bodies. The list shows the memories on disk, also when memory is off for the agent.
[
{
"name": "preferred-editor",
"description": "User edits code in Neovim",
"type": "user",
"source": "agent",
"createdAt": "2026-07-12",
"updatedAt": "2026-07-12",
"size": 58
}
]type is user, feedback, project or reference. source tells you which writer made the
memory: agent, sweep, user or import. size is the body length in characters.
GET /agents/:id/memory/:name
Returns one memory with its content. It has the same fields as the list, but not size.
PUT /agents/:id/memory/:name
Creates or replaces a memory. The name comes from the path. The HQ records memories from this
route with source: "user".
| Field | Type | Rules |
|---|---|---|
description | string | One line, 1 to 200 characters |
type | string | user, feedback, project or reference |
content | string | 1 to 2,048 characters |
The name has lowercase letters, digits and hyphens. It starts with a letter or digit and has a maximum of 64 characters.
curl -X PUT http://127.0.0.1:9300/agents/3f9c2a1b/memory/preferred-editor \
-H "Authorization: Bearer $DASH_TOKEN" \
-H "Content-Type: application/json" \
-d '{"description": "User edits code in Neovim", "type": "user", "content": "Prefers Neovim."}'{
"record": {
"name": "preferred-editor",
"description": "User edits code in Neovim",
"type": "user",
"source": "user",
"createdAt": "2026-07-12",
"updatedAt": "2026-07-12",
"content": "Prefers Neovim."
},
"action": "created"
}action is created or updated.
DELETE /agents/:id/memory/:name
Deletes a memory and returns its name. This route also works when memory is off.
{ "name": "preferred-editor" }GET /agents/:id/memory/config
Returns the memory settings of the agent. If an agent has no memory settings, the route returns
enabled: true with sweep: "auto".
{ "enabled": true, "sweep": "auto" }PATCH /agents/:id/memory/config
Changes memory settings and returns the merged result. Fields that you omit keep their current values.
| Field | Type | Description |
|---|---|---|
enabled | boolean | false turns off the memory prompt, the memory tools and the review after each turn |
sweep | string | auto, on or off. Controls the review after each turn that saves memories |
A change to enabled restarts the warm runtimes of the agent. Thus, the next turn gets or loses
the memory tools. A change to sweep applies on the next turn.
Skills
Manage the skills that an agent can load. See Skills. Only GET /agents/:id/skills is
also under /mobile/v1, as a read-only view. All other skill routes are loopback only.
Skill routes use the plain error shape:
| Status | When |
|---|---|
400 | Invalid JSON, an unknown settings key, or an incorrect learning or minToolCalls value |
404 | Unknown agent, skill or lesson |
409 | A skill with that name exists |
422 | The skill is not valid, comes from a plugin, failed its security scan, or is dangerous |
GET /agents/:id/skills
Lists all skills that the agent can use, from plugins and from the agent.
[
{
"name": "release-notes",
"description": "Write release notes from merged PRs",
"trigger": "when asked for release notes",
"location": "/Users/you/.dash/gateway/skills/assistant/release-notes/SKILL.md",
"content": "...",
"editable": true,
"source": "managed"
}
]source is managed, agent (the agent learned it), 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.
| Field | Type | Description |
|---|---|---|
name | string | Skill name |
description | string | One-line description that the agent sees |
content | string | The instructions of the skill |
{ "name": "release-notes", "location": "/Users/you/.dash/gateway/skills/assistant/release-notes/SKILL.md" }GET /agents/:id/skills/:name
Returns one skill with the same fields as the list.
PUT /agents/:id/skills/:name
Replaces the body of a skill and keeps its frontmatter. Send {"content": "..."}. Returns the
name and location of the skill.
DELETE /agents/:id/skills/:name
Removes a skill and returns {"name": "..."}. You cannot remove plugin skills with this route.
POST /agents/:id/skills/install
Installs a skill from a git repository, a URL or a local path. The HQ scans the skill before it installs it. If the scan fails, the HQ does not install the skill.
| Field | Type | Description |
|---|---|---|
source | string | Necessary. The location of the skill |
name | string | Optional. The name for the installed skill |
{
"name": "release-notes",
"location": "/Users/you/.dash/gateway/skills/assistant/release-notes/SKILL.md",
"verdict": { "verdict": "safe", "reasons": [] }
}verdict.verdict is safe or suspicious. The HQ rejects a dangerous skill with 422.
GET /agents/:id/skills/config
Returns the skills configuration of the agent, or {} if it has none.
{ "paths": ["/opt/skills"], "learning": "auto", "minToolCalls": 3 }PATCH /agents/:id/skills/config
Merges fields into the skills configuration of the agent and returns the result. The HQ
returns 400 for other keys.
| Field | Type | Description |
|---|---|---|
paths | string[] | More local skill directories |
urls | string[] | More remote skill sources |
learning | string | auto, on or off. Controls if the agent records lessons after real work |
minToolCalls | integer | The number of tool calls that a turn needs before the HQ reviews it for lessons. 0 or more |
GET /agents/:id/skills/:name/lessons
Returns the lesson book of a skill that the agent learned. If the skill has no lesson book, the
route returns 404.
{
"version": 1,
"skill": "deploy-checks",
"description": "Checks to run before deploying",
"augments": [],
"bullets": [
{
"id": "l1",
"text": "Run the migration dry-run before deploying.",
"helpful": 2,
"harmful": 0,
"createdAt": "2026-07-12T00:00:00.000Z",
"lastTouchedAt": "2026-07-14T00:00:00.000Z"
}
],
"retired": []
}augments gives the names of other skills that this book goes with. retired holds the lessons
that the HQ removed from the active list. Each retired lesson has 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. The HQ
does not delete it, and a later review does not propose it again.
Sub-agents
Manage the helper agents that an agent can start, and the sub-agents that are active now. All
routes in this section are also under /mobile/v1.
Definitions. The five definition routes read and write the sub-agent types of one agent. On
the loopback API, they use the plain error shape. Under /mobile/v1, they use the structured
shape. A definition :name has lowercase letters, digits and hyphens, and starts with a letter or
digit. The HQ returns 400 for all other names, and 404 for an unknown agent.
GET /agents/:id/subagent-types
Returns the resolved roster of the agent: built-in types, plus workspace, plugin and per-agent definitions.
{
"types": [
{
"name": "reviewer",
"description": "Reviews a diff for correctness",
"source": "agent",
"tools": ["read", "grep"],
"model": "anthropic/claude-sonnet-4-20250514",
"background": false
}
],
"unknownAllowedTypes": ["reveiwer"]
}Each type can also have location, disallowedTools, skills, maxTurns, isolation
(worktree) and shadowedBy. The roster does not include the system prompt. Read it from the
definition route. unknownAllowedTypes lists the names in subagents.allowedTypes that match no
type. Usually, the name is not correct.
GET /agents/:id/subagent-definitions
Lists the definition files in the writable directory of the agent. Workspace and plugin definitions show only in the roster.
{ "definitions": [{ "name": "reviewer", "file": "reviewer.md" }] }GET /agents/:id/subagent-definitions/:name
Returns the raw Markdown of one definition.
{ "name": "reviewer", "raw": "---\nname: reviewer\ndescription: Reviews a diff\n---\nYou review diffs." }PUT /agents/:id/subagent-definitions/:name
Writes a definition and returns {"ok": true, "name": "reviewer"}. Send {"raw": "..."} with the
full Markdown file and its frontmatter.
| Status | When |
|---|---|
400 | Invalid JSON, empty raw, or raw larger than 256 KiB |
422 | The HQ cannot parse the definition, or the frontmatter name is not the same as :name |
The HQ validates the definition before it writes it. The agent uses the change on its next model turn.
DELETE /agents/:id/subagent-definitions/:name
Deletes a definition file and returns {"ok": true, "name": "reviewer"}. If the file does not
exist, the route returns 404.
Active sub-agents. The conversation, stop and resume routes use the structured error shape on the two mounts.
GET /conversations/:id/subagents
Lists the sub-agents that a conversation started. The HQ reads the list from saved state. Thus, the list is correct after an HQ restart.
{
"subagents": [
{
"id": "sub_01J...",
"name": "reviewer-1",
"type": "reviewer",
"description": "Review the auth diff",
"status": "done",
"background": true,
"depth": 1,
"startedAt": "2026-07-12T00:00:00.000Z",
"endedAt": "2026-07-12T00:02:10.000Z",
"usage": { "inputTokens": 12000, "outputTokens": 900 },
"toolCallCount": 7,
"report": "No blocking issues found.",
"oneShot": false
}
]
}status is running, waiting_input, done, failed, cancelled, interrupted or
max_turns. The HQ scans report before it returns it. You cannot resume a oneShot type, for
example Explore or Plan. For an unknown conversation, the route returns 404 not_found.
POST /subagents/:id/stop
Stops a sub-agent and all the sub-agents that it started. The request needs no body.
{ "ok": true, "status": "cancelled" }If :id is not a sub-agent, the route returns 404 not_found. If the sub-agent is already
complete, it returns 409.
POST /subagents/:id/resume
Sends a follow-up message to a sub-agent. The result is the same as when its parent uses the
send_message tool.
| Field | Type | Description |
|---|---|---|
message | string | Necessary. Not blank |
requestId | string | Optional correlation ID, a maximum of 256 characters. The HQ echoes it on the accepted chat frame of the turn that it starts |
{ "ok": true, "status": "running", "mode": "resumed" }mode is resumed when the message starts a new turn. It is queued when the HQ gives the
message to an active sub-agent. The route returns 400 for an incorrect body and 404 for an
unknown sub-agent. It returns 409 when the sub-agent cannot accept the message. Causes are a
one-shot type, no steer messages left, or no parent conversation.
Swarm panel
Examine and control the swarm runs of an agent. A run is the set of sub-agents that one parent
turn started. All routes here are loopback only and use the plain error shape. For an unknown
agent or run, the routes return 404.
GET /agents/:id/swarm/runs
Lists the recent runs of the agent. The list includes runs from before the last HQ restart.
{
"runs": [
{
"runId": "run_01J...",
"agentId": "3f9c2a1b",
"conversationId": "018f0f4a-...",
"startedAt": 1752278400000,
"endedAt": 1752278530000,
"finalized": true,
"workerCount": 2,
"activeCount": 0
}
]
}startedAt and endedAt are epoch milliseconds.
GET /agents/:id/swarm/runs/:runId
Returns one run with the same fields, plus a workers array with a snapshot of each worker.
POST /agents/:id/swarm/runs/:runId/workers/:workerId/cancel
Cancels one worker. Returns {"ok": true}. If the HQ cannot cancel the worker, it returns 409
with {"ok": false, "reason": "..."}. This route also works for a background worker after the
turn that started it is complete.
POST /agents/:id/swarm/runs/:runId/workers/:workerId/send
Sends a message to one worker. Send {"message": "..."}. For an empty message, the route returns
400. Returns {"ok": true}. If the worker cannot accept the message, the route returns 409
with {"ok": false, "reason": "..."}.
POST /agents/:id/conversations/:conversationId/swarm/cancel
Cancels all workers that are not complete in the live swarm turn of the conversation. Workers continue after the chat stream stops. Thus, the stop button uses this route to stop them.
{ "cancelled": true }cancelled is false when there was no live turn. You can call the route again safely.
Browser
Manage the browser that agents use, and the saved logins in their browser profiles. See Browser. All routes here are loopback only.
Browser routes use the plain error shape. For an incorrect profile name, they return 400. For a
profile that an open browser uses, they return 409. For an unknown agent, they return 404
with {"error":"Agent not found"}.
GET /browser/status
Returns the browser that the HQ starts, the progress of the managed download, and the number of live browser sessions.
| Query | Description |
|---|---|
refresh=1 | Look for a browser again and do not use the cached result. With all other values, the route only reads the status |
{
"source": "chrome",
"path": "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
"install": { "state": "idle", "done": 0, "total": 0 },
"liveSessions": 1,
"sizes": { "downloadMb": 190, "diskMb": 370 }
}| Field | Description |
|---|---|
source | config, chrome, edge, managed, or null when the HQ found no browser that works |
path | The browser executable, when the HQ found one |
managedBuildId | The installed managed build, if there is one |
unverifiedPath, unverifiedSource | A browser on disk that did not start in time. Try again. Do not download a browser |
install.state | idle, downloading, verifying, done or failed. Byte counts are in done and total. error is optional |
liveSessions | The number of open browser sessions |
sizes | Approximate download size and disk size of the managed browser, in MB |
POST /browser/install
Starts the download of the managed browser. The route returns HTTP 202 immediately with the
current progress. To follow the progress, poll GET /browser/status. If a download is in
progress, the route returns the same 202 and does not start a second download. When a download
fails, the status shows install.state: "failed".
{ "install": { "state": "downloading", "done": 0, "total": 199229440 } }GET /agents/:id/browser/profiles
Lists the saved browser profiles of the agent.
{
"profiles": [
{ "name": "work", "lastUsedAt": "2026-07-12T00:00:00.000Z", "sizeBytes": 41943040, "inUse": false }
]
}DELETE /agents/:id/browser/profiles/:name
Deletes a saved profile with its cookies and logins, and returns {"name": "work"}. While an open
browser uses the profile, the route returns 409.
Channels
Connect and manage channels for Telegram and WhatsApp. See the
Telegram and WhatsApp page. The HQ saves channels in ~/.dash/gateway/channels.json. All
routes here are loopback only and use the plain error shape.
| Status | When |
|---|---|
400 | Invalid JSON, a missing necessary field, a field of the wrong type, or routing that names an unknown agent |
404 | Unknown channel |
409 | A channel with that name exists |
Channel object
{
"name": "family-telegram",
"adapter": "telegram",
"globalDenyList": [],
"allowedUsers": ["123456789"],
"routing": [
{
"condition": { "type": "default" },
"agentId": "3f9c2a1b",
"allowList": [],
"denyList": []
}
],
"registeredAt": "2026-07-12T00:00:00.000Z"
}adapter is telegram or whatsapp. A routing condition is {"type": "default"},
{"type": "sender", "ids": [...]} or {"type": "group", "ids": [...]}.
GET /channels
Lists all channels as an array of channel objects.
POST /channels
Creates a channel, starts its adapter, and returns HTTP 201 with {"ok": true}.
| Field | Type | Description |
|---|---|---|
name | string | Necessary. Unique channel name |
adapter | string | Necessary. telegram or whatsapp |
routing | array | Necessary. Routing rules. Each agentId must exist |
globalDenyList | string[] | Optional. The HQ ignores these senders on all routes |
allowedUsers | string[] | Optional. The senders that can use the channel |
Store the credential of the channel first with POST /credentials. For Telegram, store the bot
token under channel:<name>:token. For WhatsApp, store the auth JSON under
channel:<name>:whatsapp-auth. If the credential is missing, the route returns 400.
curl -X POST http://127.0.0.1:9300/channels \
-H "Authorization: Bearer $DASH_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "family-telegram",
"adapter": "telegram",
"routing": [{"condition": {"type": "default"}, "agentId": "3f9c2a1b", "allowList": [], "denyList": []}]
}'GET /channels/:name
Returns one channel object. URL-encode the name.
PUT /channels/:name
Updates a channel and returns the updated channel object. Send one or more of routing,
globalDenyList and allowedUsers. Fields that you omit do not change. Each field must be an
array.
DELETE /channels/:name
Stops the adapter of the channel, removes the channel, and returns {"ok": true}.
Credentials
Store provider API keys and channel tokens in the encrypted store of the HQ
(~/.dash/gateway/credentials.enc). See Secrets. All routes here are
loopback only and use the plain error shape.
Some key formats have a special meaning:
| Key | Meaning |
|---|---|
<provider>-api-key:<name> | A provider API key, for example anthropic-api-key:default. When you set or delete one, the HQ clears the model list and the speech status |
<provider>-key-meta:<name> | Metadata for the provider key with the same name. The HQ writes it with the date that you added the key, and deletes it with the key |
channel:<name>:token | A Telegram bot token. When you set it, the HQ restarts that channel with the new token |
GET /credentials
Lists the stored credential keys. The route never returns values.
["anthropic-api-key:default", "anthropic-key-meta:default", "channel:family-telegram:token"]POST /credentials
Sets one credential and returns HTTP 201 with {"ok": true}. key and value are necessary,
and must not be empty.
curl -X POST http://127.0.0.1:9300/credentials \
-H "Authorization: Bearer $DASH_TOKEN" \
-H "Content-Type: application/json" \
-d '{"key": "anthropic-api-key:default", "value": "sk-ant-..."}'Active agents use the new value on their next turn.
DELETE /credentials/:key
Deletes a credential and returns {"ok": true}. URL-encode the key.
For a provider API key, this also deletes its saved refresh token, expiry and metadata. If you set that key again, the HQ treats it as a pasted API key.
POST /credentials/provider-oauth
Saves a complete provider subscription credential in one update. Returns HTTP 201 with {"ok": true}.
| Field | Type | Description |
|---|---|---|
provider | string | Necessary. Nonempty provider ID, such as openai or anthropic. No colon or control characters |
name | string | Necessary. Nonempty key name, with no control characters |
access | string | Necessary. Nonblank access token, at most 8,192 characters |
refresh | string | Necessary. Nonblank refresh token, at most 8,192 characters |
expires | number | Necessary. Positive, finite Unix timestamp in milliseconds for the access-token expiry |
Do not send other fields. Invalid fields return HTTP 400 with {"error": "Invalid provider OAuth credentials"}. The HQ refreshes its provider caches after the update.
PUT /credentials/provider/:provider/:name
Replaces the provider credential with a pasted API key. URL-encode the provider ID and key name. Their rules are the same as above.
Send {"value": "<api-key>"}. The value must be nonempty, contain no whitespace and have at most 8,192 characters. Do not send other fields.
The HQ removes the saved refresh token and expiry, and resets the key metadata. Returns HTTP 201 with {"ok": true}. Invalid fields return HTTP 400 with {"error": "Invalid provider API key"}.
DELETE /credentials/provider/:provider/:name
Removes the provider key, refresh token, expiry and metadata in one update. URL-encode both path values. Returns {"ok": true}.
An invalid provider ID or key name returns HTTP 400 with {"error": "Invalid provider credential name"}.
Phones and the web app use the AI providers routes of the Mobile API. These routes manage provider API keys and the sign-in with a Claude or ChatGPT subscription.
Models
List the models that 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 gets the model list of each configured provider and filters it through the bundled
provider catalogs. It keeps the result for six hours. If a provider fails, the HQ tries it again
after five minutes. Until then, it keeps the last good models of that provider. When you set or
delete a provider API key, the HQ clears the stored list. If no provider key exists, the response
lists the built-in models of each catalog with source: "bootstrap".
GET /models
curl http://127.0.0.1:9300/models \
-H "Authorization: Bearer $DASH_TOKEN"{
"models": [
{ "value": "anthropic/claude-opus-4-5", "label": "Claude Opus 4.5", "provider": "anthropic" }
],
"source": "live",
"errors": {},
"fetchedAt": "2026-07-12T00:00:00.000Z",
"supportedModelsReviewedAt": "2026-07-01"
}| Field | Description |
|---|---|
models[].value | The provider/model ID for the model field of an agent |
source | live after the HQ gets the lists from the providers. bootstrap when the HQ has no provider key |
errors | Provider ID to error text, for the providers that failed |
fetchedAt | The time when the HQ got the list |
supportedModelsReviewedAt | Review date of the newest provider catalog |
On the loopback API, ?debug=true adds bootstrap (the built-in models of the catalogs),
patterns, providersConfigured and providersAvailable. In the mobile namespace, all query
parameters cause 400 with the structured validation_failed error.
POST /models/refresh
Gets the model list again without the stored result, and returns the same shape as GET /models.
Speech
Dictation and read-aloud. All speech routes are also under /mobile/v1, with the same
handler. Hands-free voice mode uses the chat WebSocket. See
HQ Chat WebSocket.
Speech errors use the structured error shape:
| Status | code | Meaning |
|---|---|---|
400 | validation_failed | Malformed request |
400 | invalid | The provider rejected the request as not valid |
401 | unauthorized | The provider rejected its API key. This is not an HQ bearer failure |
413 | too_large | The body or the audio is larger than a size limit |
413 | too_long | The text has more than 4,000 characters |
500 | configuration_unavailable | The HQ cannot safely read or preserve its speech settings (retryable: false) |
502 | provider | The provider returned an error |
502 | network | The HQ could not connect to the provider (retryable: true) |
503 | unavailable | The HQ has no speech provider (retryable: true) |
For GET /speech/config and PATCH /speech/config, this configuration error identifies a problem with speech.json.
Repair the file or its permissions before you retry. See Configuration recovery.
GET /speech/config
Returns the current speech configuration and the status of each provider.
{
"config": {
"stt": { "provider": "openrouter", "model": "openai/whisper-large-v3" },
"tts": { "provider": "openrouter", "model": "minimax/speech-2.8-turbo", "voice": "English_expressive_narrator" },
"realtime": { "provider": null }
},
"providers": [
{
"id": "openrouter",
"capabilities": { "transcription": true, "speech": true, "realtime": false },
"available": true
},
{
"id": "realtime",
"capabilities": { "transcription": false, "speech": false, "realtime": true },
"available": false,
"reason": "no_provider_offers_realtime"
}
]
}A provider that is not available has a reason: no_credential or
no_provider_offers_realtime. By default, stt.language is the language of the HQ computer, for example en. It is absent after you set it to null (automatic detection). tts.speed
is present only when you set it.
PATCH /speech/config
Merges a partial configuration, saves it, and returns the same shape as GET /speech/config.
Keys that you omit keep their current values. For an unknown key in the body, the route returns
400.
| Field | Rules |
|---|---|
stt.provider, stt.model | Not empty, a maximum of 200 characters |
stt.language | 2 to 8 characters, or null to go back to automatic detection |
tts.provider, tts.model, tts.voice | Not empty, a maximum of 200 characters |
tts.speed | Number from 0.25 to 4 |
realtime.provider | Necessary when you send realtime. A string or null |
curl -X PATCH http://127.0.0.1:9300/speech/config \
-H "Authorization: Bearer $DASH_TOKEN" \
-H "Content-Type: application/json" \
-d '{"stt": {"language": null}}'If the declared Content-Length is more than 64 KiB, the route returns 413 too_large. It does
not read the body.
GET /speech/models
Lists the models of one kind that the configured provider has.
| Query | Description |
|---|---|
kind | Necessary. transcription or speech |
{
"models": [
{ "id": "minimax/speech-2.8-turbo", "name": "MiniMax Speech 2.8 Turbo", "kind": "speech", "voices": ["English_expressive_narrator"] }
]
}POST /speech/transcriptions
Transcribes one recorded clip. Send the audio inline as base64 in JSON. The route does not accept multipart data.
| Field | Type | Description |
|---|---|---|
audio | string | Necessary. Base64 audio, not empty, a maximum of 8 MiB after decode |
format | string | Necessary. wav, m4a, mp3, flac, ogg, webm or aac |
language | string | Optional language hint |
curl -X POST http://127.0.0.1:9300/speech/transcriptions \
-H "Authorization: Bearer $DASH_TOKEN" \
-H "Content-Type: application/json" \
-d '{"audio": "<base64>", "format": "m4a"}'{ "text": "Hello from dashsquad.", "durationSeconds": 1.8 }durationSeconds is present only when the provider gives it. If the declared Content-Length is
more than 12 MiB, the route returns 413 too_large. It does not read the body.
POST /speech/speech
Makes speech from text. Send {"text": "..."} with 1 to 4,000 characters.
curl -X POST http://127.0.0.1:9300/speech/speech \
-H "Authorization: Bearer $DASH_TOKEN" \
-H "Content-Type: application/json" \
-d '{"text": "Hello from dashsquad."}' \
--output reply.mp3The success body is audio, not JSON. The HQ streams it as audio/mpeg. A model that gives only
PCM audio, for example Google Gemini TTS, returns a full audio/wav file. An error before the
first audio chunk is still JSON. Thus, accept audio/mpeg, audio/wav and application/json.
If the declared Content-Length is more than 64 KiB, the route returns 413 too_large.
Message routing
Configure the automatic Steer and Follow Up routing. When a user sends a message during an active turn, a small classifier model decides how the HQ uses the message. Both routes are loopback only and use the structured error shape.
If the HQ cannot safely read or preserve message-routing.json, these routes return 500 configuration_unavailable with retryable: false.
Repair the file or its permissions before you retry. See Configuration recovery.
GET /message-routing/config
Returns the saved settings and the mode that the HQ uses now.
{
"config": {
"mode": "auto",
"model": "typesafe/jev-1.13",
"steerMinConfidence": 0.8,
"followUpMinConfidence": 0.5,
"timeoutMs": 1500
},
"effectiveMode": "auto"
}| Field | Description |
|---|---|
config.mode | auto (the classifier decides) or ask (the client asks the user). Default auto |
config.model | The classifier model. Default typesafe/jev-1.13 |
config.steerMinConfidence | Minimum confidence for a Steer result, from 0 to 1. Default 0.8 |
config.followUpMinConfidence | Minimum confidence for a Follow Up result, from 0 to 1. Default 0.5 |
config.timeoutMs | Time limit for the classifier, in milliseconds. Default 1500 |
config.baseUrl | Optional. The classifier endpoint. If it is not set, the HQ uses OpenRouter |
effectiveMode | auto or ask. The mode that the HQ uses now |
reason | no_openrouter_key when mode is auto but the HQ has no OpenRouter key for the OpenRouter endpoint. Then effectiveMode is ask |
PATCH /message-routing/config
Merges fields into the settings, saves them, and returns the same shape as
GET /message-routing/config.
| Field | Rules |
|---|---|
mode | auto or ask |
model | Not empty |
steerMinConfidence, followUpMinConfidence | Number from 0 to 1 |
timeoutMs | Integer from 100 to 10,000 |
baseUrl | An http or https URL |
curl -X PATCH http://127.0.0.1:9300/message-routing/config \
-H "Authorization: Bearer $DASH_TOKEN" \
-H "Content-Type: application/json" \
-d '{"mode": "ask"}'For invalid JSON, a body that is not an object, an unknown key or an incorrect value, the route
returns 400 validation_failed.
MCP servers
Connect Model Context Protocol servers. Agents can then use the tools of these servers. All
routes here are loopback only and use the plain error shape. The name of a server tool is
<server>__<tool>. To give an agent a server, add the server name to the mcpServers list of the
agent.
MCP server object
{
"name": "github",
"transport": { "type": "streamable-http", "url": "https://mcp.example.com/github" },
"status": "connected",
"tools": ["github__search_issues", "github__create_issue"]
}transport is one of these:
type | Fields |
|---|---|
stdio | command, optional args |
sse | url, optional headers |
streamable-http | url, optional headers |
status is connected, disconnected, reconnecting, error, needs_reauth or awaiting_authorization.
While authorization is pending, authUrl contains the browser authorization link. An optional error field gives the authorization failure message. The tools are available after the status becomes connected.
GET /runtime/mcp/servers
Lists all saved MCP servers as an array of MCP server objects.
POST /runtime/mcp/servers
Saves and connects a new MCP server. A connected server returns HTTP 201. If browser authorization is necessary, the route returns HTTP 202 with status, authUrl and serverName.
| Field | Type | Description |
|---|---|---|
name | string | Necessary. Letters, digits, - and _, without __ |
transport | object | Necessary. See the table above |
env | object | Optional environment variables for a stdio server |
auth | object | Optional OAuth settings: type: "oauth", grantType (authorization_code or client_credentials), clientId, clientSecret, issuer, scopes. issuer is the authorization server of clientId and clientSecret. If you do not set it, the HQ sends them only to the first authorization server that accepts them |
toolTimeout | number | Optional time limit for a tool call, in milliseconds. Default 60,000. Set a higher value for tools that need longer; cancellation still stops a pending call |
maxReconnectAttempts | number | Optional. The number of times that the HQ tries to connect again. Default 10 |
curl -X POST http://127.0.0.1:9300/runtime/mcp/servers \
-H "Authorization: Bearer $DASH_TOKEN" \
-H "Content-Type: application/json" \
-d '{"name": "files", "transport": {"type": "stdio", "command": "npx", "args": ["-y", "@modelcontextprotocol/server-filesystem", "/tmp"]}}'{ "status": "connected", "serverName": "files", "tools": ["files__read_file", "files__list_directory"] }For HTTP 202, open authUrl on the computer that runs the HQ. Complete sign-in within five minutes. Poll GET /runtime/mcp/servers/:name for the result.
{ "status": "awaiting_authorization", "authUrl": "https://auth.example.com/authorize", "serverName": "files" }| Status | When |
|---|---|
400 | Invalid JSON, or name or transport is missing |
403 | The server URL is not on the allowlist |
409 | A server with that name exists, its removal is in progress, or it disconnected before the response |
500 | The connection failed, or the name is not valid |
GET /runtime/mcp/servers/:name
Returns one MCP server object.
DELETE /runtime/mcp/servers/:name
Disconnects and removes a server. The HQ also removes the server from the mcpServers list of each
agent. It also clears the saved OAuth tokens and registration for that connector. If the connector asks you to sign in after you add it again, complete that sign-in. Returns {"ok": true}.
While removal is in progress, a second removal, add, reconnect or reauthorization for the same name can return HTTP 409. Wait for removal to finish before you add a replacement.
POST /runtime/mcp/servers/:name/reconnect
Disconnects a saved server and connects it again.
{ "ok": true, "status": "connected", "serverName": "files" }If browser authorization is necessary, returns HTTP 202 with status: "awaiting_authorization", authUrl and serverName. Poll the server object for the result.
POST /runtime/mcp/servers/:name/reauthorize
Starts authorization again for a server with the status needs_reauth. If browser authorization is necessary, returns HTTP 202 with status: "awaiting_authorization", authUrl and serverName. Otherwise, returns {"ok": true, "status": "...", "serverName": "..."}. Poll the server object for the result.
For an unknown server, the route returns 404. For a server in a different state, it returns 500. A server whose removal is in progress returns 409.
POST /runtime/mcp/servers/:name/cancel-authorization
Cancels a pending HQ authorization request and closes its callback listener. Returns {"ok": true, "status": "needs_reauth"}. The saved connector configuration remains. Use the reauthorize route to start a new request.
For an unknown server, returns 404. If no authorization is pending, or removal is in progress, returns 409. Closing the Desktop dialog does not call this route.
GET /runtime/mcp/allowlist
Returns the URL patterns that remote MCP servers must match, as an array of strings. If the list
is empty, the HQ accepts all URLs. The HQ does not check stdio servers.
PUT /runtime/mcp/allowlist
Replaces the allowlist. Send a JSON array of strings. In a pattern, * matches a sequence of any
characters.
curl -X PUT http://127.0.0.1:9300/runtime/mcp/allowlist \
-H "Authorization: Bearer $DASH_TOKEN" \
-H "Content-Type: application/json" \
-d '["https://mcp.example.com/*"]'{ "ok": true, "count": 1 }Plugins
Install and manage plugins. See Plugins. All routes here are loopback only and use the plain error shape. The HQ saves each change first. Then it loads all plugins again to apply the change.
GET /plugins
Lists all plugins in name order. The list includes disabled plugins and plugins that failed.
{
"records": [
{
"name": "demo",
"displayName": "Demo",
"version": "1.0.0",
"description": "A demo plugin",
"status": "loaded",
"dir": "/Users/you/.dash/gateway/plugins/demo",
"skillDirs": ["/Users/you/.dash/gateway/plugins/demo/skills"],
"activated": ["skills"],
"noop": ["bin", "mcp"]
}
]
}status is loaded, disabled or error. activated lists the component kinds that the HQ
uses. noop lists the kinds that are present but not active, for example the code components of
a plugin that you do not trust. A built-in plugin has builtin: true. A plugin that failed has
failure with phase (discovery, manifest or route), error and failedAt.
POST /plugins/install
Gets, scans and installs a plugin, then loads all plugins again. The HQ enables the plugin but
does not trust it. Its code components stay off until you trust it with PUT /plugins/:name.
| Field | Type | Description |
|---|---|---|
source | string | Necessary. The location of the plugin |
name | string | Optional. The name for the installed plugin |
curl -X POST http://127.0.0.1:9300/plugins/install \
-H "Authorization: Bearer $DASH_TOKEN" \
-H "Content-Type: application/json" \
-d '{"source": "git:owner/repo"}'Returns HTTP 201:
{
"name": "demo",
"version": "1.0.0",
"description": "A demo plugin",
"location": "/Users/you/.dash/gateway/plugins/demo",
"scanVerdict": "safe",
"scanReasons": [],
"source": "git:owner/repo"
}If the install succeeds but the reload fails, the plugin stays installed. The HQ returns HTTP
200 with {"ok": true, "installed": {...}, "note": "...", "error": "..."}. The next reload
loads the plugin.
| Status | When |
|---|---|
400 | Invalid JSON or an empty source |
404 | The HQ did not find the source |
409 | A plugin with that name exists, or a built-in plugin has that name |
422 | The manifest is not valid, the archive is corrupt, the scan failed, or the plugin is dangerous |
PUT /plugins/:name
Enables, disables, trusts or untrusts a plugin. Then the HQ loads all plugins again and returns the updated record.
| Field | Type | Description |
|---|---|---|
enabled | boolean | true when the plugin loads |
trusted | boolean | true when its code components, for example MCP servers and executables, run |
curl -X PUT http://127.0.0.1:9300/plugins/demo \
-H "Authorization: Bearer $DASH_TOKEN" \
-H "Content-Type: application/json" \
-d '{"trusted": true}'For a field that is not a boolean, the route returns 400. For an unknown plugin, it returns
404. If the reload fails, or the plugin is missing after the reload, the route returns 409
with error, plugin and note. In that case, the HQ saved the setting, but the active plugins
did not change.
DELETE /plugins/:name
Removes the saved settings of a plugin. If you installed the plugin through this API, the HQ also deletes its directory. Then the HQ loads all plugins again.
{ "ok": true, "path": "/Users/you/.dash/gateway/plugins/demo" }path is present only when the HQ deleted a directory. For a built-in plugin, the route returns
400. Disable a built-in plugin instead. For an unknown plugin, the route returns 404. If the
reload fails after the removal, the route returns 409 with ok: true, removed: true, error
and note. The removal is complete.
POST /plugins/reload
Finds all plugins again. This route does not change settings.
{ "ok": true, "reloadedAt": "2026-07-12T00:00:00.000Z" }If the reload fails, the route returns 500 with {"error": "..."}.
GET /runtime/plugins
Returns a short view for the model and credential pickers. It lists the providers that plugins add, and all plugins that are not disabled.
{
"providers": [
{ "id": "myllm", "label": "MyLLM", "credentialPrefix": "myllm-api-key", "pluginName": "myllm-provider", "ui": {} }
],
"plugins": [{ "name": "demo", "displayName": "Demo", "version": "1.0.0" }]
}Events
GET /events
Streams HQ changes as server-sent events.
Also under /mobile/v1, with the same events. Clients use the stream to
know when to get cached data again.
curl -N http://127.0.0.1:9300/events \
-H "Authorization: Bearer $DASH_TOKEN"The name of each event is its type. Its data is the JSON event:
event: agent:config-changed
data: {"type":"agent:config-changed","agent":"assistant","fields":["model"]}
: keepaliveThe HQ sends a : keepalive comment each 30 seconds.
type | Fields |
|---|---|
agent:config-changed | agent (name), fields (["*"] on create, ["removed"] on delete) |
channel:created | channel |
channel:config-changed | channel, fields |
channel:removed | channel |
channel:restarted | channel, reason |
conversation:changed | conversationId, revision |
conversation:deleted | conversationId, revision |
mcp:server-added | server |
mcp:server-removed | server |
plugin:config-changed | plugin, fields |
plugin:installed | plugin |
plugin:removed | plugin |
plugin:reloaded | none |
swarm:run-changed | agentId, runId |
Ignore event types that you do not know.
GET /agents/:agentId/conversations/:conversationId/events
Replays the durable chat events of a conversation after a sequence number. Thus, a client can get
the events that it lost when its WebSocket disconnected. It does not have to send the prompt
again. Also under /mobile/v1. Errors use the structured shape.
| Query | Description |
|---|---|
sinceSeq | Optional integer, 0 or more. Default 0. The route returns the entries with a higher sequence number |
{
"entries": [
{
"seq": 42,
"msgId": "00000000-0000-4000-8000-000000000002",
"agentId": "3f9c2a1b",
"conversationId": "018f0f4a-...",
"timestamp": "2026-07-12T00:00:00.000Z",
"payload": { "type": "event", "event": { "type": "text_delta", "text": "Hi" } }
}
]
}For a deleted conversation, the route returns {"entries": []}. For a query parameter other than
one sinceSeq, it returns 400 validation_failed. For an unknown agent, it returns
404 not_found. It also returns 404 not_found for a conversation of a different agent. See
HQ Chat WebSocket for the event payloads and for how to resume a turn.
Conversations and chat
Other pages document conversations, messages and chat:
- HQ Mobile API documents the conversation lists, messages, titles, deletion,
archive and restore, and
POST /mobile/v1/ws-ticket. - HQ Chat WebSocket documents
/ws/chat. This includes messages, runs, answers, cancel, voice mode and the browser live view.
This listener also serves these conversation routes with the management bearer. They use the same handlers as the mobile routes:
| Method | Path | Same as |
|---|---|---|
POST, GET | /conversations | /mobile/v1/conversations |
GET, PATCH, DELETE | /conversations/:id | /mobile/v1/conversations/:id |
POST | /conversations/:id/archive | /mobile/v1/conversations/:id/archive |
POST | /conversations/:id/restore | /mobile/v1/conversations/:id/restore |
GET | /conversations/:id/messages | /mobile/v1/conversations/:id/messages |
GET | /conversations/:id/bootstrap | /mobile/v1/conversations/:id/bootstrap |
GET | /agents/:agentId/conversations/:conversationId/events | /mobile/v1/agents/:agentId/conversations/:conversationId/events |
On this listener, archive and restore return the management form of the conversation. It has
pendingCount and pendingScheduling (running or paused) in place of pendingFollowUpCount
and queuePaused.
The management listener on port 9300 and the channel server on port 9200 use different
credentials. Native clients never get the management bearer. The relay accepts only the mobile
bearer, on /mobile/v1 and /ws/chat.
Conversation decisions
These routes are loopback only. Send the management bearer. They are not mobile routes. They expose the latest recorded question card for one conversation. They do not add a decision screen to Desktop, the web app or the mobile apps.
GET /agents/:agentId/conversations/:conversationId/decision
Returns { agentId, conversationId, decision }. decision is null if no decision is recorded.
A decision has these fields:
| Field | Description |
|---|---|
id, revision | Decision identity and current revision |
source | Original runId, segmentTurnId, toolCallId and event seq |
questions | The recorded question card questions and options |
state | pending or answered |
answers | Recorded answers, when present |
continuation | Follow Up inputId, runId and current state, when present |
createdAt, updatedAt | ISO timestamps |
The route accepts no query parameters. Read it again before you answer a changed decision.
POST /agents/:agentId/conversations/:conversationId/decision/answer
Send the decision ID and revision from the read response. Use a new commandId for a new command.
{
"commandId": "answer-01",
"decisionId": "<decision.id>",
"expectedRevision": 12,
"answers": [
{ "question": "Which format?", "selected": ["Markdown"] }
]
}Answer every recorded question once. Copy each question and selected option label exactly.
For a single-select question, select at most one option.
To give a free-text answer, use other with nonblank text.
Each answer needs a selected option or nonblank other text.
The HQ records the answer and submits one Follow Up. A response contains commandId, replayed
and decision. Here, decision is the full snapshot with agentId, conversationId and decision.
Its continuation state describes the Follow Up; an accepted answer does not mean the work is complete.
Retry an unchanged command with the same commandId to get its recorded result with replayed: true.
Do not reuse that ID for a different request. This returns 409 validation_failed.
A stale revision or a decision that is no longer pending returns 409 revision_conflict with details.currentDecision.
Read the current decision before you submit a new command.
Malformed answer structures return 400 validation_failed.
Answers that do not match the recorded questions return 422 validation_failed.
POST /agents/:agentId/conversations/:conversationId/decision/defer
Send commandId, decisionId and expectedRevision, without answers.
This records the command receipt but keeps the decision pending. It does not submit a Follow Up.
The reply and unchanged-command retry rules are the same as for the answer route.
Both POST routes reject unknown body fields and query parameters.
If decision execution is unavailable, they return 503 gateway_offline with retryable: true.
Saved conversation drafts
These read-only routes are loopback only and need the management bearer. They return Markdown drafts saved in a conversation. They do not publish or send a draft. A saved draft does not prove that its source run or task is complete. These routes do not add a draft viewer to the client apps.
GET /agents/:agentId/conversations/:conversationId/artifacts
Returns { artifacts, nextCursor }. Each item contains metadata for the latest version of one draft.
The list is ordered by artifactId.
| Query parameter | Description |
|---|---|
limit | Items per page, from 1 to 100; default 50 |
cursor | The nextCursor from the previous page; omit for the first page |
A null cursor means there is no next page.
Metadata includes agentId, conversationId, artifactId, version, title, contentType,
sha256, byteLength, createdAt and source.
contentType is text/markdown. source identifies the runId, segmentTurnId and toolCallId.
The list does not include draft content.
GET /agents/:agentId/conversations/:conversationId/artifacts/:artifactId
Returns the metadata and content for one saved version.
Omit the version query parameter to read the latest version.
Set version to a positive integer to read an earlier version.
A saved version does not change when a later version is saved.
An unknown artifact or version returns 404 not_found.
Artifact IDs contain 1–100 letters, digits, underscores or hyphens and start with a letter or digit. Both routes reject unknown query parameters.
There is no draft-write HTTP route. The HQ save_draft tool saves drafts for a restricted agent profile.
That profile sets tools to ["save_draft"], with memory and sub-agents disabled.
The agent can also ask question cards.
A new draft uses expectedVersion: 0; a revision uses the latest saved version.
Draft content must be nonempty UTF-8 text of at most 256 KiB. After an unacknowledged save, read the artifact before you retry.