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:
Authorization header for every authenticated /mobile/v1 HTTP or SSE request and for the
/ws/chat WebSocket upgrade:
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 fromGET /mobile/v1/health.
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 callGET /mobile/v1/identity with their phone-scoped Mobile bearer. The unprefixed loopback
GET /identity alias requires the administrative management bearer instead.
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
201, a ConversationSummary, and an ETag containing the quoted revision:
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 quotedIf-Match header:
409 with revision_conflict. Replace the cached
summary with details.current, then retry deliberately with that current revision.
409 with conversation_busy and the owning
activeTurnId. Resume or cancel that turn before retrying with a freshly read revision.
Deletion and archived history
SuccessfulDELETE /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 port9200, including its browser-compatible
query-token fallback:
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:
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
- One voice session per socket. Sending
voice_startwhile a session is already open stops the previous one withvoice_stopped { reason: 'replaced' }before the new session starts. voice_audiosent before the HQ’s ownvoice_state listeningis silently dropped, not rejected — the phone begins streaming the instant it sendsvoice_start, before the HQ has confirmed the speech provider is available.- The
voice_transcriptcarrying theturnIdthat starts a turn is always emitted before the resumable hub’sacceptedfor 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 thoughstoppedis a valid enum value on the wire; every session end is avoice_stopped { reason }frame instead. voice_audio.pcmdecodes to at most 16384 bytes (16 KiB) per frame; one byte over answers the ordinaryerrorframe withcode: 'validation_failed', notvoice_error.- After the last
voice_speechof a turn the HQ stays inspeakinguntil the client sendsvoice_playedwith aseqat least as high as that chunk’s, or until an 8-second safety timer fires. Clients that flush playback when the session leavesspeakingmust send it, or every reply’s last sentence is cut off. Avoice_playedfor audio a barge-in discarded should not be sent; a stale or out-of-orderseqis 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:
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):
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 unprefixedGET /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.