Skip to main content
The Management API lets you monitor and control the HQ programmatically. It’s the same API Desktop uses to deploy agents, connect messaging apps, and store credentials. Its unprefixed administrative surface binds to 127.0.0.1 on port 9300 by default. Native clients do not receive that administrative access. On a local network they use the HQ’s pinned HTTPS/WSS listener on the configured mobile port, which defaults to 9400. Both frozen pairing port fields carry that same listener port. The listener exposes only /mobile/v1 and /ws/chat. Paths in the Conversations section are relative to /mobile/v1. A relay exposes the same native surface over TLS on port 443.

Authentication

The health resource — GET /health, also available as GET /mobile/v1/health — is the only management resource that does not require authentication. Desktop loopback clients continue to use the management Bearer for unprefixed management routes and the separate chat token accepted by the loopback channel server on port 9200:
Native clients instead receive one phone-scoped Mobile bearer. They send it in the Authorization header for every authenticated /mobile/v1 HTTP or SSE request and for the /ws/chat WebSocket upgrade:
Native WebSocket clients send that same bearer in the Authorization header, never in the URL. When a request travels through the hosted relay, the device additionally sends x-dash-relay-credential. The relay credential authorizes that device at the relay; it does not replace the Mobile bearer enforced by the HQ.

Health

GET /health

Returns HQ status and supported mobile capabilities. No authentication is required. The same response is available from GET /mobile/v1/health.
Response
Use conversation-sync-v1 to enable HQ-authoritative conversation lists and messages. Use chat-resume-v1 to enable durable sequences, replay, and resumable WebSocket turns.

Runtime status

GET /runtime/status

Returns a current operational snapshot on the loopback Management API. Requires the management Bearer token. This endpoint is unavailable through /mobile/v1, the mobile LAN listener, or the hosted relay.
GET /health remains the lightweight liveness check. Runtime status reports process state; it does not test provider availability or imply that queued work has finished. It contains no credentials, conversation text, or pending-message contents.

Identity

GET /identity

Returns the HQ’s stable ID and Ed25519 public key. Native clients call GET /mobile/v1/identity with their phone-scoped Mobile bearer. The unprefixed loopback GET /identity alias requires the administrative management bearer instead.
The public key identifies the HQ and is safe to return to clients. The corresponding private key never leaves the HQ machine.

Agents

Create and manage agents. New and updated agents are persisted to ~/.dash/gateway/agents.json. Native clients use these routes beneath /mobile/v1. Mobile create accepts name, model, and systemPrompt; mobile update accepts model and/or systemPrompt. Agent responses never include provider API keys. The unprefixed routes remain available for Desktop and support its full configuration fields. Agent skills are managed under the unprefixed agent routes — GET/POST /agents/:id/skills, GET/PUT/DELETE /agents/:id/skills/:name, POST /agents/:id/skills/install, and GET/PATCH /agents/:id/skills/config. See Skills. Example: create a mobile agent

Conversations

The HQ is the authority for capable clients’ conversation metadata, messages, active turn, revision, and replay sequence. All routes below use the /mobile/v1 native base and require the phone-scoped Mobile bearer. Conversation pages default to 50 items in newest-updated order. Pass nextCursor back unchanged as cursor; cursors are opaque. Message pages default to 100 items. They select the newest page, return that page in chronological order, and paginate backward by passing nextCursor as before. throughSeq tells the client which durable sequence the page includes through.

Create and read a conversation

Creation returns HTTP 201, a ConversationSummary, and an ETag containing the quoted revision:
Reusing the same requestId returns the original conversation instead of creating a duplicate. GET /conversations/:id also returns the current ETag.

Update with ETag and If-Match

Pass the revision you last read in a quoted If-Match header:
A successful change increments the revision and returns a new ETag. If another client changed the conversation first, the HQ returns HTTP 409 with revision_conflict. Replace the cached summary with details.current, then retry deliberately with that current revision.
If a turn is active, deleting returns HTTP 409 with conversation_busy and the owning activeTurnId. Resume or cancel that turn before retrying with a freshly read revision.

Deletion and archived history

Successful DELETE /conversations/:id immediately removes its messages and replay entries, then returns a summary whose status is deleted and whose revision has advanced. The HQ retains that tombstone so GET remains deterministic. A later PATCH or repeated DELETE returns HTTP 410 with a non-retryable not_found error. Deleting an agent behaves differently: the HQ archives its conversations instead of deleting them. Their saved summaries, messages, and replay entries remain readable through the conversation routes even though the agent can no longer start a turn.

Resumable chat

Desktop loopback clients use the channel server on port 9200, including its browser-compatible query-token fallback:
Native clients connect to the same unprefixed path on the pinned mobile listener and keep the Mobile bearer out of the URL. This example uses the default port; use the port from the pairing payload when the HQ configured an override:
For relay connections, use wss://<relay-host>/ws/chat on port 443 with the same Authorization header plus x-dash-relay-credential. Only use resumable frames after health advertises chat-resume-v1. A capable send includes resumable: true and uses one stable turn ID:
The first durable server frame accepts the message and assigns its message IDs and sequence:
Client frames Server frames Every server frame with a durable seq is persisted before it is broadcast. Admission errors sent before a turn is accepted have no sequence and are not journaled or replayable. Durable sequences are conversation-global and increase across turns. After a connection gap, call GET /agents/:agentId/conversations/:conversationId/events?sinceSeq=<last-seq> on the versioned management API, apply entries in ascending sequence order, then send resume with the same turn ID and last durable sequence. Do not resend the message with a new turn ID. Closing a socket only detaches that resumable subscriber; provider work continues at the HQ. Send cancel to stop it. Messages without resumable: true retain the legacy connection-owned behavior and are cancelled when their socket closes. Clients must preserve or safely ignore unknown event variants so newer HQs remain compatible. message also accepts an optional modality: 'text' | 'voice'. The HQ sets this to 'voice' only for the turn that its own hands-free voice session starts on the user’s behalf (see Voice frames below) — it appends the spoken-mode system prompt for that turn only. A dictated message is still ordinary typed text once transcribed: a client sending it as a message frame must omit modality or send 'text', never 'voice'.

Voice frames

Hands-free voice mode adds ten frames (five client, five server) to the same /ws/chat socket used for resumable chat, gated on the same speech-v1 capability as the Speech routes. id is the client-generated session id and is echoed on every voice_* frame in both directions. Client frames Server frames
Session rules:
  • One voice session per socket. Sending voice_start while a session is already open stops the previous one with voice_stopped { reason: 'replaced' } before the new session starts.
  • voice_audio sent before the HQ’s own voice_state listening is silently dropped, not rejected — the phone begins streaming the instant it sends voice_start, before the HQ has confirmed the speech provider is available.
  • The voice_transcript carrying the turnId that starts a turn is always emitted before the resumable hub’s accepted for that turn, so a client can render the optimistic user row before the turn is confirmed.
  • The HQ never sends voice_state { state: 'stopped' }, even though stopped is a valid enum value on the wire; every session end is a voice_stopped { reason } frame instead.
  • voice_audio.pcm decodes to at most 16384 bytes (16 KiB) per frame; one byte over answers the ordinary error frame with code: 'validation_failed', not voice_error.
  • After the last voice_speech of a turn the HQ stays in speaking until the client sends voice_played with a seq at least as high as that chunk’s, or until an 8-second safety timer fires. Clients that flush playback when the session leaves speaking must send it, or every reply’s last sentence is cut off. A voice_played for audio a barge-in discarded should not be sent; a stale or out-of-order seq is ignored.

Channels

Connect and manage messaging apps such as Telegram and WhatsApp. Channels are persisted to ~/.dash/gateway/channels.json. See Messaging Apps for routing rules and access control.

Credentials

Store provider API keys and messaging tokens in the HQ’s encrypted store (~/.dash/gateway/credentials.enc). See Secrets.

Models

Native clients use GET /mobile/v1/models. The model list is fetched from provider APIs and cached. Adding or changing a credential refreshes it on the next request. See AI Providers.

Speech

Dictation and read-aloud support. Mounted on both the unprefixed administrative surface and /mobile/v1; native clients use the /mobile/v1 paths. Hands-free voice mode is a separate, WebSocket-based flow on the same speech-v1 capability — see Voice frames under Resumable chat. GET /health advertises the speech-v1 capability only while a speech provider can actually transcribe and speak; an HQ without a configured provider omits it, and older clients must tolerate capability strings they don’t recognize. PATCH /speech/config is a shallow per-section merge: an omitted key keeps its current value, and an unknown key anywhere in the body is a 400 rather than a silent drop. stt.language is the one field where the distinction between omitted and null matters — omitting it leaves the language alone, while sending null clears it back to the provider’s own auto-detection:
POST /speech/transcriptions takes base64 audio inline rather than multipart, so every client sends a plain JSON body:
The decoded audio must be at most 8 MiB (413 too_large); a clip is expected to be at most 60 seconds. The HQ also fast-rejects on a Content-Length above 12 MB before reading the body at all, so an oversized upload is refused without being buffered. POST /speech/speech is the only operation in this namespace whose success body isn’t JSON — it streams MPEG audio, except for a PCM-only model (currently Google’s Gemini TTS), which comes back as a complete audio/wav file instead of a stream. text is capped at 4,000 characters (413 too_long):
An error raised before the first audio chunk is still a JSON MobileApiError, so a client must accept audio/mpeg, audio/wav, and application/json on this route. /speech/* reuses the MobileApiError envelope but passes the provider-level error code through untranslated: unauthorized (the provider rejected the API key), unavailable (no provider is configured — retryable), too_large / too_long (payload over a limit), provider (the provider returned an error), network (the HQ couldn’t reach it), and invalid (a malformed request).

Events

The unprefixed GET /events and replay routes remain available for Desktop. Native clients use the corresponding /mobile/v1 routes listed under Conversations. The HQ’s durable event log lets clients replay missed chat frames without sending the prompt to the model again. See Architecture.

Error responses

Mobile v1 failures use one structured shape:
details is present only when the error has structured recovery data. Public codes are unauthorized, not_found, validation_failed, revision_conflict, conversation_busy, rate_limited, gateway_offline, and capability_required. Respect retryable; for rate limits, also honor the server-provided retry delay. Some unprefixed compatibility routes return the older { "error": "..." } shape. Native clients should use /mobile/v1 and the structured errors above.
The loopback administrative API on port 9300 and desktop channel server on port 9200 retain their separate management and chat credentials. Native clients do not receive the administrative bearer: the pinned mobile listener uses one phone-scoped Mobile bearer for both /mobile/v1 and /ws/chat.