dashsquad

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:

ListenerDefault addressFlagServes
Managementhttp://127.0.0.1:9300--management-portAll routes on this page, plus /mobile/v1
Channel serverhttp://127.0.0.1:9200--channel-portThe /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:

MarkMeaning
Loopback onlyThe route is on the management listener only, with the management bearer
Also under /mobile/v1The 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:

VariableTokenAccepted on
DASHSQUAD_HQ_TOKENManagement bearerAll routes outside /mobile/v1
DASHSQUAD_HQ_CHAT_TOKENMobile bearerOn 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:

PathPurpose
GET /healthLiveness probe
GET /mobile/v1/healthLiveness 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.

ShapeExampleUsed 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:

StatusBodyWhen
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

MethodPathMobilePurpose
GET/healthYesHQ status and capabilities (no auth)
GET/infoNoAgent summary and supported API versions
GET/runtime/statusNoSnapshot of execution, pool, channels and relay
POST/lifecycle/shutdownNoStop the HQ cleanly
GET/identityYesHQ ID and public key
GET/agentsYesList agents
POST/agentsYesCreate an agent
GET/agents/:idYesGet one agent
PUT/agents/:idYesUpdate an agent
DELETE/agents/:idYesDelete an agent
POST/agents/:id/disableYesDisable an agent
POST/agents/:id/enableYesEnable an agent
POST/agents/:agentId/conversation-titleNoMake a conversation title
GET/agents/:id/memoryYesList the memories of an agent
GET/agents/:id/memory/:nameYesRead one memory
PUT/agents/:id/memory/:nameNoCreate or replace a memory
DELETE/agents/:id/memory/:nameYesDelete a memory
GET/agents/:id/memory/configNoRead memory settings
PATCH/agents/:id/memory/configNoChange memory settings
GET/agents/:id/skillsYes (read-only view)List the skills of an agent
POST/agents/:id/skillsNoCreate a skill
GET/agents/:id/skills/:nameNoRead one skill
PUT/agents/:id/skills/:nameNoReplace the body of a skill
DELETE/agents/:id/skills/:nameNoRemove a skill
POST/agents/:id/skills/installNoInstall a skill from a source
GET/agents/:id/skills/configNoRead skill settings
PATCH/agents/:id/skills/configNoChange skill settings
GET/agents/:id/skills/:name/lessonsNoRead the lessons of a learned skill
DELETE/agents/:id/skills/:name/lessons/:lessonIdNoRetire one lesson
GET/agents/:id/subagent-typesYesResolved sub-agent roster
GET/agents/:id/subagent-definitionsYesList the definition files of an agent
GET/agents/:id/subagent-definitions/:nameYesRead one definition
PUT/agents/:id/subagent-definitions/:nameYesWrite one definition
DELETE/agents/:id/subagent-definitions/:nameYesDelete one definition
GET/conversations/:id/subagentsYesList the sub-agents of a conversation
POST/subagents/:id/stopYesStop a sub-agent
POST/subagents/:id/resumeYesSend a follow-up message to a sub-agent
GET/agents/:id/swarm/runsNoList swarm runs
GET/agents/:id/swarm/runs/:runIdNoGet one swarm run
POST/agents/:id/swarm/runs/:runId/workers/:workerId/cancelNoCancel one worker
POST/agents/:id/swarm/runs/:runId/workers/:workerId/sendNoSend a message to one worker
POST/agents/:id/conversations/:conversationId/swarm/cancelNoCancel the swarm turn of a conversation
GET/browser/statusNoBrowser status and install progress
POST/browser/installNoStart the download of the managed browser
GET/agents/:id/browser/profilesNoList the browser profiles of an agent
DELETE/agents/:id/browser/profiles/:nameNoDelete a browser profile
GET/channelsNoList channels
POST/channelsNoCreate a channel
GET/channels/:nameNoGet one channel
PUT/channels/:nameNoUpdate a channel
DELETE/channels/:nameNoRemove a channel
GET/credentialsNoList credential keys
POST/credentialsNoSet a credential
DELETE/credentials/:keyNoDelete a credential
POST/credentials/provider-oauthNoSave a complete provider subscription credential
PUT/credentials/provider/:provider/:nameNoReplace a provider credential with an API key
DELETE/credentials/provider/:provider/:nameNoRemove a provider credential and its refresh data
GET/modelsYes (read-only view)List available models
POST/models/refreshNoGet a new model list now
GET/speech/configYesSpeech settings and provider status
PATCH/speech/configYesChange speech settings
GET/speech/modelsYesList speech models
POST/speech/transcriptionsYesTranscribe a clip
POST/speech/speechYesMake speech from text
GET/message-routing/configNoRead the Steer and Follow Up routing settings
PATCH/message-routing/configNoChange the Steer and Follow Up routing settings
GET/runtime/mcp/serversNoList MCP servers
POST/runtime/mcp/serversNoAdd an MCP server
GET/runtime/mcp/servers/:nameNoGet one MCP server
DELETE/runtime/mcp/servers/:nameNoRemove an MCP server
POST/runtime/mcp/servers/:name/reconnectNoConnect an MCP server again
POST/runtime/mcp/servers/:name/reauthorizeNoStart the sign-in of an MCP server again
POST/runtime/mcp/servers/:name/cancel-authorizationNoCancel a pending connector authorization
GET/runtime/mcp/allowlistNoRead the MCP URL allowlist
PUT/runtime/mcp/allowlistNoReplace the MCP URL allowlist
GET/pluginsNoList plugins
POST/plugins/installNoInstall a plugin
PUT/plugins/:nameNoEnable, disable, trust or untrust a plugin
DELETE/plugins/:nameNoRemove a plugin
POST/plugins/reloadNoFind plugins again
GET/runtime/pluginsNoPlugin providers and names for pickers
GET/eventsYesServer-sent event stream
GET/agents/:agentId/conversations/:conversationId/eventsYesReplay the durable events of a conversation
GET, PATCH/agents/:id/autonomyYesDaily limit and unattended usage of an agent
GET/conversations/:id/goalYesThe latest goal of a conversation
POST/conversations/:id/goal/commandsYesCreate, pause, resume, clear or change a goal
GET/agents/:id/goalsYesThe goals of an agent
GET, POST/agents/:id/routinesYesList routines and wake-ups, or create a routine
GET, PATCH, DELETE/routines/:routineIdYesRead, change or delete one routine
POST/routines/:routineId/runYesStart one run of a routine now
GET/routines/:routineId/runsYesThe run history of a routine
GET/agents/:id/threadsYesSquad message threads of an agent
GET/threads/:threadIdYesOne thread and its messages
POST/threads/:threadId/pause, /resume, /closeYesPause, resume or close a thread
GET/needs-youYesItems 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"
  ]
}
FieldTypeDescription
status"healthy"Always "healthy" while the HQ serves requests
startedAtstringISO timestamp of the start of this HQ process
pidnumberHQ process ID
agentsnumberNumber of registered agents
channelsnumberNumber of configured channels
apiVersion1The same on /health and /mobile/v1/health
contractVersion1The mobile contract version. An older HQ does not send this field
versionstringHQ software version at startup; loopback /health only
capabilitiesstring[]Features that depend on the HQ's runtime state

Capabilities:

CapabilityWhen present
providers-v1When the HQ loads plugin providers, so the mobile /providers routes exist
speech-v1When a speech provider can transcribe and speak. If the provider check fails, the HQ omits this value. The request does not fail
long-horizon-v1When 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-v1Always on a current HQ. The HQ attributes provider failures (providerFailure) and accepts retries (retryOf)
chat-archive-v1Always on a current HQ. The archive and restore routes for one chat exist
cancel-step-v1Always on a current HQ. The chat WebSocket accepts cancel_step
agent-rename-v1Always on a current HQ. PUT /agents/:id accepts name
chat-tools-v1Always 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 }
}
FieldDescription
execution.acceptingtrue when the HQ accepts new work
execution.activeCanonicalTurnsActive conversation turns. This count includes turns that still clean up after a cancel
execution.activeLegacyTurnsActive channel turns or legacy turns. This count includes cleanup after a cancel
execution.quiescingAgentsNumber of agents that cannot start new turns now
poolCurrent runtime count, capacity, pinned runtime count, and counts by agent ID
channelsName and health of each active channel: connected, connecting, disconnected or needs_reauth
relay.connectiondisabled, connecting, connected, disconnected or stopped
relay.activeStreamsNumber 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.

StatuscodeWhen
400validation_failedThe 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)
404not_foundNo agent has this ID
409validation_failedThe HQ has started to delete the agent. Or the name is in use: details is { "field": "name", "reason": "taken" }
500gateway_offlineUnexpected 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

FieldTypeNotes
namestringNecessary on create. Not blank, and unique. The HQ compares names without case. See PUT /agents/:id to rename an agent
modelstringNecessary on create. Not blank, in the form provider/model
systemPromptstringNecessary on create. Can be empty
fallbackModelsstring[]Strings that are not blank
toolsstring[]Tool names that are not blank. See Tools
skillsobjectOnly paths, urls, learning (auto, on, off), minToolCalls (integer, 0 or more)
providerApiKeysobjectProvider ID to key string. The HQ stores it and never returns it
workspacestringIf you omit it or send an empty string, the HQ sets a default workspace
maxTokensinteger1 or more
mcpServersstring[]Names of the MCP servers that this agent can use
swarmobjectOnly enabled, maxConcurrentWorkers, maxWorkersPerRun, maxSteersPerWorker, maxRunSeconds, allowedModels
subagentsobjectOnly enabled, delegation (auto or explicit), allowedTypes, allowedModels, maxConcurrent, maxPerTurn, maxRunSeconds, maxDepth, modelAliases. See Configuration
pluginsstring[] or nullPlugins that this agent can use. null clears the list
providersstring[] or nullProviders that this agent can use. null clears the list
messagingobjectOnly 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:

Statusdetails.reasonWhen
400emptyThe name is empty
400too_longThe name has more than 64 characters. details.maxLength is 64
400invalidThe name is not a string, or has a line break or a control character
409takenAnother 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.

FieldTypeDescription
textstringNecessary. The message to make a title for. The HQ removes spaces at the start and end
{ "title": "Release planning" }

Errors use the plain shape:

StatusWhen
400Invalid JSON, or text is missing or empty
404Unknown agent ({"error":"agent not found"})
502The 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:

StatusWhen
400Invalid JSON, a body that is not an object, or an incorrect name, description, type or content
404Unknown agent or memory
409The agent has 200 memories, and this write adds one more
503Memory 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".

FieldTypeRules
descriptionstringOne line, 1 to 200 characters
typestringuser, feedback, project or reference
contentstring1 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.

FieldTypeDescription
enabledbooleanfalse turns off the memory prompt, the memory tools and the review after each turn
sweepstringauto, 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:

StatusWhen
400Invalid JSON, an unknown settings key, or an incorrect learning or minToolCalls value
404Unknown agent, skill or lesson
409A skill with that name exists
422The 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.

FieldTypeDescription
namestringSkill name
descriptionstringOne-line description that the agent sees
contentstringThe 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.

FieldTypeDescription
sourcestringNecessary. The location of the skill
namestringOptional. 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.

FieldTypeDescription
pathsstring[]More local skill directories
urlsstring[]More remote skill sources
learningstringauto, on or off. Controls if the agent records lessons after real work
minToolCallsintegerThe 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.

StatusWhen
400Invalid JSON, empty raw, or raw larger than 256 KiB
422The 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.

FieldTypeDescription
messagestringNecessary. Not blank
requestIdstringOptional 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.

QueryDescription
refresh=1Look 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 }
}
FieldDescription
sourceconfig, chrome, edge, managed, or null when the HQ found no browser that works
pathThe browser executable, when the HQ found one
managedBuildIdThe installed managed build, if there is one
unverifiedPath, unverifiedSourceA browser on disk that did not start in time. Try again. Do not download a browser
install.stateidle, downloading, verifying, done or failed. Byte counts are in done and total. error is optional
liveSessionsThe number of open browser sessions
sizesApproximate 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.

StatusWhen
400Invalid JSON, a missing necessary field, a field of the wrong type, or routing that names an unknown agent
404Unknown channel
409A 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}.

FieldTypeDescription
namestringNecessary. Unique channel name
adapterstringNecessary. telegram or whatsapp
routingarrayNecessary. Routing rules. Each agentId must exist
globalDenyListstring[]Optional. The HQ ignores these senders on all routes
allowedUsersstring[]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:

KeyMeaning
<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>:tokenA 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}.

FieldTypeDescription
providerstringNecessary. Nonempty provider ID, such as openai or anthropic. No colon or control characters
namestringNecessary. Nonempty key name, with no control characters
accessstringNecessary. Nonblank access token, at most 8,192 characters
refreshstringNecessary. Nonblank refresh token, at most 8,192 characters
expiresnumberNecessary. 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"
}
FieldDescription
models[].valueThe provider/model ID for the model field of an agent
sourcelive after the HQ gets the lists from the providers. bootstrap when the HQ has no provider key
errorsProvider ID to error text, for the providers that failed
fetchedAtThe time when the HQ got the list
supportedModelsReviewedAtReview 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:

StatuscodeMeaning
400validation_failedMalformed request
400invalidThe provider rejected the request as not valid
401unauthorizedThe provider rejected its API key. This is not an HQ bearer failure
413too_largeThe body or the audio is larger than a size limit
413too_longThe text has more than 4,000 characters
500configuration_unavailableThe HQ cannot safely read or preserve its speech settings (retryable: false)
502providerThe provider returned an error
502networkThe HQ could not connect to the provider (retryable: true)
503unavailableThe 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.

FieldRules
stt.provider, stt.modelNot empty, a maximum of 200 characters
stt.language2 to 8 characters, or null to go back to automatic detection
tts.provider, tts.model, tts.voiceNot empty, a maximum of 200 characters
tts.speedNumber from 0.25 to 4
realtime.providerNecessary 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.

QueryDescription
kindNecessary. 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.

FieldTypeDescription
audiostringNecessary. Base64 audio, not empty, a maximum of 8 MiB after decode
formatstringNecessary. wav, m4a, mp3, flac, ogg, webm or aac
languagestringOptional 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.mp3

The 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"
}
FieldDescription
config.modeauto (the classifier decides) or ask (the client asks the user). Default auto
config.modelThe classifier model. Default typesafe/jev-1.13
config.steerMinConfidenceMinimum confidence for a Steer result, from 0 to 1. Default 0.8
config.followUpMinConfidenceMinimum confidence for a Follow Up result, from 0 to 1. Default 0.5
config.timeoutMsTime limit for the classifier, in milliseconds. Default 1500
config.baseUrlOptional. The classifier endpoint. If it is not set, the HQ uses OpenRouter
effectiveModeauto or ask. The mode that the HQ uses now
reasonno_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.

FieldRules
modeauto or ask
modelNot empty
steerMinConfidence, followUpMinConfidenceNumber from 0 to 1
timeoutMsInteger from 100 to 10,000
baseUrlAn 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:

typeFields
stdiocommand, optional args
sseurl, optional headers
streamable-httpurl, 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.

FieldTypeDescription
namestringNecessary. Letters, digits, - and _, without __
transportobjectNecessary. See the table above
envobjectOptional environment variables for a stdio server
authobjectOptional 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
toolTimeoutnumberOptional 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
maxReconnectAttemptsnumberOptional. 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" }
StatusWhen
400Invalid JSON, or name or transport is missing
403The server URL is not on the allowlist
409A server with that name exists, its removal is in progress, or it disconnected before the response
500The 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.

FieldTypeDescription
sourcestringNecessary. The location of the plugin
namestringOptional. 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.

StatusWhen
400Invalid JSON or an empty source
404The HQ did not find the source
409A plugin with that name exists, or a built-in plugin has that name
422The 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.

FieldTypeDescription
enabledbooleantrue when the plugin loads
trustedbooleantrue 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"]}

: keepalive

The HQ sends a : keepalive comment each 30 seconds.

typeFields
agent:config-changedagent (name), fields (["*"] on create, ["removed"] on delete)
channel:createdchannel
channel:config-changedchannel, fields
channel:removedchannel
channel:restartedchannel, reason
conversation:changedconversationId, revision
conversation:deletedconversationId, revision
mcp:server-addedserver
mcp:server-removedserver
plugin:config-changedplugin, fields
plugin:installedplugin
plugin:removedplugin
plugin:reloadednone
swarm:run-changedagentId, 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.

QueryDescription
sinceSeqOptional 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:

MethodPathSame 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:

FieldDescription
id, revisionDecision identity and current revision
sourceOriginal runId, segmentTurnId, toolCallId and event seq
questionsThe recorded question card questions and options
statepending or answered
answersRecorded answers, when present
continuationFollow Up inputId, runId and current state, when present
createdAt, updatedAtISO 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 parameterDescription
limitItems per page, from 1 to 100; default 50
cursorThe 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.

On this page

OverviewAuthenticationError shapesRequest checksRoute indexHealth and statusGET /healthGET /infoGET /runtime/statusPOST /lifecycle/shutdownGET /identityAgentsAgent objectConfiguration fieldsGET /agentsPOST /agentsGET /agents/:idPUT /agents/:idDELETE /agents/:idPOST /agents/:id/disablePOST /agents/:id/enablePOST /agents/:agentId/conversation-titleMemoryGET /agents/:id/memoryGET /agents/:id/memory/:namePUT /agents/:id/memory/:nameDELETE /agents/:id/memory/:nameGET /agents/:id/memory/configPATCH /agents/:id/memory/configSkillsGET /agents/:id/skillsPOST /agents/:id/skillsGET /agents/:id/skills/:namePUT /agents/:id/skills/:nameDELETE /agents/:id/skills/:namePOST /agents/:id/skills/installGET /agents/:id/skills/configPATCH /agents/:id/skills/configGET /agents/:id/skills/:name/lessonsDELETE /agents/:id/skills/:name/lessons/:lessonIdSub-agentsGET /agents/:id/subagent-typesGET /agents/:id/subagent-definitionsGET /agents/:id/subagent-definitions/:namePUT /agents/:id/subagent-definitions/:nameDELETE /agents/:id/subagent-definitions/:nameGET /conversations/:id/subagentsPOST /subagents/:id/stopPOST /subagents/:id/resumeSwarm panelGET /agents/:id/swarm/runsGET /agents/:id/swarm/runs/:runIdPOST /agents/:id/swarm/runs/:runId/workers/:workerId/cancelPOST /agents/:id/swarm/runs/:runId/workers/:workerId/sendPOST /agents/:id/conversations/:conversationId/swarm/cancelBrowserGET /browser/statusPOST /browser/installGET /agents/:id/browser/profilesDELETE /agents/:id/browser/profiles/:nameChannelsChannel objectGET /channelsPOST /channelsGET /channels/:namePUT /channels/:nameDELETE /channels/:nameCredentialsGET /credentialsPOST /credentialsDELETE /credentials/:keyPOST /credentials/provider-oauthPUT /credentials/provider/:provider/:nameDELETE /credentials/provider/:provider/:nameModelsGET /modelsPOST /models/refreshSpeechGET /speech/configPATCH /speech/configGET /speech/modelsPOST /speech/transcriptionsPOST /speech/speechMessage routingGET /message-routing/configPATCH /message-routing/configMCP serversMCP server objectGET /runtime/mcp/serversPOST /runtime/mcp/serversGET /runtime/mcp/servers/:nameDELETE /runtime/mcp/servers/:namePOST /runtime/mcp/servers/:name/reconnectPOST /runtime/mcp/servers/:name/reauthorizePOST /runtime/mcp/servers/:name/cancel-authorizationGET /runtime/mcp/allowlistPUT /runtime/mcp/allowlistPluginsGET /pluginsPOST /plugins/installPUT /plugins/:nameDELETE /plugins/:namePOST /plugins/reloadGET /runtime/pluginsEventsGET /eventsGET /agents/:agentId/conversations/:conversationId/eventsConversations and chatConversation decisionsGET /agents/:agentId/conversations/:conversationId/decisionPOST /agents/:agentId/conversations/:conversationId/decision/answerPOST /agents/:agentId/conversations/:conversationId/decision/deferSaved conversation draftsGET /agents/:agentId/conversations/:conversationId/artifactsGET /agents/:agentId/conversations/:conversationId/artifacts/:artifactId